Index: uspace/lib/ui/src/menu.c
===================================================================
--- uspace/lib/ui/src/menu.c	(revision d7f82635cf4b387fce37f7f70fc8cc0cffde419b)
+++ uspace/lib/ui/src/menu.c	(revision d92b8e8f16d19c0ce7828d8dfbc2f178747ce076)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -35,4 +35,5 @@
 
 #include <adt/list.h>
+#include <ctype.h>
 #include <errno.h>
 #include <gfx/color.h>
@@ -43,4 +44,7 @@
 #include <stdlib.h>
 #include <str.h>
+#include <uchar.h>
+#include <ui/ui.h>
+#include <ui/accel.h>
 #include <ui/control.h>
 #include <ui/paint.h>
@@ -50,5 +54,4 @@
 #include <ui/resource.h>
 #include <ui/window.h>
-#include "../private/menubar.h"
 #include "../private/menu.h"
 #include "../private/resource.h"
@@ -58,12 +61,16 @@
 	menu_frame_h = 4,
 	menu_frame_w_text = 2,
-	menu_frame_h_text = 1
+	menu_frame_h_text = 1,
+	menu_frame_h_margin_text = 1
 };
 
 static void ui_menu_popup_close(ui_popup_t *, void *);
+static void ui_menu_popup_kbd(ui_popup_t *, void *, kbd_event_t *);
 static void ui_menu_popup_pos(ui_popup_t *, void *, pos_event_t *);
+static void ui_menu_key_press_unmod(ui_menu_t *, kbd_event_t *);
 
 static ui_popup_cb_t ui_menu_popup_cb = {
 	.close = ui_menu_popup_close,
+	.kbd = ui_menu_popup_kbd,
 	.pos = ui_menu_popup_pos
 };
@@ -71,11 +78,10 @@
 /** Create new menu.
  *
+ * @param parent Parent window
  * @param mbar Menu bar
- * @param caption Caption
  * @param rmenu Place to store pointer to new menu
  * @return EOK on success, ENOMEM if out of memory
  */
-errno_t ui_menu_create(ui_menu_bar_t *mbar, const char *caption,
-    ui_menu_t **rmenu)
+errno_t ui_menu_create(ui_window_t *parent, ui_menu_t **rmenu)
 {
 	ui_menu_t *menu;
@@ -85,12 +91,5 @@
 		return ENOMEM;
 
-	menu->caption = str_dup(caption);
-	if (menu->caption == NULL) {
-		free(menu);
-		return ENOMEM;
-	}
-
-	menu->mbar = mbar;
-	list_append(&menu->lmenus, &mbar->menus);
+	menu->parent = parent;
 	list_initialize(&menu->entries);
 
@@ -117,49 +116,18 @@
 	}
 
-	list_remove(&menu->lmenus);
 	free(menu->caption);
 	free(menu);
 }
 
