[Concept,19/24] bootctl: Provide a multi-themed UI

Message ID 20251018084117.1798704-20-sjg@u-boot.org
State New
Headers
Series bootctl: Expand bootctl to include a new UI |

Commit Message

Simon Glass Oct. 18, 2025, 8:41 a.m. UTC
  From: Simon Glass <sjg@chromium.org>

Add a new 'multi' UI which can display in a simple format (like GRUB) as
well as a more modern format with a mouse.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 boot/bootctl/Makefile   |   1 +
 boot/bootctl/multi_ui.c | 588 ++++++++++++++++++++++++++++++++++++++++
 include/bootctl.dtsi    |   9 +-
 3 files changed, 597 insertions(+), 1 deletion(-)
 create mode 100644 boot/bootctl/multi_ui.c
  

Patch

diff --git a/boot/bootctl/Makefile b/boot/bootctl/Makefile
index 4ed842fa752..0288fc038ad 100644
--- a/boot/bootctl/Makefile
+++ b/boot/bootctl/Makefile
@@ -11,4 +11,5 @@  obj-y += logic.o
 obj-y += simple_meas.o
 obj-y += simple_state.o
 obj-y += simple_ui.o
+obj-y += multi_ui.o
 obj-y += util.o
diff --git a/boot/bootctl/multi_ui.c b/boot/bootctl/multi_ui.c
new file mode 100644
index 00000000000..adf48808c0f
--- /dev/null
+++ b/boot/bootctl/multi_ui.c
@@ -0,0 +1,588 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Provides a 'multiboot' menu on a graphical display
+ *
+ * This is based on Heinrich's design shared in late August.
+ *
+ * Copyright 2025 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#define LOG_CATEGORY	UCLASS_BOOTCTL
+
+#include <alist.h>
+#include <bootctl.h>
+#include <bootstd.h>
+#include <dm.h>
+#include <expo.h>
+#include <video.h>
+#include <video_console.h>
+#include <bootctl/logic.h>
+#include <bootctl/oslist.h>
+#include <bootctl/ui.h>
+#include <bootctl/util.h>
+#include "../bootflow_internal.h"
+#include "../scene_internal.h"
+
+enum {
+	/* Bar on the left */
+	BAR_X		= 0,
+	BAR_Y		= 0,
+	BAR_W		= 70,
+	BAR_H		= 800,
+
+	HELP_Y		= 675,
+	SETTINGS_Y	= 720,
+
+	MAIN_X		= 150,
+	MAIN_Y		= 150,
+
+	IMAGES_Y	= 225,
+	BOX_W		= 300,
+	BOX_H		= 300,
+	BOX_MARGIN	= 10,
+
+	// gap between boxes
+	GAP_X		= 20,
+	GAP_Y		= 20,
+};
+
+/**
+ * struct multiboot_ui_priv - Driver-specific private data for multiboot UI
+ *
+ * @use_bootflow_props: true to use bootflow_menu_set_props(), false for multiboot_ui_set_props()
+ */
+struct multiboot_ui_priv {
+	bool use_bootflow_props;
+};
+
+static int multiboot_ui_probe(struct udevice *dev)
+{
+	struct bc_ui_priv *upriv = dev_get_uclass_priv(dev);
+	struct udevice *ldev;
+	int ret;
+
+	ret = uclass_first_device_err(UCLASS_BOOTCTL, &ldev);
+	if (ret)
+		return log_msg_ret("sup", ret);
+
+	upriv->lpriv = dev_get_priv(ldev);
+
+	return 0;
+}
+
+static int multiboot_ui_bind(struct udevice *dev)
+{
+	struct bootctl_uc_plat *ucp = dev_get_uclass_plat(dev);
+
+	ucp->desc = "Graphical or textual display for user";
+
+	return 0;
+}
+
+int setup_version(struct expo *exp, int i, const struct bootflow *bflow,
+		  const char **versp)
+{
+	const char *vers = NULL;
+	struct abuf *buf;
+	char *str;
+	int ret;
+
+	ret = expo_edit_str(exp, STR_DESC + i, NULL, &buf);
+	if (ret)
+		return ret;
+	abuf_printf(buf, bflow->os_name ? bflow->os_name : bflow->name);
+
+	str = buf->data;
+	if (!strncmp("Ubuntu", str, 6) && strlen(str) > 20) {
+		/* get the space string after the 24.04[.1] */
+		char *p = strchr(str + 8, ' ');
+
+		/* get the space after that */
+		char *q = p ? strchr(p + 1, ' ') : NULL;
+
+		vers = str + 7;
+		if (q)
+			*q = '\0';
+		else
+			*p = '\0';
+	}
+
+	if (versp)
+		*versp = vers;
+
+	return 0;
+}
+
+static int multiboot_set_item_props(struct scene *scn, int i,
+				    const struct bootflow *bflow)
+{
+	struct expo *exp = scn->expo;
+	struct abuf *buf;
+	int x, y, ret = 0;
+
+	x = MAIN_X + i * (BOX_W + GAP_X);
+	y = IMAGES_Y;
+
+	ret = setup_version(exp, i, bflow, NULL);
+
+	scene_obj_set_bbox(scn, ITEM_BOX + i, x, IMAGES_Y,
+			   x + BOX_W, IMAGES_Y + BOX_H);
+	scene_obj_set_hide(scn, ITEM_BOX + i, false);
+
+	scene_obj_set_pos(scn, ITEM_DESC + i, x + BOX_MARGIN,
+			  IMAGES_Y + 80);
+	scene_obj_set_pos(scn, ITEM_LABEL + i, x + BOX_MARGIN,
+			  IMAGES_Y + 80 + 20);
+	scene_obj_set_pos(scn, ITEM_VERSION_NAME + i, x + BOX_MARGIN,
+			  IMAGES_Y + 80 + 70);
+	scene_obj_set_pos(scn, ITEM_PREVIEW + i, x + BOX_MARGIN,
+			  IMAGES_Y + 5);
+	scene_obj_set_pos(scn, ITEM_VERIFIED + i,
+			  x + BOX_MARGIN + 40 + 32, IMAGES_Y + 80 + 21);
+
+	ret |= scene_obj_set_hide(scn, ITEM_PREVIEW + i, false);
+	ret |= scene_obj_set_hide(scn, ITEM_BOX + i, false);
+	ret |= scene_obj_set_hide(scn, ITEM_VERSION_NAME + i, false);
+	ret |= scene_obj_set_hide(scn, ITEM_VERIFIED + i, false);
+
+	/* Hide key in multiboot mode (not used with mouse) */
+	ret |= scene_obj_set_hide(scn, ITEM_KEY + i, true);
+	if (ret)
+		return log_msg_ret("msp", ret);
+
+	/* Set font sizes for multiboot UI */
+	ret = scene_txt_set_font(scn, ITEM_LABEL + i, "ubuntu_light", 18);
+	ret |= scene_txt_set_font(scn, ITEM_DESC + i, "ubuntu_bold", 20);
+	ret |= scene_txt_set_font(scn, ITEM_VERSION_NAME + i, NULL, 18);
+	if (ret)
+		return log_msg_ret("msq", ret);
+
+	ret = expo_edit_str(exp, STR_LABEL + i, NULL, &buf);
+	if (!ret)
+		abuf_printf(buf, "Canonical");
+
+	return 0;
+}
+
+static int multiboot_ui_set_props(struct udevice *dev, struct scene *scn,
+				  struct bootstd_priv *std)
+{
+	struct bc_ui_priv *upriv = dev_get_uclass_priv(dev);
+	struct logic_priv *lpriv = upriv->lpriv;
+	struct expo *exp = scn->expo;
+	struct expo_theme *theme = &exp->theme;
+	struct abuf *buf;
+	int i, ret = 0;
+
+	/* Set multiboot-specific strings */
+	ret = expo_edit_str(exp, STR_PROMPT1B, NULL, &buf);
+	if (!ret)
+		abuf_printf(buf, "Select image to boot");
+
+	ret = expo_edit_str(exp, STR_PROMPT2, NULL, &buf);
+	if (!ret)
+		abuf_printf(buf, "Images");
+
+	/* Show multiboot-specific objects */
+	scene_obj_set_hide(scn, OBJ_BOX, false);
+	scene_obj_set_hide(scn, OBJ_OTHER_LOGO, false);
+	scene_obj_set_hide(scn, OBJ_SETTINGS, false);
+	scene_obj_set_hide(scn, OBJ_HELP, false);
+	scene_obj_set_hide(scn, OBJ_POINTER, true);
+	scene_menu_set_pointer(scn, OBJ_MENU, 0);
+
+	/* Enable mouse for multiboot UI */
+	expo_set_mouse_enable(exp, true);
+
+	/* Use manual positioning for menu */
+	scene_obj_set_manual(scn, OBJ_MENU, true);
+
+	ret = expo_edit_str(upriv->expo, STR_MENU_TITLE, NULL, &buf);
+	if (ret)
+		return log_msg_ret("set", ret);
+	abuf_printf(buf, "Welcome to Multiboot");
+
+	scene_obj_set_halign(scn, OBJ_MENU_TITLE, SCENEOA_LEFT);
+	scene_obj_set_pos(scn, OBJ_MENU_TITLE, MAIN_X, 50);
+
+	scene_obj_set_pos(scn, OBJ_PROMPT1B, MAIN_X, 120);
+	scene_obj_set_halign(scn, OBJ_PROMPT1B, SCENEOA_LEFT);
+
+	scene_obj_set_pos(scn, OBJ_PROMPT2, MAIN_X, 180);
+	scene_obj_set_halign(scn, OBJ_PROMPT2, SCENEOA_LEFT);
+
+	scene_obj_set_hide(scn, OBJ_AUTOBOOT, !lpriv->opt_autoboot);
+
+	if (upriv->logo) {
+		ret = scene_obj_set_pos(scn, OBJ_U_BOOT_LOGO, 1045, 10);
+		if (ret)
+			return log_msg_ret("lop", ret);
+	}
+
+	scene_obj_set_bbox(scn, OBJ_BOX, BAR_X, BAR_Y, BAR_X + BAR_W,
+			   BAR_Y + BAR_H);
+	scene_box_set_fill(scn, OBJ_BOX, true);
+
+	scene_obj_set_bbox(scn, OBJ_OTHER_LOGO, BAR_X, BAR_Y, BAR_X + BAR_W,
+			   BAR_Y + 50);
+	scene_obj_set_halign(scn, OBJ_OTHER_LOGO, SCENEOA_CENTRE);
+
+	scene_obj_set_bbox(scn, OBJ_SETTINGS, BAR_X, SETTINGS_Y, BAR_X + BAR_W,
+			   SETTINGS_Y + 24);
+	scene_obj_set_halign(scn, OBJ_SETTINGS, SCENEOA_CENTRE);
+
+	scene_obj_set_bbox(scn, OBJ_HELP, BAR_X, HELP_Y, BAR_X + BAR_W,
+			   HELP_Y + 24);
+	scene_obj_set_halign(scn, OBJ_HELP, SCENEOA_CENTRE);
+
+	if (ofnode_valid(std->theme)) {
+		ret = expo_setup_theme(upriv->expo, std->theme);
+		if (ret)
+			return log_msg_ret("thm", ret);
+	}
+
+	theme->white_on_black = false;
+
+	ret = expo_apply_theme(exp, true);
+	if (ret)
+		return log_msg_ret("asn", ret);
+
+	scene_obj_set_manual(scn, OBJ_MENU, true);
+
+	for (i = 0; i < std->bootflows.count; i++) {
+		const struct bootflow *bflow;
+
+		bflow = alist_get(&std->bootflows, i, struct bootflow);
+		if (!bflow)
+			return log_msg_ret("mbb", -ENOENT);
+
+		multiboot_set_item_props(scn, i, bflow);
+	}
+
+	expo_set_mouse_enable(exp, true);
+
+	scene_txt_set_font(scn, OBJ_MENU_TITLE, NULL, 60);
+	scene_txt_set_font(scn, OBJ_PROMPT1B, NULL, 30);
+	scene_txt_set_font(scn, OBJ_PROMPT2, "ubuntu_bold", 30);
+
+	scene_menu_select_item(scn, OBJ_MENU, 0);
+	scene_set_highlight_id(scn, 0);
+	exp->show_highlight = false;
+
+	return 0;
+}
+
+static int multiboot_ui_print(struct udevice *dev, const char *msg)
+{
+	printf("%s", msg);
+
+	return 0;
+}
+
+static int multiboot_ui_show(struct udevice *dev)
+{
+	struct bc_ui_priv *upriv = dev_get_uclass_priv(dev);
+	struct multiboot_ui_priv *priv = dev_get_priv(dev);
+	struct bootstd_priv *std;
+	struct scene *scn;
+	struct abuf *buf;
+	uint scene_id;
+	int ret;
+
+	ret = bootstd_get_priv(&std);
+	if (ret)
+		return log_msg_ret("sdb", ret);
+	ret = bootflow_menu_setup(std, 0, &upriv->expo);
+	if (ret)
+		return log_msg_ret("sds", ret);
+
+	expo_set_mouse_enable(upriv->expo, true);
+
+	ret = expo_first_scene_id(upriv->expo);
+	if (ret < 0)
+		return log_msg_ret("ufs", ret);
+	scene_id = ret;
+	scn = expo_lookup_scene_id(upriv->expo, scene_id);
+
+	ret = expo_edit_str(upriv->expo, STR_PROMPT1B, NULL, &buf);
+	if (!ret)
+		abuf_printf(buf, "Select image to boot");
+
+	ret = expo_edit_str(upriv->expo, STR_PROMPT2, NULL, &buf);
+	if (!ret)
+		abuf_printf(buf, "Images");
+
+	ret = expo_edit_str(upriv->expo, STR_AUTOBOOT,
+			    &upriv->autoboot_template,
+			    &upriv->autoboot_str);
+	if (ret)
+		return log_msg_ret("ses", ret);
+
+	if (upriv->logo) {
+		ret = scene_img_set_data(scn, OBJ_U_BOOT_LOGO, upriv->logo,
+					 upriv->logo_size);
+		if (ret)
+			return log_msg_ret("log", ret);
+	}
+
+	ret |= scene_img(scn, "multipass", OBJ_OTHER_LOGO,
+			 video_image_getptr(multipass), NULL);
+
+	ret |= scene_img(scn, "settings", OBJ_SETTINGS,
+			 video_image_getptr(settings), NULL);
+
+	ret |= scene_img(scn, "help", OBJ_HELP,
+			 video_image_getptr(help), NULL);
+
+	log_debug("theme '%s'\n", ofnode_get_name(std->theme));
+
+	if (priv->use_bootflow_props) {
+		void *logo = upriv->logo ? (void *)upriv->logo :
+				video_get_u_boot_logo(NULL);
+
+		ret = bootflow_menu_set_props(upriv->expo, scn, logo,
+					      "Boot Control");
+		if (ret)
+			return log_msg_ret("bfprops", ret);
+	} else {
+		ret = multiboot_ui_set_props(dev, scn, std);
+		if (ret)
+			return log_msg_ret("props", ret);
+	}
+
+	ret = scene_arrange(scn);
+	if (ret)
+		return log_msg_ret("usa", ret);
+
+	upriv->scn = scn;
+
+	ret = device_find_first_child_by_uclass(upriv->expo->display,
+						UCLASS_VIDEO_CONSOLE,
+						&upriv->console);
+	if (ret)
+		return log_msg_ret("suq", ret);
+	vidconsole_set_quiet(upriv->console, true);
+	expo_enter_mode(upriv->expo);
+
+	return 0;
+}
+
+static int multiboot_ui_add(struct udevice *dev, struct osinfo *info)
+{
+	struct bc_ui_priv *upriv = dev_get_uclass_priv(dev);
+	struct logic_priv *lpriv = upriv->lpriv;
+	struct bootstd_priv *std;
+	const char *vers = NULL;
+	struct scene *scn;
+	struct abuf *buf;
+	int seq, ret;
+
+	info = alist_add(&lpriv->osinfo, *info);
+	if (!info)
+		return log_msg_ret("mua", -ENOMEM);
+	seq = bootstd_add_bootflow(&info->bflow);
+	if (seq + 1 != lpriv->osinfo.count)
+		return log_msg_ret("mdb", seq);
+	ret = bootflow_menu_add(upriv->expo, &info->bflow, seq, &scn);
+	if (ret)
+		return log_msg_ret("mda", ret);
+
+	ret = setup_version(upriv->expo, seq, &info->bflow, &vers);
+
+	if (vers) {
+		void *logo;
+
+		ret = expo_edit_str(upriv->expo, STR_LABEL + seq, NULL, &buf);
+		if (!ret)
+			abuf_printf(buf, "Canonical");
+
+		scene_obj_set_hide(scn, ITEM_VERSION_NAME + seq, false);
+		scene_txt_set_font(scn, ITEM_DESC + seq, "ubuntu_bold",
+				   20);
+		ret = expo_edit_str(upriv->expo, STR_VERSION_NAME + seq, NULL,
+				    &buf);
+		if (!ret) {
+			if (!strncmp("24.04", vers, 5))
+				abuf_printf(buf, "Noble Numbat");
+			else if (!strncmp("25.04", vers, 5))
+				abuf_printf(buf, "Plucky Puffin");
+			else if (!strncmp("22.04", vers, 5))
+				abuf_printf(buf, "Jammy Jellyfish");
+		}
+
+		logo = video_image_getptr(canonical);
+		ret |= scene_img(scn, "preview", ITEM_PREVIEW + seq, logo,
+				 NULL);
+
+		logo = video_image_getptr(tick);
+		ret |= scene_img(scn, "verified", ITEM_VERIFIED + seq, logo,
+				 NULL);
+	}
+
+	ret = bootstd_get_priv(&std);
+	if (ret)
+		return log_msg_ret("sup", ret);
+
+	multiboot_set_item_props(scn, seq, &info->bflow);
+
+	ret = expo_calc_dims(upriv->expo);
+	if (ret)
+		return log_msg_ret("ecd", ret);
+
+	if (lpriv->default_os &&
+	    !strcmp(lpriv->default_os, info->bflow.os_name))
+		scene_menu_select_item(scn, OBJ_MENU, ITEM + seq);
+	ret = scene_arrange(scn);
+	if (ret)
+		return log_msg_ret("sua", ret);
+
+	return 0;
+}
+
+static int multiboot_ui_render(struct udevice *dev)
+{
+	struct bc_ui_priv *upriv = dev_get_uclass_priv(dev);
+	int ret;
+
+	ret = abuf_printf(upriv->autoboot_str,
+			  upriv->autoboot_template.data,
+			  upriv->lpriv->autoboot_remain_s);
+	if (ret < 0)
+		return log_msg_ret("uip", ret);
+
+	ret = expo_arrange(upriv->expo);
+	if (ret)
+		return log_msg_ret("sda", ret);
+	ret = expo_render(upriv->expo);
+	if (ret)
+		return log_msg_ret("sdr", ret);
+
+	return 0;
+}
+
+static int multiboot_ui_switch_layout(struct udevice *dev)
+{
+	struct bc_ui_priv *upriv = dev_get_uclass_priv(dev);
+	struct multiboot_ui_priv *priv = dev_get_priv(dev);
+	struct bootstd_priv *std;
+	struct scene *scn;
+	int ret;
+
+	/* Toggle the layout mode */
+	priv->use_bootflow_props = !priv->use_bootflow_props;
+
+	/* Get the current scene */
+	scn = expo_lookup_scene_id(upriv->expo, upriv->expo->scene_id);
+	if (!scn)
+		return log_msg_ret("scn", -ENOENT);
+
+	ret = bootstd_get_priv(&std);
+	if (ret)
+		return log_msg_ret("std", ret);
+
+	/* Re-apply properties with the new layout */
+	if (priv->use_bootflow_props) {
+		/* Apply simple_ui style using bootflow_menu_set_props() */
+		void *logo = upriv->logo ? (void *)upriv->logo :
+					     video_get_u_boot_logo(NULL);
+
+		ret = bootflow_menu_set_props(upriv->expo, scn, logo,
+					      "Boot control");
+		if (ret)
+			return log_msg_ret("bfprops", ret);
+	} else {
+		/* Apply multiboot style using multiboot_ui_set_props() */
+		ret = multiboot_ui_set_props(dev, scn, std);
+		if (ret)
+			return log_msg_ret("props", ret);
+	}
+
+	/* Calculate dimensions then re-arrange */
+	ret = expo_calc_dims(upriv->expo);
+	if (ret)
+		return log_msg_ret("ecd", ret);
+
+	ret = scene_arrange(scn);
+	if (ret)
+		return log_msg_ret("arr", ret);
+
+	return 0;
+}
+
+static int multiboot_ui_poll(struct udevice *dev, int *seqp, bool *selectedp)
+{
+	struct bc_ui_priv *upriv = dev_get_uclass_priv(dev);
+	struct logic_priv *lpriv = upriv->lpriv;
+	int seq, ret;
+	bool ok = true;
+
+	*seqp = -1;
+	*selectedp = false;
+	ret = bootflow_menu_poll(upriv->expo, &seq);
+
+	ok = !ret;
+	if (ret == -ERESTART || ret == -EREMCHG) {
+		lpriv->autoboot_active = false;
+		scene_obj_set_hide(upriv->scn, OBJ_AUTOBOOT, true);
+		ok = true;
+	} else if (ret == -ECOMM) {
+		/* Layout change requested */
+		ret = multiboot_ui_switch_layout(dev);
+		if (ret)
+			return log_msg_ret("swl", ret);
+		ret = -ECOMM;  /* Restore the original return code */
+		ok = true;
+	} else if (ret == -EAGAIN || ret == -ENOTTY) {
+		ok = true;
+	}
+
+	*seqp = seq;
+	if (ret) {
+		if (!ok)
+			return log_msg_ret("sdp", ret);
+		if (ret == -EAGAIN || ret == -ERESTART || ret == -ECOMM ||
+		    ret == -ENOTTY)
+			return 0;
+		return 1;
+	}
+
+	*selectedp = true;
+
+	return 0;
+}
+
+static int multiboot_ui_of_to_plat(struct udevice *dev)
+{
+	struct bc_ui_priv *upriv = dev_get_uclass_priv(dev);
+
+	upriv->logo = dev_read_prop(dev, "logo", &upriv->logo_size);
+
+	return 0;
+}
+
+static struct bc_ui_ops ops = {
+	.print	= multiboot_ui_print,
+	.show	= multiboot_ui_show,
+	.add	= multiboot_ui_add,
+	.render	= multiboot_ui_render,
+	.poll	= multiboot_ui_poll,
+	.switch_layout = multiboot_ui_switch_layout,
+};
+
+static const struct udevice_id multiboot_ui_ids[] = {
+	{ .compatible = "bootctl,multiboot-ui" },
+	{ .compatible = "bootctl,ui" },
+	{ }
+};
+
+U_BOOT_DRIVER(multiboot_ui) = {
+	.name		= "multiboot_ui",
+	.id		= UCLASS_BOOTCTL_UI,
+	.of_match	= multiboot_ui_ids,
+	.of_to_plat	= multiboot_ui_of_to_plat,
+	.bind		= multiboot_ui_bind,
+	.probe		= multiboot_ui_probe,
+	.ops		= &ops,
+	.priv_auto	= sizeof(struct multiboot_ui_priv),
+};
diff --git a/include/bootctl.dtsi b/include/bootctl.dtsi
index 2d3a83c4576..d7c191770a7 100644
--- a/include/bootctl.dtsi
+++ b/include/bootctl.dtsi
@@ -115,10 +115,17 @@ 
 #endif
 	};
 
-	ui {
+	ui-multi {
 		compatible = "bootctl,multiboot-ui", "bootctl,simple-ui",
 			"bootctl,ui";
 		graphical = "if-available";
 		textual = "if-available";
 	};
+
+	ui-simple {
+		compatible = "bootctl,simple-ui", "bootctl,ui";
+		graphical = "if-available";
+		textual = "if-available";
+	};
+
 };