From patchwork Thu Jan 22 04:11:43 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 1774 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1769055198; bh=EWhJMefZ7D4SJ+OegXAvKLelX4S+tlurY6h9CpoUxQk=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=bjRDR1Zo15rZ+cZ4rozDyLi3p8J6zUBiUy6MGrba502vIBPAaEg2gCJksYQ2ymTP1 XMK0fKDi85lOLVc0McwtBjbWMBYH0NPvDo3Ajbo+uRWyKfpBaB7sEnbRI3hymrQeOp 8df/Ciypo6EwSauMNU5dSbTdm3HkucP/kLJzyUJAInA2nQepc+iAgxlAKpL0p0ErBu pLkL2Km5gqkeyZVFyu30i1ogK8tV0OWzajKcD980eh7FA0yEjXZSDNcV05IPOEwhHN GqEKaI5rZ3jovnicrHoDWPByPvER1anM7xci45aGBI3+Qvl3ATaO2vBg0uslyUGleo 4cdWgQdpWTOLA== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 41BAA6962E for ; Wed, 21 Jan 2026 21:13:18 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id V1kSLigWJmfq for ; Wed, 21 Jan 2026 21:13:18 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1769055198; bh=EWhJMefZ7D4SJ+OegXAvKLelX4S+tlurY6h9CpoUxQk=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=bjRDR1Zo15rZ+cZ4rozDyLi3p8J6zUBiUy6MGrba502vIBPAaEg2gCJksYQ2ymTP1 XMK0fKDi85lOLVc0McwtBjbWMBYH0NPvDo3Ajbo+uRWyKfpBaB7sEnbRI3hymrQeOp 8df/Ciypo6EwSauMNU5dSbTdm3HkucP/kLJzyUJAInA2nQepc+iAgxlAKpL0p0ErBu pLkL2Km5gqkeyZVFyu30i1ogK8tV0OWzajKcD980eh7FA0yEjXZSDNcV05IPOEwhHN GqEKaI5rZ3jovnicrHoDWPByPvER1anM7xci45aGBI3+Qvl3ATaO2vBg0uslyUGleo 4cdWgQdpWTOLA== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 2CF406962A for ; Wed, 21 Jan 2026 21:13:18 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1769055196; bh=GcwvnKpP78aYDtubhDyEj6xteEhBIcx2VYPjzFKXuNk=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=AEGHExwa9JRTM0vq5mGjQghTcnVKCkZWb0DaRi3H+eRlP1Y92UjxTibr/7I1HiSY2 vP7cZVLJ57Aurr25led0fuc9XyAIzmU1D4nu4HgvBxbSmRPPJzaws0RukKUz/W/Kfk J1ps+GlOZy7FDBC55hqvmYom0Qo6lZU3bFhfGZOhhXMseksTCJOGf0F5tOWO9HdUuu mQWJRZptuyZluRx2uY8uLyXB9wrKIFlh4JgFl1Pc5aIKJSa0EVPXVrq9PTEpSIp7g5 Fqf85fO8MK3dg8603wBIFaKdItCGeHnee6ejuHTRCTgQeHS7TKd6+FGotx9srd36G1 OL+/Gdq0C89sA== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 562CF69616; Wed, 21 Jan 2026 21:13:16 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id j2BCDyAhbGpz; Wed, 21 Jan 2026 21:13:16 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1769055192; bh=lGH6RdQU89zNm9u4x77toaDqgmK7Q8DUfM9anQ4ZqjM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=WSDH8mecLRSiInNateZntRn6+Z1/nF2bLC1KzP9zQaaFX374W5N6h/5NZTL2bOVB+ yEjILSmdHZ3Ht5fmzOYZJLYMxYAR+aWjT1Ga8qIbWs6oj/eSNMjeE0CdyF9/Bp5nU9 p3Xd8Q93HfvHwBk+jvQtWNfjjcO2Hxp37mJaXU4HKbTgkai32pJgeRDDKWUwtYFvvl F+fYaAcyY6PFsH1mv3Cbd5yaKf6aTu0NX26LEcZ4jYY8gGZWR5wkQUjbiXGNQld3ok xvtJEwPYSJoP6OxFlTZyJXDmSOu1nl4kzQlsWtBORafNkVXrKwVOXaAO1gS+tehtrU tBK/d2XQmEPGA== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id F0AB8694CC; Wed, 21 Jan 2026 21:13:11 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Wed, 21 Jan 2026 21:11:43 -0700 Message-ID: <20260122041155.174721-16-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260122041155.174721-1-sjg@u-boot.org> References: <20260122041155.174721-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: MZ4Y25CVPU4WYICSHE4BQ3CCWANECFHA X-Message-ID-Hash: MZ4Y25CVPU4WYICSHE4BQ3CCWANECFHA X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass , "Claude Opus 4 . 5" X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 15/16] expo: Add visual line navigation for multi-line textedit List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass 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 Signed-off-by: Simon Glass --- 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 #include #include +#include #include #include #include @@ -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));