-/** Get first menu in menu bar.
- *
- * @param mbar Menu bar
- * @return First menu or @c NULL if there is none
- */
-ui_menu_t *ui_menu_first(ui_menu_bar_t *mbar)
-{
-	link_t *link;
-
-	link = list_first(&mbar->menus);
-	if (link == NULL)
-		return NULL;
-
-	return list_get_instance(link, ui_menu_t, lmenus);
-}
-
-/** Get next menu in menu bar.
- *
- * @param cur Current menu
- * @return Next menu or @c NULL if @a cur is the last one
- */
-ui_menu_t *ui_menu_next(ui_menu_t *cur)
-{
-	link_t *link;
-
-	link = list_next(&cur->lmenus, &cur->mbar->menus);
-	if (link == NULL)
-		return NULL;
-
-	return list_get_instance(link, ui_menu_t, lmenus);
-}
-
-/** Get menu caption.
- *
- * @param menu Menu
- * @return Caption (owned by @a menu)
- */
-const char *ui_menu_caption(ui_menu_t *menu)
-{
-	return menu->caption;
+/** Set menu callbacks.
+ *
+ * @param menu Menu
+ * @param cb Callbacks
+ * @param arg Callback argument
+ */
+void ui_menu_set_cb(ui_menu_t *menu, ui_menu_cb_t *cb, void *arg)
+{
+	menu->cb = cb;
+	menu->arg = arg;
 }
 
@@ -173,10 +141,10 @@
     ui_menu_geom_t *geom)
 {
-	ui_resource_t *res;
 	gfx_coord2_t edim;
 	gfx_coord_t frame_w;
 	gfx_coord_t frame_h;
-
-	res = ui_window_get_res(menu->mbar->window);
+	ui_resource_t *res;
+
+	res = ui_window_get_res(menu->parent);
 
 	if (res->textmode) {
@@ -202,18 +170,4 @@
 }
 
-/** Get menu rectangle.
- *
- * @param menu Menu
- * @param spos Starting position (top-left corner)
- * @param rect Place to store menu rectangle
- */
-void ui_menu_get_rect(ui_menu_t *menu, gfx_coord2_t *spos, gfx_rect_t *rect)
-{
-	ui_menu_geom_t geom;
-
-	ui_menu_get_geom(menu, spos, &geom);
-	*rect = geom.outer_rect;
-}
-
 /** Get UI resource from menu.
  *
@@ -230,6 +184,7 @@
  * @param menu Menu
  * @param prect Parent rectangle around which the menu should be placed
- */
-errno_t ui_menu_open(ui_menu_t *menu, gfx_rect_t *prect)
+ * @param idev_id Input device associated with the menu's seat
+ */
+errno_t ui_menu_open(ui_menu_t *menu, gfx_rect_t *prect, sysarg_t idev_id)
 {
 	ui_popup_t *popup = NULL;
@@ -239,4 +194,7 @@
 	errno_t rc;
 
+	/* Select first entry */
+	menu->selected = ui_menu_entry_first(menu);
+
 	/* Determine menu dimensions */
 
@@ -248,7 +206,8 @@
 	params.rect = geom.outer_rect;
 	params.place = *prect;
-
-	rc = ui_popup_create(menu->mbar->ui, menu->mbar->window, &params,
-	    &popup);
+	params.idev_id = idev_id;
+
+	rc = ui_popup_create(ui_window_get_ui(menu->parent), menu->parent,
+	    &params, &popup);
 	if (rc != EOK)
 		return rc;
@@ -268,4 +227,96 @@
 	ui_popup_destroy(menu->popup);
 	menu->popup = NULL;
+}
+
+/** Determine if menu is open.
+ *
+ * @param menu Menu
+ * @return @c true iff menu is open
+ */
+bool ui_menu_is_open(ui_menu_t *menu)
+{
+	return menu->popup != NULL;
+}
+
+/** Paint menu.
+ *
+ * @param menu Menu
+ * @param spos Starting position (top-left corner)
+ * @return EOK on success or an error code
+ */
+errno_t ui_menu_paint_bg_gfx(ui_menu_t *menu, gfx_coord2_t *spos)
+{
+	ui_resource_t *res;
+	ui_menu_geom_t geom;
+	gfx_rect_t bg_rect;
+	errno_t rc;
+
+	res = ui_menu_get_res(menu);
+	ui_menu_get_geom(menu, spos, &geom);
+
+	/* Paint menu frame */
+
+	rc = gfx_set_color(res->gc, res->wnd_face_color);
+	if (rc != EOK)
+		goto error;
+
+	rc = ui_paint_outset_frame(res, &geom.outer_rect, &bg_rect);
+	if (rc != EOK)
+		goto error;
+
+	/* Paint menu background */
+
+	rc = gfx_set_color(res->gc, res->wnd_face_color);
+	if (rc != EOK)
+		goto error;
+
+	rc = gfx_fill_rect(res->gc, &bg_rect);
+	if (rc != EOK)
+		goto error;
+
+	return EOK;
+error:
+	return rc;
+}
+
+/** Paint menu.
+ *
+ * @param menu Menu
+ * @param spos Starting position (top-left corner)
+ * @return EOK on success or an error code
+ */
+errno_t ui_menu_paint_bg_text(ui_menu_t *menu, gfx_coord2_t *spos)
+{
+	ui_resource_t *res;
+	ui_menu_geom_t geom;
+	gfx_rect_t rect;
+	errno_t rc;
+
+	res = ui_menu_get_res(menu);
+	ui_menu_get_geom(menu, spos, &geom);
+
+	/* Paint menu background */
+
+	rc = gfx_set_color(res->gc, res->wnd_face_color);
+	if (rc != EOK)
+		goto error;
+
+	rc = gfx_fill_rect(res->gc, &geom.outer_rect);
+	if (rc != EOK)
+		goto error;
+
+	/* Paint menu box */
+
+	rect = geom.outer_rect;
+	rect.p0.x += menu_frame_h_margin_text;
+	rect.p1.x -= menu_frame_h_margin_text;
+
+	rc = ui_paint_text_box(res, &rect, ui_box_single, res->wnd_face_color);
+	if (rc != EOK)
+		goto error;
+
+	return EOK;
+error:
+	return rc;
 }
 
@@ -282,5 +333,4 @@
 	ui_menu_entry_t *mentry;
 	ui_menu_geom_t geom;
-	gfx_rect_t bg_rect;
 	errno_t rc;
 
@@ -288,21 +338,9 @@
 	ui_menu_get_geom(menu, spos, &geom);
 
-	/* Paint menu frame */
-
-	rc = gfx_set_color(res->gc, res->wnd_face_color);
-	if (rc != EOK)
-		goto error;
-
-	rc = ui_paint_outset_frame(res, &geom.outer_rect, &bg_rect);
-	if (rc != EOK)
-		goto error;
-
-	/* Paint menu background */
-
-	rc = gfx_set_color(res->gc, res->wnd_face_color);
-	if (rc != EOK)
-		goto error;
-
-	rc = gfx_fill_rect(res->gc, &bg_rect);
+	/* Paint menu frame and background */
+	if (res->textmode)
+		rc = ui_menu_paint_bg_text(menu, spos);
+	else
+		rc = ui_menu_paint_bg_gfx(menu, spos);
 	if (rc != EOK)
 		goto error;
@@ -369,8 +407,150 @@
 		/* Press outside menu - close it */
 		if (event->type == POS_PRESS)
-			ui_menu_bar_select(menu->mbar, NULL, NULL);
+			ui_menu_close_req(menu);
 	}
 
 	return ui_unclaimed;
