Index: uspace/lib/ui/meson.build
===================================================================
--- uspace/lib/ui/meson.build	(revision 5bfeeaac03e40cd83f281d33742a6709621cbed8)
+++ uspace/lib/ui/meson.build	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -57,4 +57,7 @@
 	'test/label.c',
 	'test/main.c',
+	'test/menu.c',
+	'test/menubar.c',
+	'test/menuentry.c',
 	'test/paint.c',
 	'test/pbutton.c',
Index: uspace/lib/ui/private/menu.h
===================================================================
--- uspace/lib/ui/private/menu.h	(revision 5bfeeaac03e40cd83f281d33742a6709621cbed8)
+++ uspace/lib/ui/private/menu.h	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -39,5 +39,7 @@
 
 #include <adt/list.h>
+#include <gfx/coord.h>
 #include <stdbool.h>
+#include <types/ui/menu.h>
 
 /** Actual structure of menu.
Index: uspace/lib/ui/private/menubar.h
===================================================================
--- uspace/lib/ui/private/menubar.h	(revision 5bfeeaac03e40cd83f281d33742a6709621cbed8)
+++ uspace/lib/ui/private/menubar.h	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -40,4 +40,5 @@
 #include <adt/list.h>
 #include <gfx/coord.h>
+#include <types/ui/menu.h>
 #include <types/ui/menubar.h>
 
Index: uspace/lib/ui/private/menuentry.h
===================================================================
--- uspace/lib/ui/private/menuentry.h	(revision 5bfeeaac03e40cd83f281d33742a6709621cbed8)
+++ uspace/lib/ui/private/menuentry.h	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -76,4 +76,5 @@
 extern void ui_menu_entry_get_geom(ui_menu_entry_t *, gfx_coord2_t *,
     ui_menu_entry_geom_t *);
+extern void ui_menu_entry_cb(ui_menu_entry_t *);
 
 #endif
Index: uspace/lib/ui/src/menuentry.c
===================================================================
--- uspace/lib/ui/src/menuentry.c	(revision 5bfeeaac03e40cd83f281d33742a6709621cbed8)
+++ uspace/lib/ui/src/menuentry.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -286,7 +286,16 @@
 
 		/* Call back */
-		if (mentry->cb != NULL)
-			mentry->cb(mentry, mentry->arg);
-	}
+		ui_menu_entry_cb(mentry);
+	}
+}
+
+/** Call menu entry callback.
+ *
+ * @param mentry Menu entry
+ */
+void ui_menu_entry_cb(ui_menu_entry_t *mentry)
+{
+	if (mentry->cb != NULL)
+		mentry->cb(mentry, mentry->arg);
 }
 
Index: uspace/lib/ui/test/image.c
===================================================================
--- uspace/lib/ui/test/image.c	(revision 5bfeeaac03e40cd83f281d33742a6709621cbed8)
+++ uspace/lib/ui/test/image.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -43,8 +43,4 @@
 PCUT_TEST_SUITE(image);
 
-typedef struct {
-	bool clicked;
-} test_cb_resp_t;
-
 /** Create and destroy image */
 PCUT_TEST(create_destroy)
Index: uspace/lib/ui/test/main.c
===================================================================
--- uspace/lib/ui/test/main.c	(revision 5bfeeaac03e40cd83f281d33742a6709621cbed8)
+++ uspace/lib/ui/test/main.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -37,4 +37,7 @@
 PCUT_IMPORT(image);
 PCUT_IMPORT(label);
+PCUT_IMPORT(menu);
+PCUT_IMPORT(menubar);
+PCUT_IMPORT(menuentry);
 PCUT_IMPORT(paint);
 PCUT_IMPORT(pbutton);
