[PATCH 00/16] expo: Add multiline editing support for textedit
From: Simon Glass <simon.glass@canonical.com> This series enhances the expo text-input handling, particularly for the textedit object which supports multi-line text. The vidconsole uses a single shared context for cursor state, which causes problems when text-input objects need their own cursor blinking. This series adds dedicated vidconsole contexts for text input, then removes the old entry save/restore mechanism which is no longer needed. For multiline editing , this series adds support for navigating between visual lines using Ctrl-P and Ctrl-N. The expo textedit uses text measurement to position the cursor at the correct pixel location within multi-line text, maintaining the horizontal position when moving between lines. The series also fixes some bugs in text-input handling and adds some tests for rendering and keypress-handling. Simon Glass (16): video: Update vidconsole_idle() to process all contexts expo: Add tin parameter to scene_txtin_close() test: expo: Use expo_enter_mode() in textline render test expo: Use a dedicated vidconsole-context for text input expo: Drop entry save/restore in textline rendering video: Remove vidconsole_entry_save/restore() test: video: Add tests for independent vidconsole contexts expo: Fix text-input close on BKEY_SELECT expo: Fix text-input buffer size for CLI editing test: expo: Add open/close testing to expo_render_textedit test: expo: Add keypress tests to expo_render_textedit cli: Add multiline support to CLI line editing video: Add len parameter to vidconsole_measure() expo: support cursor positioning for multiline textedits expo: Add visual line navigation for multi-line textedit test: expo: Add more keypress tests for textedit boot/scene.c | 7 +- boot/scene_internal.h | 13 +- boot/scene_txtin.c | 218 +++++++++++++++++++++++++----- common/cli_readline.c | 17 +++ drivers/video/console_normal.c | 32 ----- drivers/video/console_truetype.c | 43 +----- drivers/video/vidconsole-uclass.c | 50 ++----- include/cli.h | 9 +- include/expo.h | 4 +- include/video_console.h | 56 +------- test/boot/expo.c | 142 +++++++++++++++++++ test/dm/video.c | 109 +++++++++------ 12 files changed, 458 insertions(+), 242 deletions(-) -- 2.43.0 base-commit: ad61e1d21a99f0342d7b70a6b3e997fd36dacbff branch: expg
From: Simon Glass <simon.glass@canonical.com> Update vidconsole_idle() to handle cursor display for all contexts, not just the default one. This ensures that cursors in client-allocated contexts (such as expo text-input objects) are properly shown during idle time. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- drivers/video/vidconsole-uclass.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/drivers/video/vidconsole-uclass.c b/drivers/video/vidconsole-uclass.c index b6bd3133037..513c8f3bacb 100644 --- a/drivers/video/vidconsole-uclass.c +++ b/drivers/video/vidconsole-uclass.c @@ -1064,9 +1064,8 @@ void vidconsole_set_bitmap_font(struct udevice *dev, struct vidconsole_ctx *ctx, ctx->xstart_frac = 0; } -void vidconsole_idle(struct udevice *dev) +static void vidconsole_idle_ctx(struct udevice *dev, struct vidconsole_ctx *ctx) { - struct vidconsole_ctx *ctx = vidconsole_ctx(dev); struct vidconsole_cursor *curs = &ctx->curs; /* Only handle cursor if it's enabled */ @@ -1080,6 +1079,15 @@ void vidconsole_idle(struct udevice *dev) } } +void vidconsole_idle(struct udevice *dev) +{ + struct vidconsole_priv *priv = dev_get_uclass_priv(dev); + struct vidconsole_ctx **ctxp; + + alist_for_each(ctxp, &priv->ctx_list) + vidconsole_idle_ctx(dev, *ctxp); +} + #ifdef CONFIG_CURSOR void vidconsole_readline_start(struct udevice *dev, void *vctx, bool indent) { -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a text-input info parameter to scene_txtin_close() to prepare for passing the vidconsole context when closing. For now the caller passes NULL but this will be updated in a later commit. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene_internal.h | 3 ++- boot/scene_txtin.c | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/boot/scene_internal.h b/boot/scene_internal.h index d9ca1fef90e..71cbc85c358 100644 --- a/boot/scene_internal.h +++ b/boot/scene_internal.h @@ -588,8 +588,9 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, * Close out the text editor after use * * @scn: Scene containing the object + * @tin: Text-input info */ -void scene_txtin_close(struct scene *scn); +void scene_txtin_close(struct scene *scn, struct scene_txtin *tin); /** * scene_obj_calc_bbox() - Calculate bounding boxes for an object diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index 9b8edcc6439..1dfa3a02a6a 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -106,7 +106,7 @@ static void scene_txtin_putch(struct cli_line_state *cls, int ch) vidconsole_put_char(scn->expo->cons, NULL, ch); } -void scene_txtin_close(struct scene *scn) +void scene_txtin_close(struct scene *scn, struct scene_txtin *tin) { /* cursor is not needed now */ vidconsole_readline_end(scn->expo->cons, NULL); @@ -180,7 +180,7 @@ int scene_txtin_send_key(struct scene_obj *obj, struct scene_txtin *tin, memcpy(abuf_data(&tin->buf), abuf_data(&scn->buf), abuf_size(&scn->buf)); - scene_txtin_close(scn); + scene_txtin_close(scn, NULL); } else { event->type = EXPOACT_QUIT; log_debug("menu quit\n"); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The expo_render_textline test renders text-input objects without calling expo_enter_mode(). This leaves manual_sync disabled, allowing the video idle function to run and show cursors unexpectedly during rendering. Add expo_enter_mode() at the start of the test and expo_exit_mode() at the end to enable manual sync mode, preventing idle cursor updates from interfering with the framebuffer comparisons. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/boot/expo.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/boot/expo.c b/test/boot/expo.c index f94927eb6b7..2ace10994b6 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -1433,6 +1433,7 @@ static int expo_render_textline(struct unit_test_state *uts) ut_assertok(create_test_expo(uts, &exp, &scn, &menu, &buf, &logo_copy)); dev = exp->display; + expo_enter_mode(exp); id = scene_textline(scn, "textline", OBJ_TEXTLINE, 20, &tline); ut_assert(id > 0); @@ -1528,6 +1529,7 @@ static int expo_render_textline(struct unit_test_state *uts) abuf_uninit(&buf); abuf_uninit(&logo_copy); + expo_exit_mode(exp); expo_destroy(exp); return 0; -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Currently text-input objects (textline, textedit) use the default vidconsole context (NULL). This causes the cursor state to be shared with other vidconsole operations, leading to interference. Add a dedicated vidconsole context (ctx) to struct scene_txtin and use it for all vidconsole operations. The context is created when the text-input is opened and disposed when the object is destroyed. Also add scene_txtin_destroy() to properly clean up the text buffer and vidconsole context when text-input objects are destroyed. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene.c | 3 +++ boot/scene_internal.h | 10 ++++++++++ boot/scene_txtin.c | 37 ++++++++++++++++++++++++++++--------- include/expo.h | 2 ++ 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/boot/scene.c b/boot/scene.c index 7cef20dbb1b..1ea0d1b1c46 100644 --- a/boot/scene.c +++ b/boot/scene.c @@ -108,6 +108,9 @@ void scene_obj_destroy(struct scene_obj *obj) { if (obj->type == SCENEOBJT_MENU) scene_menu_destroy((struct scene_obj_menu *)obj); + else if (obj->type == SCENEOBJT_TEXTLINE || + obj->type == SCENEOBJT_TEXTEDIT) + scene_txtin_destroy(obj->scene, scene_obj_txtin(obj)); free(obj->name); free(obj); } diff --git a/boot/scene_internal.h b/boot/scene_internal.h index 71cbc85c358..3c22b10c2f4 100644 --- a/boot/scene_internal.h +++ b/boot/scene_internal.h @@ -521,6 +521,16 @@ void scene_menu_calc_bbox(struct scene_obj_menu *menu, */ int scene_txtin_init(struct scene_txtin *tin, uint size, uint line_chars); +/** + * scene_txtin_destroy() - Destroy a text-input object's resources + * + * Frees memory allocated for the text buffer and vidconsole context + * + * @scn: Scene containing the object + * @tin: Text-input info to destroy + */ +void scene_txtin_destroy(struct scene *scn, struct scene_txtin *tin); + /** * scene_txtin_arrange() - Arrange common parts of a text-input object * diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index 1dfa3a02a6a..457d782b4cf 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -14,6 +14,7 @@ #include <menu.h> #include <video_console.h> #include <linux/errno.h> +#include <linux/kernel.h> #include <linux/string.h> #include "scene_internal.h" @@ -30,6 +31,13 @@ int scene_txtin_init(struct scene_txtin *tin, uint size, uint line_chars) return 0; } +void scene_txtin_destroy(struct scene *scn, struct scene_txtin *tin) +{ + abuf_uninit(&tin->buf); + if (tin->ctx && scn->expo->cons) + vidconsole_ctx_dispose(scn->expo->cons, tin->ctx); +} + int scene_txtin_arrange(struct scene *scn, struct expo_arrange_info *arr, struct scene_obj *obj, struct scene_txtin *tin) { @@ -66,6 +74,7 @@ int scene_txtin_render_deps(struct scene *scn, struct scene_obj *obj, struct cli_line_state *cls = &tin->cls; const bool open = obj->flags & SCENEOF_OPEN; struct udevice *cons = scn->expo->cons; + void *ctx = tin->ctx; uint i; /* if open, render the edit text on top of the background */ @@ -75,16 +84,16 @@ int scene_txtin_render_deps(struct scene *scn, struct scene_obj *obj, ret = vidconsole_entry_restore(cons, &scn->entry_save); if (ret) return log_msg_ret("sav", ret); - scene_render_obj(scn, tin->edit_id, NULL); + scene_render_obj(scn, tin->edit_id, ctx); /* move cursor back to the correct position */ for (i = cls->num; i < cls->eol_num; i++) - vidconsole_put_char(cons, NULL, '\b'); + vidconsole_put_char(cons, ctx, '\b'); ret = vidconsole_entry_save(cons, &scn->entry_save); if (ret) return log_msg_ret("sav", ret); - vidconsole_show_cursor(cons, NULL); + vidconsole_show_cursor(cons, ctx); } return 0; @@ -101,15 +110,16 @@ int scene_txtin_render_deps(struct scene *scn, struct scene_obj *obj, */ static void scene_txtin_putch(struct cli_line_state *cls, int ch) { + struct scene_txtin *tin = container_of(cls, struct scene_txtin, cls); struct scene *scn = cls->priv; - vidconsole_put_char(scn->expo->cons, NULL, ch); + vidconsole_put_char(scn->expo->cons, tin->ctx, ch); } void scene_txtin_close(struct scene *scn, struct scene_txtin *tin) { /* cursor is not needed now */ - vidconsole_readline_end(scn->expo->cons, NULL); + vidconsole_readline_end(scn->expo->cons, tin->ctx); } int scene_txtin_open(struct scene *scn, struct scene_obj *obj, @@ -118,8 +128,17 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, struct cli_line_state *cls = &tin->cls; struct udevice *cons = scn->expo->cons; struct scene_obj_txt *txt; + void *ctx; int ret; + ctx = tin->ctx; + if (!ctx) { + ret = vidconsole_ctx_new(cons, &ctx); + if (ret) + return log_msg_ret("ctx", ret); + tin->ctx = ctx; + } + /* Copy the text into the scene buffer in case the edit is cancelled */ memcpy(abuf_data(&scn->buf), abuf_data(&tin->buf), abuf_size(&scn->buf)); @@ -129,8 +148,8 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, if (!txt) return log_msg_ret("cur", -ENOENT); - vidconsole_set_cursor_pos(cons, NULL, txt->obj.bbox.x0, txt->obj.bbox.y0); - vidconsole_entry_start(cons, NULL); + vidconsole_set_cursor_pos(cons, ctx, txt->obj.bbox.x0, txt->obj.bbox.y0); + vidconsole_entry_start(cons, ctx); cli_cread_init(cls, abuf_data(&tin->buf), tin->line_chars); cls->insert = true; cls->putch = scene_txtin_putch; @@ -141,7 +160,7 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, return log_msg_ret("sav", ret); /* make sure the cursor is visible */ - vidconsole_readline_start(cons, NULL, true); + vidconsole_readline_start(cons, ctx, true); return 0; } @@ -180,7 +199,7 @@ int scene_txtin_send_key(struct scene_obj *obj, struct scene_txtin *tin, memcpy(abuf_data(&tin->buf), abuf_data(&scn->buf), abuf_size(&scn->buf)); - scene_txtin_close(scn, NULL); + scene_txtin_close(scn, tin); } else { event->type = EXPOACT_QUIT; log_debug("menu quit\n"); diff --git a/include/expo.h b/include/expo.h index afaf1e8d107..adbe02922ab 100644 --- a/include/expo.h +++ b/include/expo.h @@ -509,6 +509,7 @@ struct scene_menitem { * @edit_id: ID of the editable text object (not string ID) * @line_chars: Nominal number of characters in a line * @buf: Text buffer containing current text + * @ctx: Vidconsole context for this text-input object * @cls: CLI line state for text editing */ struct scene_txtin { @@ -516,6 +517,7 @@ struct scene_txtin { uint edit_id; uint line_chars; struct abuf buf; + void *ctx; struct cli_line_state cls; }; -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> With per-object vidconsole contexts, each text-input object maintains its own cursor position and state. The entry save/restore calls in scene_txtin_render_deps() are no longer needed since the context persists between renders. Remove the vidconsole_entry_save() and vidconsole_entry_restore() calls, simplifying the rendering code. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene_txtin.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index 457d782b4cf..1829fabf9cc 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -79,19 +79,11 @@ int scene_txtin_render_deps(struct scene *scn, struct scene_obj *obj, /* if open, render the edit text on top of the background */ if (open) { - int ret; - - ret = vidconsole_entry_restore(cons, &scn->entry_save); - if (ret) - return log_msg_ret("sav", ret); scene_render_obj(scn, tin->edit_id, ctx); /* move cursor back to the correct position */ for (i = cls->num; i < cls->eol_num; i++) vidconsole_put_char(cons, ctx, '\b'); - ret = vidconsole_entry_save(cons, &scn->entry_save); - if (ret) - return log_msg_ret("sav", ret); vidconsole_show_cursor(cons, ctx); } -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> With per-object vidconsole contexts, entry save/restore is no longer needed. Each text-input object maintains its own context which persists across renders and key handling. Remove vidconsole_entry_save() and vidconsole_entry_restore() along with the entry_save and entry_restore ops from vidconsole drivers. Also remove the entry_save field from struct scene and the associated tests. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene.c | 2 - boot/scene_txtin.c | 17 +------- drivers/video/console_normal.c | 32 --------------- drivers/video/console_truetype.c | 34 +--------------- drivers/video/vidconsole-uclass.c | 31 -------------- include/expo.h | 2 - include/video_console.h | 48 ---------------------- test/dm/video.c | 67 ------------------------------- 8 files changed, 3 insertions(+), 230 deletions(-) diff --git a/boot/scene.c b/boot/scene.c index 1ea0d1b1c46..52cb7055eb3 100644 --- a/boot/scene.c +++ b/boot/scene.c @@ -92,7 +92,6 @@ int scene_new(struct expo *exp, const char *name, uint id, struct scene **scnp) free(scn); return log_msg_ret("buf", -ENOMEM); } - abuf_init(&scn->entry_save); INIT_LIST_HEAD(&scn->obj_head); scn->id = resolve_id(exp, id); @@ -122,7 +121,6 @@ void scene_destroy(struct scene *scn) list_for_each_entry_safe(obj, next, &scn->obj_head, sibling) scene_obj_destroy(obj); - abuf_uninit(&scn->entry_save); abuf_uninit(&scn->buf); free(scn->name); free(scn); diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index 1829fabf9cc..da8d49af04f 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -147,9 +147,6 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, cls->putch = scene_txtin_putch; cls->priv = scn; cli_cread_add_initial(cls); - ret = vidconsole_entry_save(cons, &scn->entry_save); - if (ret) - return log_msg_ret("sav", ret); /* make sure the cursor is visible */ vidconsole_readline_start(cons, ctx, true); @@ -204,20 +201,10 @@ int scene_txtin_send_key(struct scene_obj *obj, struct scene_txtin *tin, event->select.id = obj->id; key = '\n'; fallthrough; - default: { - struct udevice *cons = scn->expo->cons; - int ret; - - ret = vidconsole_entry_restore(cons, &scn->entry_save); - if (ret) - return log_msg_ret("sav", ret); - ret = cread_line_process_ch(cls, key); - ret = vidconsole_entry_save(cons, &scn->entry_save); - if (ret) - return log_msg_ret("sav", ret); + default: + cread_line_process_ch(cls, key); break; } - } return 0; } diff --git a/drivers/video/console_normal.c b/drivers/video/console_normal.c index d36a5c0ff8f..303900d73cb 100644 --- a/drivers/video/console_normal.c +++ b/drivers/video/console_normal.c @@ -133,36 +133,6 @@ static __maybe_unused int console_get_cursor_info(struct udevice *dev, return 0; } -static __maybe_unused int normal_entry_save(struct udevice *dev, - struct abuf *buf) -{ - struct console_ctx *ctx = vidconsole_ctx(dev); - const uint size = sizeof(*ctx); - - if (xpl_phase() <= PHASE_SPL) - return -ENOSYS; - - if (!abuf_realloc(buf, size)) - return log_msg_ret("sav", -ENOMEM); - - memcpy(abuf_data(buf), ctx, size); - - return 0; -} - -static __maybe_unused int normal_entry_restore(struct udevice *dev, - struct abuf *buf) -{ - struct console_ctx *ctx = vidconsole_ctx(dev); - - if (xpl_phase() <= PHASE_SPL) - return -ENOSYS; - - memcpy(ctx, abuf_data(buf), sizeof(*ctx)); - - return 0; -} - static int console_putc_xy(struct udevice *dev, void *vctx, uint x_frac, uint y, int cp) { @@ -179,8 +149,6 @@ struct vidconsole_ops console_ops = { .ctx_new = console_simple_ctx_new, #ifdef CONFIG_CURSOR .get_cursor_info = console_get_cursor_info, - .entry_save = normal_entry_save, - .entry_restore = normal_entry_restore, #endif }; diff --git a/drivers/video/console_truetype.c b/drivers/video/console_truetype.c index 25cc8241bf7..78683f8de13 100644 --- a/drivers/video/console_truetype.c +++ b/drivers/video/console_truetype.c @@ -1156,34 +1156,6 @@ static int truetype_ctx_new(struct udevice *dev, void *vctx) return 0; } -static int truetype_entry_save(struct udevice *dev, struct abuf *buf) -{ - struct console_tt_ctx *ctx = vidconsole_ctx(dev); - const uint size = sizeof(*ctx); - - if (xpl_phase() <= PHASE_SPL) - return -ENOSYS; - - if (!abuf_realloc(buf, size)) - return log_msg_ret("sav", -ENOMEM); - - memcpy(abuf_data(buf), ctx, size); - - return 0; -} - -static int truetype_entry_restore(struct udevice *dev, struct abuf *buf) -{ - struct console_tt_ctx *ctx = vidconsole_ctx(dev); - - if (xpl_phase() <= PHASE_SPL) - return -ENOSYS; - - memcpy(ctx, abuf_data(buf), sizeof(*ctx)); - - return 0; -} - static int truetype_get_cursor_info(struct udevice *dev, void *vctx) { struct console_tt_ctx *ctx = vctx; @@ -1196,9 +1168,7 @@ static int truetype_get_cursor_info(struct udevice *dev, void *vctx) return -ENOSYS; /* - * figure out where to place the cursor. This driver ignores the - * passed-in values, since an entry_restore() must have been done before - * calling this function. + * Figure out where to place the cursor. * * A current quirk is that the cursor is always at xcur_frac, since we * output characters directly to the console as they are typed by the @@ -1315,8 +1285,6 @@ struct vidconsole_ops console_truetype_ops = { .measure = truetype_measure, .nominal = truetype_nominal, .ctx_new = truetype_ctx_new, - .entry_save = truetype_entry_save, - .entry_restore = truetype_entry_restore, .get_cursor_info = truetype_get_cursor_info, .mark_start = truetype_mark_start, }; diff --git a/drivers/video/vidconsole-uclass.c b/drivers/video/vidconsole-uclass.c index 513c8f3bacb..0f9e9e35a98 100644 --- a/drivers/video/vidconsole-uclass.c +++ b/drivers/video/vidconsole-uclass.c @@ -801,37 +801,6 @@ int vidconsole_ctx_dispose(struct udevice *dev, void *vctx) return 0; } -int vidconsole_entry_save(struct udevice *dev, struct abuf *buf) -{ - struct vidconsole_ops *ops = vidconsole_get_ops(dev); - int ret; - - if (ops->entry_save) { - ret = ops->entry_save(dev, buf); - if (ret != -ENOSYS) - return ret; - } - - /* no data so make sure the buffer is empty */ - abuf_realloc(buf, 0); - - return 0; -} - -int vidconsole_entry_restore(struct udevice *dev, struct abuf *buf) -{ - struct vidconsole_ops *ops = vidconsole_get_ops(dev); - int ret; - - if (ops->entry_restore) { - ret = ops->entry_restore(dev, buf); - if (ret != -ENOSYS) - return ret; - } - - return 0; -} - #ifdef CONFIG_CURSOR int vidconsole_show_cursor(struct udevice *dev, void *vctx) { diff --git a/include/expo.h b/include/expo.h index adbe02922ab..d63fbd0c8ad 100644 --- a/include/expo.h +++ b/include/expo.h @@ -208,7 +208,6 @@ struct expo_string { * @highlight_id: ID of highlighted object, if any * @cls: cread state to use for input * @buf: Buffer for input - * @entry_save: Buffer to hold vidconsole text-entry information * @sibling: Node to link this scene to its siblings * @obj_head: List of objects in the scene */ @@ -221,7 +220,6 @@ struct scene { uint highlight_id; struct cli_line_state cls; struct abuf buf; - struct abuf entry_save; struct list_head sibling; struct list_head obj_head; }; diff --git a/include/video_console.h b/include/video_console.h index c047d45cf53..13d32dea2a9 100644 --- a/include/video_console.h +++ b/include/video_console.h @@ -405,30 +405,6 @@ struct vidconsole_ops { */ int (*ctx_dispose)(struct udevice *dev, void *ctx); - /** - * entry_save() - Save any text-entry information for later use - * - * Saves text-entry context such as a list of positions for each - * character in the string. - * - * @dev: Console device to use - * @buf: Buffer to hold saved data - * Return: 0 if OK, -ENOMEM if out of memory - */ - int (*entry_save)(struct udevice *dev, struct abuf *buf); - - /** - * entry_restore() - Restore text-entry information for current use - * - * Restores text-entry context such as a list of positions for each - * character in the string. - * - * @dev: Console device to use - * @buf: Buffer containing data to restore - * Return: 0 if OK, -ve on error - */ - int (*entry_restore)(struct udevice *dev, struct abuf *buf); - /** * get_cursor_info() - Get cursor position info * @@ -558,30 +534,6 @@ int vidconsole_ctx_new(struct udevice *dev, void **ctxp); */ int vidconsole_ctx_dispose(struct udevice *dev, void *ctx); -/** - * vidconsole_entry_save() - Save any text-entry information for later use - * - * Saves text-entry context such as a list of positions for each - * character in the string. - * - * @dev: Console device to use - * @buf: Buffer to hold saved data - * Return: 0 if OK, -ENOMEM if out of memory - */ -int vidconsole_entry_save(struct udevice *dev, struct abuf *buf); - -/** - * entry_restore() - Restore text-entry information for current use - * - * Restores text-entry context such as a list of positions for each - * character in the string. - * - * @dev: Console device to use - * @buf: Buffer containing data to restore - * Return: 0 if OK, -ve on error - */ -int vidconsole_entry_restore(struct udevice *dev, struct abuf *buf); - #ifdef CONFIG_CURSOR /** * vidconsole_show_cursor() - Show the cursor diff --git a/test/dm/video.c b/test/dm/video.c index b4a1200e481..fe6407dad35 100644 --- a/test/dm/video.c +++ b/test/dm/video.c @@ -1516,70 +1516,3 @@ static int dm_test_video_context_alloc(struct unit_test_state *uts) return 0; } DM_TEST(dm_test_video_context_alloc, UTF_SCAN_PDATA | UTF_SCAN_FDT); - -/* Test vidconsole entry save/restore */ -static int check_entry_save(struct unit_test_state *uts, struct udevice *con) -{ - struct vidconsole_priv *priv; - struct vidconsole_ctx *ctx; - int xcur_frac, ycur; - struct abuf buf; - - priv = dev_get_uclass_priv(con); - ctx = priv->ctx; - - /* Move cursor to a known position */ - vidconsole_position_cursor(con, 5, 3); - xcur_frac = ctx->xcur_frac; - ycur = ctx->ycur; - - /* Save the state */ - abuf_init(&buf); - ut_assertok(vidconsole_entry_save(con, &buf)); - - /* Move cursor to a different position */ - vidconsole_position_cursor(con, 10, 7); - ut_assert(ctx->xcur_frac != xcur_frac || ctx->ycur != ycur); - - /* Restore the state */ - ut_assertok(vidconsole_entry_restore(con, &buf)); - - /* Verify cursor is back at saved position */ - ut_asserteq(xcur_frac, ctx->xcur_frac); - ut_asserteq(ycur, ctx->ycur); - - abuf_uninit(&buf); - - return 0; -} - -/* Test entry save/restore with bitmap font */ -static int dm_test_video_entry_save(struct unit_test_state *uts) -{ - struct udevice *dev, *con; - - ut_assertok(select_vidconsole(uts, "vidconsole0")); - ut_assertok(video_get_nologo(uts, &dev)); - ut_assertok(uclass_get_device(UCLASS_VIDEO_CONSOLE, 0, &con)); - ut_assertok(vidconsole_select_font(con, NULL, "8x16", 0)); - - ut_assertok(check_entry_save(uts, con)); - - return 0; -} -DM_TEST(dm_test_video_entry_save, UTF_SCAN_PDATA | UTF_SCAN_FDT); - -/* Test entry save/restore with truetype font */ -static int dm_test_video_entry_save_tt(struct unit_test_state *uts) -{ - struct udevice *dev, *con; - - ut_assertok(video_get_nologo(uts, &dev)); - ut_assertok(uclass_get_device(UCLASS_VIDEO_CONSOLE, 0, &con)); - ut_assertok(vidconsole_select_font(con, NULL, NULL, 30)); - - ut_assertok(check_entry_save(uts, con)); - - return 0; -} -DM_TEST(dm_test_video_entry_save_tt, UTF_SCAN_PDATA | UTF_SCAN_FDT); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add tests to verify that multiple vidconsole contexts operate independently. The tests create two contexts, set different cursor positions in each, write text to each context, and verify that operations on one context do not affect the other. Two tests are added: - dm_test_video_context_indep: Tests with bitmap font - dm_test_video_context_indep_tt: Tests with truetype font Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/dm/video.c | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/test/dm/video.c b/test/dm/video.c index fe6407dad35..d3ecb62b43a 100644 --- a/test/dm/video.c +++ b/test/dm/video.c @@ -1516,3 +1516,91 @@ static int dm_test_video_context_alloc(struct unit_test_state *uts) return 0; } DM_TEST(dm_test_video_context_alloc, UTF_SCAN_PDATA | UTF_SCAN_FDT); + +/* Test that two vidconsole contexts are independent */ +static int dm_test_video_context_indep(struct unit_test_state *uts) +{ + struct vidconsole_ctx *ctx1, *ctx2; + struct udevice *dev, *con; + + ut_assertok(select_vidconsole(uts, "vidconsole0")); + ut_assertok(video_get_nologo(uts, &dev)); + ut_assertok(uclass_get_device(UCLASS_VIDEO_CONSOLE, 0, &con)); + ut_assertok(vidconsole_select_font(con, NULL, "8x16", 0)); + + /* Create two contexts */ + ut_assertok(vidconsole_ctx_new(con, (void **)&ctx1)); + ut_assertok(vidconsole_ctx_new(con, (void **)&ctx2)); + + /* Set different cursor positions in each context */ + vidconsole_set_cursor_pos(con, ctx1, 100, 50); + vidconsole_set_cursor_pos(con, ctx2, 200, 100); + + /* Verify positions are independent */ + ut_asserteq(VID_TO_POS(100), ctx1->xcur_frac); + ut_asserteq(50, ctx1->ycur); + ut_asserteq(VID_TO_POS(200), ctx2->xcur_frac); + ut_asserteq(100, ctx2->ycur); + + /* Write to ctx1, verify ctx2 is unchanged */ + vidconsole_put_string(con, ctx1, "Hello"); + ut_asserteq(VID_TO_POS(200), ctx2->xcur_frac); + ut_asserteq(100, ctx2->ycur); + + /* Write to ctx2, verify it moves independently */ + vidconsole_put_string(con, ctx2, "World"); + ut_assert(ctx2->xcur_frac > VID_TO_POS(200)); + ut_asserteq(100, ctx2->ycur); + + /* ctx1 should have moved from the first write */ + ut_assert(ctx1->xcur_frac > VID_TO_POS(100)); + ut_asserteq(50, ctx1->ycur); + + ut_assertok(vidconsole_ctx_dispose(con, ctx1)); + ut_assertok(vidconsole_ctx_dispose(con, ctx2)); + + return 0; +} +DM_TEST(dm_test_video_context_indep, UTF_SCAN_PDATA | UTF_SCAN_FDT); + +/* Test multiple contexts with truetype font */ +static int dm_test_video_context_indep_tt(struct unit_test_state *uts) +{ + struct vidconsole_ctx *ctx1, *ctx2; + struct udevice *dev, *con; + int ctx1_x, ctx2_x; + + ut_assertok(video_get_nologo(uts, &dev)); + ut_assertok(uclass_get_device(UCLASS_VIDEO_CONSOLE, 0, &con)); + ut_assertok(vidconsole_select_font(con, NULL, NULL, 30)); + + /* Create two contexts */ + ut_assertok(vidconsole_ctx_new(con, (void **)&ctx1)); + ut_assertok(vidconsole_ctx_new(con, (void **)&ctx2)); + + /* Set different cursor positions */ + vidconsole_set_cursor_pos(con, ctx1, 50, 100); + vidconsole_set_cursor_pos(con, ctx2, 300, 200); + + /* Write different text to each */ + vidconsole_put_string(con, ctx1, "First"); + ctx1_x = ctx1->xcur_frac; + + vidconsole_put_string(con, ctx2, "Second context"); + ctx2_x = ctx2->xcur_frac; + + /* Verify they moved independently */ + ut_asserteq(ctx1_x, ctx1->xcur_frac); /* ctx1 unchanged by ctx2 write */ + ut_asserteq(100, ctx1->ycur); + ut_asserteq(ctx2_x, ctx2->xcur_frac); + ut_asserteq(200, ctx2->ycur); + + /* ctx2 should have moved more (longer string) */ + ut_assert(ctx2_x - VID_TO_POS(300) > ctx1_x - VID_TO_POS(50)); + + ut_assertok(vidconsole_ctx_dispose(con, ctx1)); + ut_assertok(vidconsole_ctx_dispose(con, ctx2)); + + return 0; +} +DM_TEST(dm_test_video_context_indep_tt, UTF_SCAN_PDATA | UTF_SCAN_FDT); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> When closing a text-input object (textline or textedit) with Enter (BKEY_SELECT), scene_txtin_close() is not called. This leaves the vidconsole context in a bad state, causing rendering issues. Also, the code falls through to process '\n' via cread_line_process_ch(), which incorrectly adds a newline character to the text buffer. Fix this by calling scene_txtin_close() for BKEY_SELECT, matching the behaviour of BKEY_QUIT, and breaking instead of falling through. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene_txtin.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index da8d49af04f..43a6c00d497 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -199,8 +199,8 @@ int scene_txtin_send_key(struct scene_obj *obj, struct scene_txtin *tin, break; event->type = EXPOACT_CLOSE; event->select.id = obj->id; - key = '\n'; - fallthrough; + scene_txtin_close(scn, tin); + break; default: cread_line_process_ch(cls, key); break; -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The CLI line-editing initialisation uses tin->line_chars as the buffer size, but this is actually the nominal line width for text wrapping, not the buffer capacity. For textline objects the two are the same, but for textedit the buffer is much larger than a single line. Use abuf_size() to get the actual buffer capacity instead. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene_txtin.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index 43a6c00d497..338c7da63d3 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -142,7 +142,7 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, vidconsole_set_cursor_pos(cons, ctx, txt->obj.bbox.x0, txt->obj.bbox.y0); vidconsole_entry_start(cons, ctx); - cli_cread_init(cls, abuf_data(&tin->buf), tin->line_chars); + cli_cread_init(cls, abuf_data(&tin->buf), abuf_size(&tin->buf)); cls->insert = true; cls->putch = scene_txtin_putch; cls->priv = scn; -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Extend the expo_render_textedit() test to cover opening the textedit, rendering with the cursor visible, then closing it with BKEY_SELECT and verifying the display returns to its previous state. This tests the full open/close cycle similar to expo_render_textline. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/boot/expo.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/boot/expo.c b/test/boot/expo.c index 2ace10994b6..4a20b00b901 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -1542,6 +1542,7 @@ static int expo_render_textedit(struct unit_test_state *uts) struct scene_obj_txtedit *ted; struct scene_obj_menu *menu; struct abuf buf, logo_copy; + struct expo_action act; struct scene *scn; struct udevice *dev; struct expo *exp; @@ -1549,6 +1550,7 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_assertok(create_test_expo(uts, &exp, &scn, &menu, &buf, &logo_copy)); dev = exp->display; + expo_enter_mode(exp); id = scene_texted(scn, "texted", OBJ_TEXTED, 40, &ted); ut_assert(id > 0); @@ -1580,9 +1582,33 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_assertok(expo_render(exp)); ut_asserteq(21662, video_compress_fb(uts, dev, false)); + /* open the textedit and re-render */ + ut_assertok(scene_set_open(scn, OBJ_TEXTED, true)); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + + /* the cursor should be at the end */ + ut_asserteq(100, ted->tin.cls.num); + ut_asserteq(100, ted->tin.cls.eol_num); + ut_asserteq(21526, 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)); + ut_asserteq(EXPOACT_CLOSE, act.type); + ut_asserteq(OBJ_TEXTED, act.select.id); + ut_assertok(scene_set_open(scn, act.select.id, false)); + + /* check the textedit is closed */ + ut_asserteq(0, ted->obj.flags & SCENEOF_OPEN); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21662, video_compress_fb(uts, dev, false)); + abuf_uninit(&buf); abuf_uninit(&logo_copy); + expo_exit_mode(exp); expo_destroy(exp); return 0; -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Extend the textedit rendering test to verify keypress handling. Add tests for: - Character input ('X') - Cursor movement (Ctrl-B to move left) - Character deletion (Ctrl-D to delete at cursor) Also check that the text content changes as expected. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/boot/expo.c | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/test/boot/expo.c b/test/boot/expo.c index 4a20b00b901..8598f32d341 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -1592,6 +1592,35 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(100, ted->tin.cls.eol_num); 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_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21607, video_compress_fb(uts, dev, false)); + + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + + /* check cursor moved back three positions, before 'e' */ + ut_asserteq(98, ted->tin.cls.num); + ut_asserteq(101, ted->tin.cls.eol_num); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21623, video_compress_fb(uts, dev, false)); + + /* delete a character at the cursor (removes 'e') */ + ut_assertok(expo_send_key(exp, CTL_CH('d'))); + + /* check character deleted at cursor position */ + ut_asserteq(98, ted->tin.cls.num); + ut_asserteq(100, ted->tin.cls.eol_num); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21541, 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)); @@ -1599,11 +1628,14 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(OBJ_TEXTED, act.select.id); ut_assertok(scene_set_open(scn, act.select.id, false)); - /* check the textedit is closed */ + /* check the textedit is closed and text is changed */ ut_asserteq(0, ted->obj.flags & SCENEOF_OPEN); + ut_asserteq_str("This\nis the initial contents of the text " + "editor but it is quite likely that more will be added latrX", + abuf_data(&ted->tin.buf)); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); - ut_asserteq(21662, video_compress_fb(uts, dev, false)); + ut_asserteq(21659, video_compress_fb(uts, dev, false)); abuf_uninit(&buf); abuf_uninit(&logo_copy); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add support for multi-line input in CLI line editing: - Add multiline bool to indicate input may contain multiple lines - Add line_nav callback for Ctrl-P/N navigation between visual lines When multiline is true, Ctrl-P/N call the line_nav callback instead of accessing command history. The callback returns the new cursor position or a negative value if at the boundary. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- common/cli_readline.c | 17 +++++++++++++++++ include/cli.h | 9 ++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/common/cli_readline.c b/common/cli_readline.c index 27fecd835a0..38f825184df 100644 --- a/common/cli_readline.c +++ b/common/cli_readline.c @@ -405,6 +405,23 @@ int cread_line_process_ch(struct cli_line_state *cls, char ichar) break; case CTL_CH('p'): case CTL_CH('n'): + if (cls->multiline && cls->line_nav) { + int new_num; + + new_num = cls->line_nav(cls, ichar == CTL_CH('p')); + if (new_num < 0) { + getcmd_cbeep(cls); + break; + } + + /* + * Just update the position - the callback handles + * cursor display since backspaces don't work across + * wrapped line boundaries + */ + cls->num = new_num; + break; + } if (cls->history) { char *hline; diff --git a/include/cli.h b/include/cli.h index a02e228bf8a..f1e5887fa56 100644 --- a/include/cli.h +++ b/include/cli.h @@ -34,10 +34,15 @@ struct cli_ch_state { * @history: true if history should be accessible * @cmd_complete: true if tab completion should be enabled (requires @prompt to * be set) + * @multiline: true if input may contain multiple lines (enables Ctrl-P/N for + * line navigation instead of history) * @buf: Buffer containing line * @prompt: Prompt for the line * @putch: Function to call to output a character (NULL to use putc()) - * @priv: Private data for putch callback + * @line_nav: Function to call for multi-line navigation (Ctrl-P/N). Called with + * @up true for previous line, false for next. Returns new cursor position, + * or -ve if at boundary + * @priv: Private data for callbacks */ struct cli_line_state { uint num; @@ -46,9 +51,11 @@ struct cli_line_state { bool insert; bool history; bool cmd_complete; + bool multiline; char *buf; const char *prompt; void (*putch)(struct cli_line_state *cls, int ch); + int (*line_nav)(struct cli_line_state *cls, bool up); void *priv; }; -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a len parameter to vidconsole_measure() to allow measuring a substring of the input text. This is useful for measuring text up to a specific cursor position, which is needed for cursor-positioning with multi-line text input. Also return the position of the character where the substring ended, needed for cursor positioning. Pass -1 for len to measure the whole string (existing behaviour). Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene.c | 2 +- drivers/video/console_truetype.c | 9 ++++++--- drivers/video/vidconsole-uclass.c | 7 ++++--- include/video_console.h | 8 ++++++-- test/dm/video.c | 6 +++--- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/boot/scene.c b/boot/scene.c index 52cb7055eb3..ff21b524843 100644 --- a/boot/scene.c +++ b/boot/scene.c @@ -538,7 +538,7 @@ int scene_obj_get_hw(struct scene *scn, uint id, int *widthp) obj->req_bbox.x1 - obj->req_bbox.x0 : -1; ret = vidconsole_measure(scn->expo->cons, gen->font_name, - gen->font_size, str, limit, &bbox, + gen->font_size, str, -1, limit, &bbox, &gen->lines); if (ret) return log_msg_ret("mea", ret); diff --git a/drivers/video/console_truetype.c b/drivers/video/console_truetype.c index 78683f8de13..25df22e31b0 100644 --- a/drivers/video/console_truetype.c +++ b/drivers/video/console_truetype.c @@ -1004,12 +1004,12 @@ static int truetype_select_font(struct udevice *dev, void *vctx, } static int truetype_measure(struct udevice *dev, const char *name, uint size, - const char *text, int pixel_limit, + const char *text, int len, int pixel_limit, struct vidconsole_bbox *bbox, struct alist *lines) { struct console_tt_metrics *met; struct vidconsole_mline mline; - const char *s, *last_space; + const char *s, *last_space, *end; int width, last_width; stbtt_fontinfo *font; int lsb, advance; @@ -1030,6 +1030,7 @@ static int truetype_measure(struct udevice *dev, const char *name, uint size, if (pixel_limit != -1) limit = tt_ceil((double)pixel_limit / met->scale); + end = len < 0 ? NULL : text + len; font = &met->font; width = 0; bbox->y1 = 0; @@ -1037,7 +1038,7 @@ static int truetype_measure(struct udevice *dev, const char *name, uint size, start = 0; last_space = NULL; last_width = 0; - for (lastch = 0, s = text; *s; s++) { + for (lastch = 0, s = text; *s && s != end; s++) { int neww; int ch = *s; @@ -1069,6 +1070,7 @@ static int truetype_measure(struct udevice *dev, const char *name, uint size, mline.bbox.x0 = 0; mline.bbox.y0 = bbox->y1; mline.bbox.x1 = tt_ceil((double)width * met->scale); + mline.xpos = (int)((double)width * met->scale); bbox->x1 = max(bbox->x1, mline.bbox.x1); bbox->y1 += met->font_size; mline.bbox.y1 = bbox->y1; @@ -1095,6 +1097,7 @@ static int truetype_measure(struct udevice *dev, const char *name, uint size, mline.bbox.x0 = 0; mline.bbox.y0 = bbox->y1; mline.bbox.x1 = tt_ceil((double)width * met->scale); + mline.xpos = (int)((double)width * met->scale); bbox->y1 += met->font_size; mline.bbox.y1 = bbox->y1; mline.start = start; diff --git a/drivers/video/vidconsole-uclass.c b/drivers/video/vidconsole-uclass.c index 0f9e9e35a98..d13b4eac272 100644 --- a/drivers/video/vidconsole-uclass.c +++ b/drivers/video/vidconsole-uclass.c @@ -665,7 +665,7 @@ int vidconsole_select_font(struct udevice *dev, void *ctx, const char *name, } int vidconsole_measure(struct udevice *dev, const char *name, uint size, - const char *text, int limit, + const char *text, int len, int limit, struct vidconsole_bbox *bbox, struct alist *lines) { struct vidconsole_ctx *ctx = vidconsole_ctx(dev); @@ -675,7 +675,8 @@ int vidconsole_measure(struct udevice *dev, const char *name, uint size, if (ops->measure) { if (lines) alist_empty(lines); - ret = ops->measure(dev, name, size, text, limit, bbox, lines); + ret = ops->measure(dev, name, size, text, len, limit, bbox, + lines); if (ret != -ENOSYS) return ret; } @@ -683,7 +684,7 @@ int vidconsole_measure(struct udevice *dev, const char *name, uint size, bbox->valid = true; bbox->x0 = 0; bbox->y0 = 0; - bbox->x1 = ctx->x_charsize * strlen(text); + bbox->x1 = ctx->x_charsize * (len < 0 ? strlen(text) : len); bbox->y1 = ctx->y_charsize; return 0; diff --git a/include/video_console.h b/include/video_console.h index 13d32dea2a9..1861138c152 100644 --- a/include/video_console.h +++ b/include/video_console.h @@ -221,11 +221,13 @@ struct vidconsole_bbox { * vidconsole_mline - Holds information about a line of measured text * * @bbox: Bounding box of the line, assuming it starts at 0,0 + * @xpos: Cursor x position at end of line (truncated, not ceiled like bbox.x1) * @start: String index of the first character in the line * @len: Number of characters in the line */ struct vidconsole_mline { struct vidconsole_bbox bbox; + int xpos; int start; int len; }; @@ -355,6 +357,7 @@ struct vidconsole_ops { * @name: Font name to use (NULL to use default) * @size: Font size to use (0 to use default) * @text: Text to measure + * @len: Number of characters to measure, or -1 for whole string * @limit: Width limit for each line, or -1 if none * @bbox: Returns bounding box of text, assuming it is positioned * at 0,0 @@ -365,7 +368,7 @@ struct vidconsole_ops { * Returns: 0 on success, -ENOENT if no such font */ int (*measure)(struct udevice *dev, const char *name, uint size, - const char *text, int limit, + const char *text, int len, int limit, struct vidconsole_bbox *bbox, struct alist *lines); /** @@ -485,6 +488,7 @@ int vidconsole_select_font(struct udevice *dev, void *ctx, const char *name, * @name: Font name to use (NULL to use default) * @size: Font size to use (0 to use default) * @text: Text to measure + * @len: Number of characters to measure, or -1 for whole string * @limit: Width limit for each line, or -1 if none * @bbox: Returns bounding box of text, assuming it is positioned * at 0,0 @@ -495,7 +499,7 @@ int vidconsole_select_font(struct udevice *dev, void *ctx, const char *name, * Returns: 0 on success, -ENOENT if no such font */ int vidconsole_measure(struct udevice *dev, const char *name, uint size, - const char *text, int limit, + const char *text, int len, int limit, struct vidconsole_bbox *bbox, struct alist *lines); /** * vidconsole_nominal() - Measure the expected width of a line of text diff --git a/test/dm/video.c b/test/dm/video.c index d3ecb62b43a..92b2ee9a6e3 100644 --- a/test/dm/video.c +++ b/test/dm/video.c @@ -982,7 +982,7 @@ static int dm_test_font_measure(struct unit_test_state *uts) ut_assertok(uclass_get_device(UCLASS_VIDEO_CONSOLE, 0, &con)); vidconsole_position_cursor(con, 0, 0); alist_init_struct(&lines, struct vidconsole_mline); - ut_assertok(vidconsole_measure(con, NULL, 0, test_string, -1, &bbox, + ut_assertok(vidconsole_measure(con, NULL, 0, test_string, -1, -1, &bbox, &lines)); ut_asserteq(0, bbox.x0); ut_asserteq(0, bbox.y0); @@ -1013,8 +1013,8 @@ static int dm_test_font_measure(struct unit_test_state *uts) ut_asserteq(strlen(test_string + nl + 1), line->len); /* now use a limit on the width */ - ut_assertok(vidconsole_measure(con, NULL, 0, test_string, limit, &bbox, - &lines)); + ut_assertok(vidconsole_measure(con, NULL, 0, test_string, -1, limit, + &bbox, &lines)); ut_asserteq(0, bbox.x0); ut_asserteq(0, bbox.y0); ut_asserteq(0x31e, bbox.x1); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> For multiline text input, using backspaces to position the cursor does not work correctly across wrapped line boundaries: the vidconsole cursor-position tracking goes wrong when backspacing at the start of a wrapped line. Fix this by using direct cursor-positioning via a call to vidconsole_set_cursor_pos() in scene_txtin_render_deps(), using the line-measurement info attached to the text object to calculate the correct position for the cursor. Single-line textline objects continue to use the backspace approach. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene_txtin.c | 56 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index 338c7da63d3..72552222fe1 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -68,6 +68,46 @@ int scene_txtin_arrange(struct scene *scn, struct expo_arrange_info *arr, return x; } +/** + * set_cursor_pos() - Set cursor position for multiline text + * + * Finds the visual line containing the cursor and sets the cursor position + * to the correct pixel location within that line. + * + * @cons: Vidconsole device + * @ctx: Vidconsole context + * @txt: Text object containing line measurement info + * @buf: Text buffer + * @pos: Cursor position in buffer + */ +static void set_cursor_pos(struct udevice *cons, void *ctx, + struct scene_obj_txt *txt, const char *buf, uint pos) +{ + const struct vidconsole_mline *mline, *res; + struct vidconsole_bbox bbox; + struct alist lines; + uint i; + + alist_init_struct(&lines, struct vidconsole_mline); + 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) + continue; + if (vidconsole_measure(cons, txt->gen.font_name, + txt->gen.font_size, buf + mline->start, + pos - mline->start, -1, &bbox, &lines)) + break; + /* measured text is within a single line, so only one result */ + res = alist_get(&lines, 0, struct vidconsole_mline); + if (!res) + break; + vidconsole_set_cursor_pos(cons, ctx, txt->obj.bbox.x0 + res->xpos, + txt->obj.bbox.y0 + mline->bbox.y0); + break; + } + alist_uninit(&lines); +} + int scene_txtin_render_deps(struct scene *scn, struct scene_obj *obj, struct scene_txtin *tin) { @@ -81,9 +121,19 @@ int scene_txtin_render_deps(struct scene *scn, struct scene_obj *obj, if (open) { scene_render_obj(scn, tin->edit_id, ctx); - /* move cursor back to the correct position */ - for (i = cls->num; i < cls->eol_num; i++) - vidconsole_put_char(cons, ctx, '\b'); + if (cls->multiline) { + /* for multiline, set cursor position directly */ + struct scene_obj_txt *txt; + + txt = scene_obj_find(scn, tin->edit_id, SCENEOBJT_NONE); + if (txt) + set_cursor_pos(cons, ctx, txt, cls->buf, + cls->num); + } else { + /* for single-line, use backspaces */ + for (i = cls->num; i < cls->eol_num; i++) + vidconsole_put_char(cons, ctx, '\b'); + } vidconsole_show_cursor(cons, ctx); } -- 2.43.0
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
From: Simon Glass <simon.glass@canonical.com> Add tests for additional keypress operations in expo_render_textedit: - Ctrl-W twice to delete words ("lik" then "quite ") - Ctrl-B four times to move back 4 characters - Ctrl-N to move cursor to next visual line at same x position - Ctrl-A to go to start of buffer - Ctrl-D to delete character at cursor - Ctrl-E to go to end of buffer - Backspace to delete character before cursor Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/boot/expo.c | 63 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/test/boot/expo.c b/test/boot/expo.c index 8006044c9c0..824fd0cca38 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -1646,6 +1646,63 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(240, ctx->ycur); ut_asserteq(21538, video_compress_fb(uts, dev, false)); + /* delete the word before cursor (deletes "lik" from "likely") */ + ut_assertok(expo_send_key(exp, CTL_CH('w'))); + ut_asserteq(64, ted->tin.cls.num); + ut_asserteq(97, ted->tin.cls.eol_num); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21462, video_compress_fb(uts, dev, false)); + + /* delete another word (deletes "quite ") */ + ut_assertok(expo_send_key(exp, CTL_CH('w'))); + ut_asserteq(58, ted->tin.cls.num); + ut_asserteq(91, ted->tin.cls.eol_num); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21241, video_compress_fb(uts, dev, false)); + + /* go back 4 characters */ + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_assertok(expo_send_key(exp, CTL_CH('b'))); + ut_asserteq(54, ted->tin.cls.num); + ut_asserteq(91, ted->tin.cls.eol_num); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21225, video_compress_fb(uts, dev, false)); + + /* move cursor to next visual line at same x position */ + ut_assertok(expo_send_key(exp, CTL_CH('n'))); + ut_asserteq(87, ted->tin.cls.num); + ut_asserteq(91, ted->tin.cls.eol_num); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21211, video_compress_fb(uts, dev, false)); + + /* go to start of buffer and delete a character */ + ut_assertok(expo_send_key(exp, CTL_CH('a'))); + ut_asserteq(0, ted->tin.cls.num); + ut_asserteq(91, ted->tin.cls.eol_num); + ut_assertok(expo_send_key(exp, CTL_CH('d'))); + ut_asserteq(0, ted->tin.cls.num); + ut_asserteq(90, ted->tin.cls.eol_num); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21147, video_compress_fb(uts, dev, false)); + + /* go to end of buffer and backspace */ + ut_assertok(expo_send_key(exp, CTL_CH('e'))); + ut_asserteq(90, ted->tin.cls.num); + ut_asserteq(90, ted->tin.cls.eol_num); + ut_assertok(expo_send_key(exp, '\x7f')); + ut_asserteq(89, ted->tin.cls.num); + ut_asserteq(89, ted->tin.cls.eol_num); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(21083, 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)); @@ -1655,12 +1712,12 @@ static int expo_render_textedit(struct unit_test_state *uts) /* check the textedit is closed and text is changed */ ut_asserteq(0, ted->obj.flags & SCENEOF_OPEN); - ut_asserteq_str("This\nis the initial contents of the text " - "editor but it is quite likely that more will be added latrX", + ut_asserteq_str("his\nis the initial contents of the text " + "editor but it is ely that more will be added latr", abuf_data(&ted->tin.buf)); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); - ut_asserteq(21659, video_compress_fb(uts, dev, false)); + ut_asserteq(21230, video_compress_fb(uts, dev, false)); abuf_uninit(&buf); abuf_uninit(&logo_copy); -- 2.43.0
participants (1)
-
Simon Glass