diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd
index b5fc6ef4..99b86983 100644
--- a/docs/labwc-actions.5.scd
+++ b/docs/labwc-actions.5.scd
@@ -54,6 +54,10 @@ Actions are used in menus and keyboard/mouse bindings.
 **
 	Move to position (x, y)
 
+**
+	Move to be centered on cursor.
+	Tries to prevent any part of the window from going off-screen.
+
 **
 	Move window relative to its current position. Positive value of x moves
 	it right, negative left. Positive value of y moves it down, negative up.
diff --git a/include/view.h b/include/view.h
index 9379d1e6..0fb10a22 100644
--- a/include/view.h
+++ b/include/view.h
@@ -306,6 +306,7 @@ void view_resize_relative(struct view *view,
 	int left, int right, int top, int bottom);
 void view_move_relative(struct view *view, int x, int y);
 void view_move(struct view *view, int x, int y);
+void view_move_to_cursor(struct view *view);
 void view_moved(struct view *view);
 void view_minimize(struct view *view, bool minimized);
 void view_store_natural_geometry(struct view *view);
diff --git a/src/action.c b/src/action.c
index 43c3ddec..fe0a6438 100644
--- a/src/action.c
+++ b/src/action.c
@@ -84,6 +84,7 @@ enum action_type {
 	ACTION_TYPE_RESIZE,
 	ACTION_TYPE_RESIZE_RELATIVE,
 	ACTION_TYPE_MOVETO,
+	ACTION_TYPE_MOVETO_CURSOR,
 	ACTION_TYPE_MOVE_RELATIVE,
 	ACTION_TYPE_SEND_TO_DESKTOP,
 	ACTION_TYPE_GO_TO_DESKTOP,
@@ -122,6 +123,7 @@ const char *action_names[] = {
 	"Resize",
 	"ResizeRelative",
 	"MoveTo",
+	"MoveToCursor",
 	"MoveRelative",
 	"SendToDesktop",
 	"GoToDesktop",
@@ -746,6 +748,11 @@ actions_run(struct view *activator, struct server *server,
 				view_move_relative(view, x, y);
 			}
 			break;
+		case ACTION_TYPE_MOVETO_CURSOR:
+			if (view) {
+				view_move_to_cursor(view);
+			}
+			break;
 		case ACTION_TYPE_SEND_TO_DESKTOP:
 			if (!view) {
 				break;
diff --git a/src/view.c b/src/view.c
index 94f2d82a..7a390d11 100644
--- a/src/view.c
+++ b/src/view.c
@@ -351,6 +351,50 @@ view_move_relative(struct view *view, int x, int y)
 	view_move(view, view->pending.x + x, view->pending.y + y);
 }
 
+void
+view_move_to_cursor(struct view *view)
+{
+	assert(view);
+
+	struct output *pending_output = output_nearest_to_cursor(view->server);
+	if (!output_is_usable(pending_output)) {
+		return;
+	}
+	if (view->fullscreen) {
+		view_set_fullscreen(view, false);
+	}
+	if (view->maximized) {
+		view_maximize(view, false, /*store_natural_geometry*/ false);
+	}
+	if (view_is_tiled(view)) {
+		view_set_untiled(view);
+		view_restore_to(view, view->natural_geometry);
+	}
+
+	struct border margin = ssd_thickness(view);
+	struct wlr_box geo = view->pending;
+	geo.width += margin.left + margin.right;
+	geo.height += margin.top + margin.bottom;
+
+	int x = view->server->seat.cursor->x - (geo.width / 2);
+	int y = view->server->seat.cursor->y - (geo.height / 2);
+
+	struct wlr_box usable = output_usable_area_in_layout_coords(pending_output);
+	if (x + geo.width > usable.x + usable.width) {
+		x = usable.x + usable.width - geo.width;
+	}
+	x = MAX(x, usable.x);
+
+	if (y + geo.height > usable.y + usable.height) {
+		y = usable.y + usable.height - geo.height;
+	}
+	y = MAX(y, usable.y);
+
+	x += margin.left;
+	y += margin.top;
+	view_move(view, x, y);
+}
+
 struct view_size_hints
 view_get_size_hints(struct view *view)
 {