+}
+
+/** Handle keyboard event in menu.
+ *
+ * @param menu Menu
+ * @param event Keyboard event
+ * @return ui_claimed iff the event was claimed
+ */
+ui_evclaim_t ui_menu_kbd_event(ui_menu_t *menu, kbd_event_t *event)
+{
+	if (event->type == KEY_PRESS && (event->mods &
+	    (KM_CTRL | KM_ALT | KM_SHIFT)) == 0) {
+		ui_menu_key_press_unmod(menu, event);
+	}
+
+	if (event->type == KEY_PRESS && (event->mods & KM_ALT) != 0 &&
+	    (event->mods & (KM_CTRL | KM_SHIFT)) == 0 && event->c != '\0')
+		ui_menu_press_accel(menu, event->c, event->kbd_id);
+
+	return ui_claimed;
+}
+
+/** Move one entry up.
+ *
+ * Non-selectable entries are skipped. If we are already at the top,
+ * we wrap around.
+ *
+ * @param menu Menu
+ */
+void ui_menu_up(ui_menu_t *menu)
+{
+	gfx_coord2_t mpos;
+	ui_menu_entry_t *nentry;
+
+	if (menu->selected == NULL)
+		return;
+
+	nentry = ui_menu_entry_prev(menu->selected);
+	if (nentry == NULL)
+		nentry = ui_menu_entry_last(menu);
+
+	/* Need to find a selectable entry */
+	while (!ui_menu_entry_selectable(nentry)) {
+		nentry = ui_menu_entry_prev(nentry);
+		if (nentry == NULL)
+			nentry = ui_menu_entry_last(menu);
+
+		/* Went completely around and found nothing? */
+		if (nentry == menu->selected)
+			return;
+	}
+
+	menu->selected = nentry;
+
+	mpos.x = 0;
+	mpos.y = 0;
+	(void) ui_menu_paint(menu, &mpos);
+}
+
+/** Move one entry down.
+ *
+ * Non-selectable entries are skipped. If we are already at the bottom,
+ * we wrap around.
+ *
+ * @param menu Menu
+ */
+void ui_menu_down(ui_menu_t *menu)
+{
+	gfx_coord2_t mpos;
+	ui_menu_entry_t *nentry;
+
+	if (menu->selected == NULL)
+		return;
+
+	nentry = ui_menu_entry_next(menu->selected);
+	if (nentry == NULL)
+		nentry = ui_menu_entry_first(menu);
+
+	/* Need to find a selectable entry */
+	while (!ui_menu_entry_selectable(nentry)) {
+		nentry = ui_menu_entry_next(nentry);
+		if (nentry == NULL)
+			nentry = ui_menu_entry_first(menu);
+
+		/* Went completely around and found nothing? */
+		if (nentry == menu->selected)
+			return;
+	}
+
+	menu->selected = nentry;
+
+	mpos.x = 0;
+	mpos.y = 0;
+	(void) ui_menu_paint(menu, &mpos);
+}
+
+/** Handle key press without modifiers in menu popup window.
+ *
+ * @param menu Menu
+ * @param event Keyboard event
+ */
+static void ui_menu_key_press_unmod(ui_menu_t *menu, kbd_event_t *event)
+{
+	ui_menu_entry_t *mentry;
+	char32_t c;
+
+	switch (event->key) {
+	case KC_ESCAPE:
+		ui_menu_close_req(menu);
+		break;
+	case KC_LEFT:
+		ui_menu_left(menu, event->kbd_id);
+		break;
+	case KC_RIGHT:
+		ui_menu_right(menu, event->kbd_id);
+		break;
+	case KC_UP:
+		ui_menu_up(menu);
+		break;
+	case KC_DOWN:
+		ui_menu_down(menu);
+		break;
+	case KC_ENTER:
+		if (menu->selected != NULL &&
+		    !ui_menu_entry_is_disabled(menu->selected))
+			ui_menu_entry_activate(menu->selected);
+		break;
+	default:
+		if (event->c != '\0') {
+			mentry = ui_menu_entry_first(menu);
+			while (mentry != NULL) {
+				c = ui_menu_entry_get_accel(mentry);
+				if (c == (char32_t)tolower(event->c) &&
+				    !ui_menu_entry_is_disabled(mentry)) {
+					ui_menu_entry_activate(mentry);
+					break;
+				}
+				mentry = ui_menu_entry_next(mentry);
+			}
+		}
+		break;
+	}
 }
 