Index: uspace/lib/ui/test/menu.c
===================================================================
--- uspace/lib/ui/test/menu.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
+++ uspace/lib/ui/test/menu.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -0,0 +1,441 @@
+/*
+ * Copyright (c) 2021 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <gfx/context.h>
+#include <gfx/coord.h>
+#include <mem.h>
+#include <pcut/pcut.h>
+#include <stdbool.h>
+#include <str.h>
+#include <ui/control.h>
+#include <ui/menu.h>
+#include <ui/menubar.h>
+#include <ui/resource.h>
+#include <ui/ui.h>
+#include "../private/dummygc.h"
+#include "../private/menu.h"
+#include "../private/menubar.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(menu);
+
+typedef struct {
+	bool expose;
+} test_resp_t;
+
+static void test_expose(void *);
+
+/** Create and destroy menu */
+PCUT_TEST(create_destroy)
+{
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	errno_t rc;
+
+	rc = ui_menu_bar_create(NULL, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	/*
+	 * Normally we don't need to destroy a menu explicitly, it will
+	 * be destroyed along with menu bar, but here we'll test destroying
+	 * it explicitly.
+	 */
+	ui_menu_destroy(menu);
+	ui_menu_bar_destroy(mbar);
+}
+
+/** ui_menu_destroy() can take NULL argument (no-op) */
+PCUT_TEST(destroy_null)
+{
+	ui_menu_destroy(NULL);
+}
+
+/** ui_menu_first() / ui_menu_next() iterate over menus */
+PCUT_TEST(first_next)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu1 = NULL;
+	ui_menu_t *menu2 = NULL;
+	ui_menu_t *m;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test 1", &menu1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu1);
+
+	rc = ui_menu_create(mbar, "Test 1", &menu2);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu2);
+
+	m = ui_menu_first(mbar);
+	PCUT_ASSERT_EQUALS(menu1, m);
+
+	m = ui_menu_next(m);
+	PCUT_ASSERT_EQUALS(menu2, m);
+
+	m = ui_menu_next(m);
+	PCUT_ASSERT_NULL(m);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** ui_menu_caption() returns the menu's caption */
+PCUT_TEST(caption)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	const char *caption;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	caption = ui_menu_caption(menu);
+	PCUT_ASSERT_NOT_NULL(caption);
+
+	PCUT_ASSERT_INT_EQUALS(0, str_cmp(caption, "Test"));
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** ui_menu_get_rect() returns outer menu rectangle */
+PCUT_TEST(get_rect)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	gfx_coord2_t pos;
+	gfx_rect_t rect;
+	const char *caption;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	caption = ui_menu_caption(menu);
+	PCUT_ASSERT_NOT_NULL(caption);
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_get_rect(menu, &pos, &rect);
+
+	PCUT_ASSERT_INT_EQUALS(0, rect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(0, rect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(8, rect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(8, rect.p1.y);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Paint menu */
+PCUT_TEST(paint)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	gfx_coord2_t pos;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	pos.x = 0;
+	pos.y = 0;
+	rc = ui_menu_paint(menu, &pos);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** ui_menu_unpaint() calls expose callback */
+PCUT_TEST(unpaint)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	test_resp_t resp;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	ui_resource_set_expose_cb(resource, test_expose, &resp);
+
+	resp.expose = false;
+	rc = ui_menu_unpaint(menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_TRUE(resp.expose);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** ui_menu_pos_event() inside menu is claimed */
+PCUT_TEST(pos_event_inside)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_evclaim_t claimed;
+	gfx_coord2_t pos;
+	pos_event_t event;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	pos.x = 0;
+	pos.y = 0;
+	event.type = POS_PRESS;
+	event.hpos = 0;
+	event.vpos = 0;
+	claimed = ui_menu_pos_event(menu, &pos, &event);
+	PCUT_ASSERT_EQUALS(ui_claimed, claimed);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** ui_menu_pos_event() outside menu closes it */
+PCUT_TEST(pos_event_outside)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_evclaim_t claimed;
+	gfx_coord2_t pos;
+	pos_event_t event;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_bar_select(mbar, &pos, menu);
+	PCUT_ASSERT_EQUALS(menu, mbar->selected);
+
+	pos.x = 10;
+	pos.y = 0;
+	event.type = POS_PRESS;
+	event.hpos = 0;
+	event.vpos = 0;
+	claimed = ui_menu_pos_event(menu, &pos, &event);
+	PCUT_ASSERT_EQUALS(ui_unclaimed, claimed);
+
+	/* Press event outside menu should close it */
+	PCUT_ASSERT_NULL(mbar->selected);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Computing menu geometry */
+PCUT_TEST(get_geom)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_geom_t geom;
+	gfx_coord2_t pos;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_get_geom(menu, &pos, &geom);
+
+	PCUT_ASSERT_INT_EQUALS(0, geom.outer_rect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(0, geom.outer_rect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(8, geom.outer_rect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(8, geom.outer_rect.p1.y);
+	PCUT_ASSERT_INT_EQUALS(4, geom.entries_rect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(4, geom.entries_rect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(4, geom.entries_rect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(4, geom.entries_rect.p1.y);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+static void test_expose(void *arg)
+{
+	test_resp_t *resp = (test_resp_t *) arg;
+
+	resp->expose = true;
+}
+
+PCUT_EXPORT(menu);
Index: uspace/lib/ui/test/menubar.c
===================================================================
--- uspace/lib/ui/test/menubar.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
+++ uspace/lib/ui/test/menubar.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -0,0 +1,391 @@
+/*
+ * Copyright (c) 2021 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <gfx/context.h>
+#include <gfx/coord.h>
+#include <mem.h>
+#include <pcut/pcut.h>
+#include <stdbool.h>
+#include <ui/control.h>
+#include <ui/menu.h>
+#include <ui/menubar.h>
+#include <ui/resource.h>
+#include <ui/ui.h>
+#include "../private/dummygc.h"
+#include "../private/menubar.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(menubar);
+
+/** Create and destroy menu bar */
+PCUT_TEST(create_destroy)
+{
+	ui_menu_bar_t *mbar = NULL;
+	errno_t rc;
+
+	rc = ui_menu_bar_create(NULL, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	ui_menu_bar_destroy(mbar);
+}
+
+/** ui_menu_bar_destroy() can take NULL argument (no-op) */
+PCUT_TEST(destroy_null)
+{
+	ui_menu_bar_destroy(NULL);
+}
+
+/** ui_menu_bar_ctl() returns control that has a working virtual destructor */
+PCUT_TEST(ctl)
+{
+	ui_menu_bar_t *mbar = NULL;
+	ui_control_t *control;
+	errno_t rc;
+
+	rc = ui_menu_bar_create(NULL, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	control = ui_menu_bar_ctl(mbar);
+	PCUT_ASSERT_NOT_NULL(control);
+
+	ui_control_destroy(control);
+}
+
+/** Set menu bar rectangle sets internal field */
+PCUT_TEST(set_rect)
+{
+	errno_t rc;
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	gfx_rect_t rect;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rect.p0.x = 1;
+	rect.p0.y = 2;
+	rect.p1.x = 3;
+	rect.p1.y = 4;
+
+	ui_menu_bar_set_rect(mbar, &rect);
+	PCUT_ASSERT_INT_EQUALS(rect.p0.x, mbar->rect.p0.x);
+	PCUT_ASSERT_INT_EQUALS(rect.p0.y, mbar->rect.p0.y);
+	PCUT_ASSERT_INT_EQUALS(rect.p1.x, mbar->rect.p1.x);
+	PCUT_ASSERT_INT_EQUALS(rect.p1.y, mbar->rect.p1.y);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Paint menu bar */
+PCUT_TEST(paint)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_bar_paint(mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Press event on menu bar entry selects menu */
+PCUT_TEST(pos_event_select)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_evclaim_t claimed;
+	pos_event_t event;
+	gfx_rect_t rect;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rect.p0.x = 0;
+	rect.p0.y = 0;
+	rect.p1.x = 50;
+	rect.p1.y = 25;
+	ui_menu_bar_set_rect(mbar, &rect);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	event.type = POS_PRESS;
+	event.hpos = 4;
+	event.vpos = 4;
+	claimed = ui_menu_bar_pos_event(mbar, &event);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_EQUALS(ui_claimed, claimed);
+
+	/* Clicking the menu bar entry should select menu */
+	PCUT_ASSERT_EQUALS(menu, mbar->selected);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Position event is forwarded to menu */
+PCUT_TEST(pos_event_menu)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_evclaim_t claimed;
+	pos_event_t event;
+	gfx_coord2_t pos;
+	gfx_rect_t rect;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rect.p0.x = 0;
+	rect.p0.y = 0;
+	rect.p1.x = 50;
+	rect.p1.y = 25;
+	ui_menu_bar_set_rect(mbar, &rect);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_bar_select(mbar, &pos, menu);
+	PCUT_ASSERT_EQUALS(menu, mbar->selected);
+
+	event.type = POS_PRESS;
+	event.hpos = 4;
+	event.vpos = 30;
+	claimed = ui_menu_bar_pos_event(mbar, &event);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_EQUALS(ui_claimed, claimed);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/* Unfocusing window closes open menu */
+PCUT_TEST(unfocus)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	gfx_coord2_t pos;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	/*
+	 * Position does not matter here. Probably should get rid of this
+	 * argument, storing the position in the menu itself.
+	 */
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_bar_select(mbar, &pos, menu);
+	PCUT_ASSERT_EQUALS(menu, mbar->selected);
+
+	/* This should unselect the menu */
+	ui_menu_bar_unfocus(mbar);
+	PCUT_ASSERT_NULL(mbar->selected);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Calling ui_menu_bar_select() with the same menu twice deselects it */
+PCUT_TEST(select_same)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	gfx_coord2_t pos;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_bar_select(mbar, &pos, menu);
+	PCUT_ASSERT_EQUALS(menu, mbar->selected);
+
+	/* Selecting again should unselect the menu */
+	ui_menu_bar_select(mbar, &pos, menu);
+	PCUT_ASSERT_NULL(mbar->selected);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Calling ui_menu_bar_select() with another menu selects it */
+PCUT_TEST(select_different)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu1 = NULL;
+	ui_menu_t *menu2 = NULL;
+	gfx_coord2_t pos;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test 1", &menu1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu1);
+
+	rc = ui_menu_create(mbar, "Test 2", &menu2);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu2);
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_bar_select(mbar, &pos, menu1);
+	PCUT_ASSERT_EQUALS(menu1, mbar->selected);
+
+	/* Selecting different menu should select it */
+	ui_menu_bar_select(mbar, &pos, menu2);
+	PCUT_ASSERT_EQUALS(menu2, mbar->selected);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+PCUT_EXPORT(menubar);
Index: uspace/lib/ui/test/menuentry.c
===================================================================
--- uspace/lib/ui/test/menuentry.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
+++ uspace/lib/ui/test/menuentry.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -0,0 +1,640 @@
+/*
+ * Copyright (c) 2021 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <gfx/context.h>
+#include <gfx/coord.h>
+#include <mem.h>
+#include <pcut/pcut.h>
+#include <stdbool.h>
+#include <ui/control.h>
+#include <ui/menu.h>
+#include <ui/menubar.h>
+#include <ui/menuentry.h>
+#include <ui/resource.h>
+#include <ui/ui.h>
+#include "../private/dummygc.h"
+#include "../private/menuentry.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(menuentry);
+
+typedef struct {
+	bool activated;
+} test_resp_t;
+
+static void test_entry_cb(ui_menu_entry_t *, void *);
+
+/** Create and destroy menu bar */
+PCUT_TEST(create_destroy)
+{
+	ui_menu_bar_t *mbar = NULL;
+	errno_t rc;
+
+	rc = ui_menu_bar_create(NULL, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	ui_menu_bar_destroy(mbar);
+}
+
+/** ui_menu_bar_destroy() can take NULL argument (no-op) */
+PCUT_TEST(destroy_null)
+{
+	ui_menu_bar_destroy(NULL);
+}
+
+/** ui_menu_entry_set_cb() .. */
+PCUT_TEST(set_cb)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	test_resp_t resp;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "Foo", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	ui_menu_entry_set_cb(mentry, test_entry_cb, &resp);
+
+	resp.activated = false;
+	ui_menu_entry_cb(mentry);
+	PCUT_ASSERT_TRUE(resp.activated);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** ui_menu_entry_first() / ui_menu_entry_next() iterate over entries */
+PCUT_TEST(first_next)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *entry1 = NULL;
+	ui_menu_entry_t *entry2 = NULL;
+	ui_menu_entry_t *e;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "Foo", &entry1);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(entry1);
+
+	rc = ui_menu_entry_create(menu, "Bar", &entry2);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(entry2);
+
+	e = ui_menu_entry_first(menu);
+	PCUT_ASSERT_EQUALS(entry1, e);
+
+	e = ui_menu_entry_next(e);
+	PCUT_ASSERT_EQUALS(entry2, e);
+
+	e = ui_menu_entry_next(e);
+	PCUT_ASSERT_NULL(e);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** ui_menu_entry_width() / ui_menu_entry_height() */
+PCUT_TEST(width_height)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	gfx_coord_t width;
+	gfx_coord_t height;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "X", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	width = ui_menu_entry_width(mentry);
+	PCUT_ASSERT_INT_EQUALS(11 + 8, width);
+
+	height = ui_menu_entry_height(mentry);
+	PCUT_ASSERT_INT_EQUALS(13 + 8, height);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Paint menu entry */
+PCUT_TEST(paint)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	gfx_coord2_t pos;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "Foo", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	pos.x = 0;
+	pos.y = 0;
+	rc = ui_menu_entry_paint(mentry, &pos);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Press and release activates menu entry */
+PCUT_TEST(press_release)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	gfx_coord2_t pos;
+	test_resp_t resp;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "X", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	ui_menu_entry_set_cb(mentry, test_entry_cb, &resp);
+	resp.activated = false;
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_entry_press(mentry, &pos);
+	PCUT_ASSERT_TRUE(mentry->inside);
+	PCUT_ASSERT_TRUE(mentry->held);
+	PCUT_ASSERT_FALSE(resp.activated);
+
+	ui_menu_entry_release(mentry);
+	PCUT_ASSERT_FALSE(mentry->held);
+	PCUT_ASSERT_TRUE(resp.activated);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Press, leave and release does not activate entry */
+PCUT_TEST(press_leave_release)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	gfx_coord2_t pos;
+	test_resp_t resp;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "X", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	ui_menu_entry_set_cb(mentry, test_entry_cb, &resp);
+	resp.activated = false;
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_entry_press(mentry, &pos);
+	PCUT_ASSERT_TRUE(mentry->inside);
+	PCUT_ASSERT_TRUE(mentry->held);
+	PCUT_ASSERT_FALSE(resp.activated);
+
+	ui_menu_entry_leave(mentry, &pos);
+	PCUT_ASSERT_FALSE(mentry->inside);
+	PCUT_ASSERT_TRUE(mentry->held);
+	PCUT_ASSERT_FALSE(resp.activated);
+
+	ui_menu_entry_release(mentry);
+	PCUT_ASSERT_FALSE(mentry->held);
+	PCUT_ASSERT_FALSE(resp.activated);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Press, leave, enter and release activates menu entry */
+PCUT_TEST(press_leave_enter_release)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	gfx_coord2_t pos;
+	test_resp_t resp;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "X", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	ui_menu_entry_set_cb(mentry, test_entry_cb, &resp);
+	resp.activated = false;
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_entry_press(mentry, &pos);
+	PCUT_ASSERT_TRUE(mentry->inside);
+	PCUT_ASSERT_TRUE(mentry->held);
+	PCUT_ASSERT_FALSE(resp.activated);
+
+	ui_menu_entry_leave(mentry, &pos);
+	PCUT_ASSERT_FALSE(mentry->inside);
+	PCUT_ASSERT_TRUE(mentry->held);
+	PCUT_ASSERT_FALSE(resp.activated);
+
+	ui_menu_entry_enter(mentry, &pos);
+	PCUT_ASSERT_TRUE(mentry->inside);
+	PCUT_ASSERT_TRUE(mentry->held);
+	PCUT_ASSERT_FALSE(resp.activated);
+
+	ui_menu_entry_release(mentry);
+	PCUT_ASSERT_FALSE(mentry->held);
+	PCUT_ASSERT_TRUE(resp.activated);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Press event inside menu entry */
+PCUT_TEST(pos_press_inside)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	gfx_coord2_t pos;
+	pos_event_t event;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "X", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	pos.x = 0;
+	pos.y = 0;
+
+	event.type = POS_PRESS;
+	event.hpos = 4;
+	event.vpos = 4;
+
+	ui_menu_entry_pos_event(mentry, &pos, &event);
+	PCUT_ASSERT_TRUE(mentry->inside);
+	PCUT_ASSERT_TRUE(mentry->held);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Press event outside menu entry */
+PCUT_TEST(pos_press_outside)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	gfx_coord2_t pos;
+	pos_event_t event;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "X", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	pos.x = 0;
+	pos.y = 0;
+
+	event.type = POS_PRESS;
+	event.hpos = 20;
+	event.vpos = 20;
+
+	ui_menu_entry_pos_event(mentry, &pos, &event);
+	PCUT_ASSERT_FALSE(mentry->inside);
+	PCUT_ASSERT_FALSE(mentry->held);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Position event moving out of menu entry */
+PCUT_TEST(pos_move_out)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	gfx_coord2_t pos;
+	pos_event_t event;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "X", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	pos.x = 0;
+	pos.y = 0;
+	ui_menu_entry_press(mentry, &pos);
+	PCUT_ASSERT_TRUE(mentry->inside);
+	PCUT_ASSERT_TRUE(mentry->held);
+
+	event.type = POS_UPDATE;
+	event.hpos = 20;
+	event.vpos = 20;
+
+	ui_menu_entry_pos_event(mentry, &pos, &event);
+	PCUT_ASSERT_FALSE(mentry->inside);
+	PCUT_ASSERT_TRUE(mentry->held);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+/** Position event moving inside menu entry */
+PCUT_TEST(pos_move_in)
+{
+	dummy_gc_t *dgc;
+	gfx_context_t *gc;
+	ui_resource_t *resource = NULL;
+	ui_menu_bar_t *mbar = NULL;
+	ui_menu_t *menu = NULL;
+	ui_menu_entry_t *mentry = NULL;
+	gfx_coord2_t pos;
+	pos_event_t event;
+	errno_t rc;
+
+	rc = dummygc_create(&dgc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	gc = dummygc_get_ctx(dgc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	rc = ui_menu_bar_create(resource, &mbar);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mbar);
+
+	rc = ui_menu_create(mbar, "Test", &menu);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(menu);
+
+	rc = ui_menu_entry_create(menu, "X", &mentry);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(mentry);
+
+	event.type = POS_UPDATE;
+	event.hpos = 4;
+	event.vpos = 4;
+
+	pos.x = 0;
+	pos.y = 0;
+
+	ui_menu_entry_pos_event(mentry, &pos, &event);
+	PCUT_ASSERT_TRUE(mentry->inside);
+	PCUT_ASSERT_FALSE(mentry->held);
+
+	ui_menu_bar_destroy(mbar);
+	ui_resource_destroy(resource);
+	dummygc_destroy(dgc);
+}
+
+static void test_entry_cb(ui_menu_entry_t *mentry, void *arg)
+{
+	test_resp_t *resp = (test_resp_t *) arg;
+
+	resp->activated = true;
+}
+
+PCUT_EXPORT(menuentry);
Index: uspace/lib/ui/test/resource.c
===================================================================
--- uspace/lib/ui/test/resource.c	(revision 5bfeeaac03e40cd83f281d33742a6709621cbed8)
+++ uspace/lib/ui/test/resource.c	(revision 1aa0e380b50a49fcd7536c60dde5800c2965e6ce)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2020 Jiri Svoboda
+ * Copyright (c) 2021 Jiri Svoboda
  * All rights reserved.
  *
@@ -44,4 +44,6 @@
 static errno_t testgc_bitmap_get_alloc(void *, gfx_bitmap_alloc_t *);
 
+static void test_expose(void *);
+
 static gfx_context_ops_t ops = {
 	.bitmap_create = testgc_bitmap_create,
@@ -68,4 +70,8 @@
 } testgc_bitmap_t;
 
+typedef struct {
+	bool expose;
+} test_resp_t;
+
 /** Create and destroy UI resource */
 PCUT_TEST(create_destroy)
@@ -97,4 +103,33 @@
 {
 	ui_resource_destroy(NULL);
+}
+
+/** ui_resource_set_expose_cb() / ui_resource_expose() */
+PCUT_TEST(set_expose_cb_expose)
+{
+	errno_t rc;
+	gfx_context_t *gc = NULL;
+	test_gc_t tgc;
+	ui_resource_t *resource = NULL;
+	test_resp_t resp;
+
+	memset(&tgc, 0, sizeof(tgc));
+	rc = gfx_context_new(&ops, &tgc, &gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ui_resource_create(gc, false, &resource);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+	PCUT_ASSERT_NOT_NULL(resource);
+
+	ui_resource_set_expose_cb(resource, test_expose, &resp);
+
+	resp.expose = false;
+	ui_resource_expose(resource);
+	PCUT_ASSERT_TRUE(resp.expose);
+
+	ui_resource_destroy(resource);
+
+	rc = gfx_context_delete(gc);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
 }
 
@@ -161,3 +196,10 @@
 }
 
+static void test_expose(void *arg)
+{
+	test_resp_t *resp = (test_resp_t *) arg;
+
+	resp->expose = true;
+}
+
 PCUT_EXPORT(resource);
