[Concept,15/16] expo: Add visual line navigation for multi-line textedit

Message ID 20260122041155.174721-16-sjg@u-boot.org
State New
Headers
Series expo: Add multiline editing support for textedit |

Commit Message

Simon Glass Jan. 22, 2026, 4:11 a.m. UTC
  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(-)
  

Patch

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));