@@ -384,6 +564,20 @@
 	ui_menu_t *menu = (ui_menu_t *)arg;
 
-	/* Close the menu */
-	ui_menu_bar_select(menu->mbar, NULL, NULL);
+	/* Forward close request to caller */
+	ui_menu_close_req(menu);
+}
+
+/** Handle keyboard event in menu popup window.
+ *
+ * @param popup Menu popup window
+ * @param arg Argument (ui_menu_t *)
+ * @param event Keyboard event
+ */
+static void ui_menu_popup_kbd(ui_popup_t *popup, void *arg, kbd_event_t *event)
+{
+	ui_menu_t *menu = (ui_menu_t *)arg;
+
+	menu->idev_id = ui_popup_get_idev_id(menu->popup);
+	ui_menu_kbd_event(menu, event);
 }
 
@@ -399,4 +593,6 @@
 	gfx_coord2_t spos;
 
+	menu->idev_id = ui_popup_get_idev_id(menu->popup);
+
 	spos.x = 0;
 	spos.y = 0;
@@ -404,4 +600,58 @@
 }
 
+/** Send menu left event.
+ *
+ * @param menu Menu
+ * @param idev_id Input device ID
+ */
+void ui_menu_left(ui_menu_t *menu, sysarg_t idev_id)
+{
+	if (menu->cb != NULL && menu->cb->left != NULL)
+		menu->cb->left(menu, menu->arg, idev_id);
+}
+
+/** Send menu right event.
+ *
+ * @param menu Menu
+ * @param idev_id Input device ID
+ */
+void ui_menu_right(ui_menu_t *menu, sysarg_t idev_id)
+{
+	if (menu->cb != NULL && menu->cb->right != NULL)
+		menu->cb->right(menu, menu->arg, idev_id);
+}
+
+/** Send menu close request event.
+ *
+ * @param menu Menu
+ */
+void ui_menu_close_req(ui_menu_t *menu)
+{
+	if (menu->cb != NULL && menu->cb->close_req != NULL)
+		menu->cb->close_req(menu, menu->arg);
+}
+
+/** Send menu accelerator key press event.
+ *
+ * @param menu Menu
+ * @param c Character
+ * @param kbd_id Keyboard ID
+ */
+void ui_menu_press_accel(ui_menu_t *menu, char32_t c, sysarg_t kbd_id)
+{
+	if (menu->cb != NULL && menu->cb->press_accel != NULL)
+		menu->cb->press_accel(menu, menu->arg, c, kbd_id);
+}
+
+/** Get ID of last device that input event.
+ *
+ * @param menu Menu
+ * @return Input device ID
+ */
+sysarg_t ui_menu_get_idev_id(ui_menu_t *menu)
+{
+	return menu->idev_id;
+}
+
 /** @}
  */
