diff --git a/.builds/alpine.yml b/.builds/alpine.yml
index 34f780e9e..59df7737c 100644
--- a/.builds/alpine.yml
+++ b/.builds/alpine.yml
@@ -4,6 +4,7 @@ packages:
   - eudev-dev
   - gdk-pixbuf-dev
   - json-c-dev
+  - libdisplay-info-dev
   - libevdev-dev
   - libinput-dev
   - libseat-dev
diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml
index 19bd2e594..9972c01ad 100644
--- a/.builds/archlinux.yml
+++ b/.builds/archlinux.yml
@@ -3,6 +3,7 @@ packages:
   - cairo
   - gdk-pixbuf2
   - json-c
+  - libdisplay-info
   - libegl
   - libinput
   - libxcb
diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml
index 861aa2553..29c6312af 100644
--- a/.builds/freebsd.yml
+++ b/.builds/freebsd.yml
@@ -20,6 +20,7 @@ packages:
 - devel/libudev-devd
 - graphics/libdrm
 - graphics/mesa-libs
+- sysutils/libdisplay-info
 - sysutils/seatd
 - x11/libinput
 - x11/libX11
diff --git a/include/sway/input/cursor.h b/include/sway/input/cursor.h
index 828ac3704..4a3774d97 100644
--- a/include/sway/input/cursor.h
+++ b/include/sway/input/cursor.h
@@ -108,6 +108,10 @@ void cursor_unhide(struct sway_cursor *cursor);
 int cursor_get_timeout(struct sway_cursor *cursor);
 void cursor_notify_key_press(struct sway_cursor *cursor);
 
+void pointer_motion(struct sway_cursor *cursor, uint32_t time_msec,
+		struct wlr_input_device *device, double dx, double dy,
+		double dx_unaccel, double dy_unaccel);
+
 void dispatch_cursor_button(struct sway_cursor *cursor,
 	struct wlr_input_device *device, uint32_t time_msec, uint32_t button,
 	enum wlr_button_state state);
diff --git a/include/sway/input/seat.h b/include/sway/input/seat.h
index 4abe91f71..7b2d3d075 100644
--- a/include/sway/input/seat.h
+++ b/include/sway/input/seat.h
@@ -4,6 +4,7 @@
 #include <wlr/types/wlr_keyboard_shortcuts_inhibit_v1.h>
 #include <wlr/types/wlr_layer_shell_v1.h>
 #include <wlr/types/wlr_seat.h>
+#include <wlr/types/wlr_touch.h>
 #include <wlr/util/edges.h>
 #include "sway/config.h"
 #include "sway/input/input-manager.h"
@@ -36,6 +37,12 @@ struct sway_seatop_impl {
 	void (*swipe_end)(struct sway_seat *seat,
 			struct wlr_pointer_swipe_end_event *event);
 	void (*rebase)(struct sway_seat *seat, uint32_t time_msec);
+	void (*touch_motion)(struct sway_seat *seat,
+			struct wlr_touch_motion_event *event, double lx, double ly);
+	void (*touch_up)(struct sway_seat *seat,
+			struct wlr_touch_up_event *event);
+	void (*touch_down)(struct sway_seat *seat,
+			struct wlr_touch_down_event *event, double lx, double ly);
 	void (*tablet_tool_motion)(struct sway_seat *seat,
 			struct sway_tablet_tool *tool, uint32_t time_msec);
 	void (*tablet_tool_tip)(struct sway_seat *seat, struct sway_tablet_tool *tool,
@@ -43,7 +50,7 @@ struct sway_seatop_impl {
 	void (*end)(struct sway_seat *seat);
 	void (*unref)(struct sway_seat *seat, struct sway_container *con);
 	void (*render)(struct sway_seat *seat, struct sway_output *output,
-			pixman_region32_t *damage);
+			const pixman_region32_t *damage);
 	bool allow_set_cursor;
 };
 
@@ -256,10 +263,13 @@ enum wlr_edges find_resize_edge(struct sway_container *cont,
 void seatop_begin_default(struct sway_seat *seat);
 
 void seatop_begin_down(struct sway_seat *seat, struct sway_container *con,
-		uint32_t time_msec, double sx, double sy);
+		double sx, double sy);
 
 void seatop_begin_down_on_surface(struct sway_seat *seat,
-		struct wlr_surface *surface, uint32_t time_msec, double sx, double sy);
+		struct wlr_surface *surface, double sx, double sy);
+
+void seatop_begin_touch_down(struct sway_seat *seat, struct wlr_surface *surface,
+		struct wlr_touch_down_event *event, double sx, double sy, double lx, double ly);
 
 void seatop_begin_move_floating(struct sway_seat *seat,
 		struct sway_container *con);
@@ -319,6 +329,15 @@ void seatop_swipe_update(struct sway_seat *seat,
 void seatop_swipe_end(struct sway_seat *seat,
 		struct wlr_pointer_swipe_end_event *event);
 
+void seatop_touch_motion(struct sway_seat *seat,
+		struct wlr_touch_motion_event *event, double lx, double ly);
+
+void seatop_touch_up(struct sway_seat *seat,
+		struct wlr_touch_up_event *event);
+
+void seatop_touch_down(struct sway_seat *seat,
+		struct wlr_touch_down_event *event, double lx, double ly);
+
 void seatop_rebase(struct sway_seat *seat, uint32_t time_msec);
 
 /**
@@ -338,7 +357,7 @@ void seatop_unref(struct sway_seat *seat, struct sway_container *con);
  * (eg. dropzone for move-tiling)
  */
 void seatop_render(struct sway_seat *seat, struct sway_output *output,
-		pixman_region32_t *damage);
+		const pixman_region32_t *damage);
 
 bool seatop_allows_set_cursor(struct sway_seat *seat);
 
diff --git a/include/sway/output.h b/include/sway/output.h
index 28be6a1e9..2aa1b2788 100644
--- a/include/sway/output.h
+++ b/include/sway/output.h
@@ -112,8 +112,7 @@ bool output_has_opaque_overlay_layer_surface(struct sway_output *output);
 
 struct sway_workspace *output_get_active_workspace(struct sway_output *output);
 
-void output_render(struct sway_output *output, struct timespec *when,
-	pixman_region32_t *damage);
+void output_render(struct sway_output *output, pixman_region32_t *damage);
 
 void output_surface_for_each_surface(struct sway_output *output,
 		struct wlr_surface *surface, double ox, double oy,
@@ -167,7 +166,7 @@ enum sway_container_layout output_get_default_layout(
 		struct sway_output *output);
 
 void render_rect(struct sway_output *output,
-		pixman_region32_t *output_damage, const struct wlr_box *_box,
+		const pixman_region32_t *output_damage, const struct wlr_box *_box,
 		float color[static 4]);
 
 void premultiply_alpha(float color[4], float opacity);
diff --git a/include/sway/surface.h b/include/sway/surface.h
index 506818976..a7a8ec3ff 100644
--- a/include/sway/surface.h
+++ b/include/sway/surface.h
@@ -15,6 +15,7 @@ struct sway_surface {
 	struct wl_event_source *frame_done_timer;
 };
 
+void surface_update_outputs(struct wlr_surface *surface);
 void surface_enter_output(struct wlr_surface *surface,
 	struct sway_output *output);
 void surface_leave_output(struct wlr_surface *surface,
diff --git a/sway/commands/swap.c b/sway/commands/swap.c
index 9355944d9..b457f121c 100644
--- a/sway/commands/swap.c
+++ b/sway/commands/swap.c
@@ -5,6 +5,7 @@
 #include "sway/commands.h"
 #include "sway/output.h"
 #include "sway/tree/arrange.h"
+#include "sway/tree/container.h"
 #include "sway/tree/root.h"
 #include "sway/tree/view.h"
 #include "sway/tree/workspace.h"
@@ -13,180 +14,6 @@
 static const char expected_syntax[] =
 	"Expected 'swap container with id|con_id|mark <arg>'";
 
-static void swap_places(struct sway_container *con1,
-		struct sway_container *con2) {
-	struct sway_container *temp = malloc(sizeof(struct sway_container));
-	temp->pending.x = con1->pending.x;
-	temp->pending.y = con1->pending.y;
-	temp->pending.width = con1->pending.width;
-	temp->pending.height = con1->pending.height;
-	temp->width_fraction = con1->width_fraction;
-	temp->height_fraction = con1->height_fraction;
-	temp->pending.parent = con1->pending.parent;
-	temp->pending.workspace = con1->pending.workspace;
-	bool temp_floating = container_is_floating(con1);
-
-	con1->pending.x = con2->pending.x;
-	con1->pending.y = con2->pending.y;
-	con1->pending.width = con2->pending.width;
-	con1->pending.height = con2->pending.height;
-	con1->width_fraction = con2->width_fraction;
-	con1->height_fraction = con2->height_fraction;
-
-	con2->pending.x = temp->pending.x;
-	con2->pending.y = temp->pending.y;
-	con2->pending.width = temp->pending.width;
-	con2->pending.height = temp->pending.height;
-	con2->width_fraction = temp->width_fraction;
-	con2->height_fraction = temp->height_fraction;
-
-	int temp_index = container_sibling_index(con1);
-	if (con2->pending.parent) {
-		container_insert_child(con2->pending.parent, con1,
-				container_sibling_index(con2));
-	} else if (container_is_floating(con2)) {
-		workspace_add_floating(con2->pending.workspace, con1);
-	} else {
-		workspace_insert_tiling(con2->pending.workspace, con1,
-				container_sibling_index(con2));
-	}
-	if (temp->pending.parent) {
-		container_insert_child(temp->pending.parent, con2, temp_index);
-	} else if (temp_floating) {
-		workspace_add_floating(temp->pending.workspace, con2);
-	} else {
-		workspace_insert_tiling(temp->pending.workspace, con2, temp_index);
-	}
-
-	free(temp);
-}
-
-static void swap_focus(struct sway_container *con1,
-		struct sway_container *con2, struct sway_seat *seat,
-		struct sway_container *focus) {
-	if (focus == con1 || focus == con2) {
-		struct sway_workspace *ws1 = con1->pending.workspace;
-		struct sway_workspace *ws2 = con2->pending.workspace;
-		enum sway_container_layout layout1 = container_parent_layout(con1);
-		enum sway_container_layout layout2 = container_parent_layout(con2);
-		if (focus == con1 && (layout2 == L_TABBED || layout2 == L_STACKED)) {
-			if (workspace_is_visible(ws2)) {
-				seat_set_focus(seat, &con2->node);
-			}
-			seat_set_focus_container(seat, ws1 != ws2 ? con2 : con1);
-		} else if (focus == con2 && (layout1 == L_TABBED
-					|| layout1 == L_STACKED)) {
-			if (workspace_is_visible(ws1)) {
-				seat_set_focus(seat, &con1->node);
-			}
-			seat_set_focus_container(seat, ws1 != ws2 ? con1 : con2);
-		} else if (ws1 != ws2) {
-			seat_set_focus_container(seat, focus == con1 ? con2 : con1);
-		} else {
-			seat_set_focus_container(seat, focus);
-		}
-	} else {
-		seat_set_focus_container(seat, focus);
-	}
-
-	if (root->fullscreen_global) {
-		seat_set_focus(seat,
-				seat_get_focus_inactive(seat, &root->fullscreen_global->node));
-	}
-}
-
-void container_swap(struct sway_container *con1, struct sway_container *con2) {
-	if (!sway_assert(con1 && con2, "Cannot swap with nothing")) {
-		return;
-	}
-	if (!sway_assert(!container_has_ancestor(con1, con2)
-				&& !container_has_ancestor(con2, con1),
-				"Cannot swap ancestor and descendant")) {
-		return;
-	}
-
-	sway_log(SWAY_DEBUG, "Swapping containers %zu and %zu",
-			con1->node.id, con2->node.id);
-
-	bool scratch1 = con1->scratchpad;
-	bool hidden1 = container_is_scratchpad_hidden(con1);
-	bool scratch2 = con2->scratchpad;
-	bool hidden2 = container_is_scratchpad_hidden(con2);
-	if (scratch1) {
-		if (hidden1) {
-			root_scratchpad_show(con1);
-		}
-		root_scratchpad_remove_container(con1);
-	}
-	if (scratch2) {
-		if (hidden2) {
-			root_scratchpad_show(con2);
-		}
-		root_scratchpad_remove_container(con2);
-	}
-
-	enum sway_fullscreen_mode fs1 = con1->pending.fullscreen_mode;
-	if (fs1) {
-		container_fullscreen_disable(con1);
-	}
-	enum sway_fullscreen_mode fs2 = con2->pending.fullscreen_mode;
-	if (fs2) {
-		container_fullscreen_disable(con2);
-	}
-
-	struct sway_seat *seat = config->handler_context.seat;
-	struct sway_container *focus = seat_get_focused_container(seat);
-	struct sway_workspace *vis1 =
-		output_get_active_workspace(con1->pending.workspace->output);
-	struct sway_workspace *vis2 =
-		output_get_active_workspace(con2->pending.workspace->output);
-	if (!sway_assert(vis1 && vis2, "con1 or con2 are on an output without a"
-				"workspace. This should not happen")) {
-		return;
-	}
-
-	char *stored_prev_name = NULL;
-	if (seat->prev_workspace_name) {
-		stored_prev_name = strdup(seat->prev_workspace_name);
-	}
-
-	swap_places(con1, con2);
-
-	if (!workspace_is_visible(vis1)) {
-		seat_set_focus(seat, seat_get_focus_inactive(seat, &vis1->node));
-	}
-	if (!workspace_is_visible(vis2)) {
-		seat_set_focus(seat, seat_get_focus_inactive(seat, &vis2->node));
-	}
-
-	swap_focus(con1, con2, seat, focus);
-
-	if (stored_prev_name) {
-		free(seat->prev_workspace_name);
-		seat->prev_workspace_name = stored_prev_name;
-	}
-
-	if (scratch1) {
-		root_scratchpad_add_container(con2, NULL);
-		if (!hidden1) {
-			root_scratchpad_show(con2);
-		}
-	}
-	if (scratch2) {
-		root_scratchpad_add_container(con1, NULL);
-		if (!hidden2) {
-			root_scratchpad_show(con1);
-		}
-	}
-
-	if (fs1) {
-		container_set_fullscreen(con2, fs1);
-	}
-	if (fs2) {
-		container_set_fullscreen(con1, fs2);
-	}
-}
-
 static bool test_con_id(struct sway_container *container, void *data) {
 	size_t *con_id = data;
 	return container->node.id == *con_id;
diff --git a/sway/config/output.c b/sway/config/output.c
index e2e48e4cc..3b524433c 100644
--- a/sway/config/output.c
+++ b/sway/config/output.c
@@ -460,6 +460,16 @@ static void queue_output_config(struct output_config *oc,
 	float scale;
 	if (oc && oc->scale > 0) {
 		scale = oc->scale;
+
+		// The factional-scale-v1 protocol uses increments of 120ths to send
+		// the scale factor to the client. Adjust the scale so that we use the
+		// same value as the clients'.
+		float adjusted_scale = round(scale * 120) / 120;
+		if (scale != adjusted_scale) {
+			sway_log(SWAY_INFO, "Adjusting output scale from %f to %f",
+				scale, adjusted_scale);
+			scale = adjusted_scale;
+		}
 	} else {
 		scale = compute_default_scale(wlr_output, pending);
 		sway_log(SWAY_DEBUG, "Auto-detected output scale: %f", scale);
diff --git a/sway/desktop/layer_shell.c b/sway/desktop/layer_shell.c
index 795eb4cd7..e16bee78d 100644
--- a/sway/desktop/layer_shell.c
+++ b/sway/desktop/layer_shell.c
@@ -383,7 +383,6 @@ static void handle_map(struct wl_listener *listener, void *data) {
 	struct sway_output *output = wlr_output->data;
 	output_damage_surface(output, sway_layer->geo.x, sway_layer->geo.y,
 		sway_layer->layer_surface->surface, true);
-	surface_enter_output(sway_layer->layer_surface->surface, output);
 	cursor_rebase_all();
 }
 
@@ -679,6 +678,8 @@ void handle_layer_shell_surface(struct wl_listener *listener, void *data) {
 	wl_list_insert(&output->layers[layer_surface->pending.layer],
 			&sway_layer->link);
 
+	surface_enter_output(layer_surface->surface, output);
+
 	// Temporarily set the layer's current state to pending
 	// So that we can easily arrange it
 	struct wlr_layer_surface_v1_state old_state = layer_surface->current;
diff --git a/sway/desktop/output.c b/sway/desktop/output.c
index 141edb491..0c8a5fd41 100644
--- a/sway/desktop/output.c
+++ b/sway/desktop/output.c
@@ -496,6 +496,12 @@ static bool scan_out_fullscreen_view(struct sway_output *output,
 	if (n_surfaces != 1) {
 		return false;
 	}
+	size_t n_popups = 0;
+	output_view_for_each_popup_surface(output, view,
+		count_surface_iterator, &n_popups);
+	if (n_popups > 0) {
+		return false;
+	}
 
 	if (surface->buffer == NULL) {
 		return false;
@@ -517,13 +523,34 @@ static bool scan_out_fullscreen_view(struct sway_output *output,
 	return wlr_output_commit(wlr_output);
 }
 
+static void get_frame_damage(struct sway_output *output,
+		pixman_region32_t *frame_damage) {
+	struct wlr_output *wlr_output = output->wlr_output;
+
+	int width, height;
+	wlr_output_transformed_resolution(wlr_output, &width, &height);
+
+	pixman_region32_init(frame_damage);
+
+	enum wl_output_transform transform =
+		wlr_output_transform_invert(wlr_output->transform);
+	wlr_region_transform(frame_damage, &output->damage_ring.current,
+		transform, width, height);
+
+	if (debug.damage != DAMAGE_DEFAULT) {
+		pixman_region32_union_rect(frame_damage, frame_damage,
+			0, 0, wlr_output->width, wlr_output->height);
+	}
+}
+
 static int output_repaint_timer_handler(void *data) {
 	struct sway_output *output = data;
-	if (output->wlr_output == NULL) {
+	struct wlr_output *wlr_output = output->wlr_output;
+	if (wlr_output == NULL) {
 		return 0;
 	}
 
-	output->wlr_output->frame_pending = false;
+	wlr_output->frame_pending = false;
 
 	struct sway_workspace *workspace = output->current.active_workspace;
 	if (workspace == NULL) {
@@ -557,6 +584,11 @@ static int output_repaint_timer_handler(void *data) {
 		}
 	}
 
+	if (!output->wlr_output->needs_frame &&
+			!pixman_region32_not_empty(&output->damage_ring.current)) {
+		return 0;
+	}
+
 	int buffer_age;
 	if (!wlr_output_attach_render(output->wlr_output, &buffer_age)) {
 		return 0;
@@ -565,20 +597,26 @@ static int output_repaint_timer_handler(void *data) {
 	pixman_region32_t damage;
 	pixman_region32_init(&damage);
 	wlr_damage_ring_get_buffer_damage(&output->damage_ring, buffer_age, &damage);
-	if (!output->wlr_output->needs_frame &&
-			!pixman_region32_not_empty(&output->damage_ring.current)) {
-		pixman_region32_fini(&damage);
-		wlr_output_rollback(output->wlr_output);
-		return 0;
-	}
 
 	struct timespec now;
 	clock_gettime(CLOCK_MONOTONIC, &now);
 
-	output_render(output, &now, &damage);
+	output_render(output, &damage);
 
 	pixman_region32_fini(&damage);
 
+	pixman_region32_t frame_damage;
+	get_frame_damage(output, &frame_damage);
+	wlr_output_set_damage(wlr_output, &frame_damage);
+	pixman_region32_fini(&frame_damage);
+
+	if (!wlr_output_commit(wlr_output)) {
+		return 0;
+	}
+
+	wlr_damage_ring_rotate(&output->damage_ring);
+	output->last_frame = now;
+
 	return 0;
 }
 
@@ -859,6 +897,12 @@ static void update_textures(struct sway_container *con, void *data) {
 	container_update_marks_textures(con);
 }
 
+static void update_output_scale_iterator(struct sway_output *output,
+		struct sway_view *view, struct wlr_surface *surface,
+		struct wlr_box *box, void *user_data) {
+	surface_update_outputs(surface);
+}
+
 static void handle_commit(struct wl_listener *listener, void *data) {
 	struct sway_output *output = wl_container_of(listener, output, commit);
 	struct wlr_output_event_commit *event = data;
@@ -873,6 +917,7 @@ static void handle_commit(struct wl_listener *listener, void *data) {
 
 	if (event->committed & WLR_OUTPUT_STATE_SCALE) {
 		output_for_each_container(output, update_textures, NULL);
+		output_for_each_surface(output, update_output_scale_iterator, NULL);
 	}
 
 	if (event->committed & (WLR_OUTPUT_STATE_TRANSFORM | WLR_OUTPUT_STATE_SCALE)) {
diff --git a/sway/desktop/render.c b/sway/desktop/render.c
index 2b7214c35..a4d633e09 100644
--- a/sway/desktop/render.c
+++ b/sway/desktop/render.c
@@ -32,7 +32,7 @@
 #endif
 
 struct render_data {
-	pixman_region32_t *damage;
+	const pixman_region32_t *damage;
 	float alpha;
 	struct wlr_box *clip_box;
 };
@@ -102,7 +102,7 @@ static void set_scale_filter(struct wlr_output *wlr_output,
 }
 
 static void render_texture(struct wlr_output *wlr_output,
-		pixman_region32_t *output_damage, struct wlr_texture *texture,
+		const pixman_region32_t *output_damage, struct wlr_texture *texture,
 		const struct wlr_fbox *src_box, const struct wlr_box *dst_box,
 		const float matrix[static 9], float alpha) {
 	struct wlr_renderer *renderer = wlr_output->renderer;
@@ -139,7 +139,7 @@ static void render_surface_iterator(struct sway_output *output,
 		struct wlr_box *_box, void *_data) {
 	struct render_data *data = _data;
 	struct wlr_output *wlr_output = output->wlr_output;
-	pixman_region32_t *output_damage = data->damage;
+	const pixman_region32_t *output_damage = data->damage;
 	float alpha = data->alpha;
 
 	struct wlr_texture *texture = wlr_surface_get_texture(surface);
@@ -175,7 +175,7 @@ static void render_surface_iterator(struct sway_output *output,
 }
 
 static void render_layer_toplevel(struct sway_output *output,
-		pixman_region32_t *damage, struct wl_list *layer_surfaces) {
+		const pixman_region32_t *damage, struct wl_list *layer_surfaces) {
 	struct render_data data = {
 		.damage = damage,
 		.alpha = 1.0f,
@@ -185,7 +185,7 @@ static void render_layer_toplevel(struct sway_output *output,
 }
 
 static void render_layer_popups(struct sway_output *output,
-		pixman_region32_t *damage, struct wl_list *layer_surfaces) {
+		const pixman_region32_t *damage, struct wl_list *layer_surfaces) {
 	struct render_data data = {
 		.damage = damage,
 		.alpha = 1.0f,
@@ -196,7 +196,7 @@ static void render_layer_popups(struct sway_output *output,
 
 #if HAVE_XWAYLAND
 static void render_unmanaged(struct sway_output *output,
-		pixman_region32_t *damage, struct wl_list *unmanaged) {
+		const pixman_region32_t *damage, struct wl_list *unmanaged) {
 	struct render_data data = {
 		.damage = damage,
 		.alpha = 1.0f,
@@ -207,7 +207,7 @@ static void render_unmanaged(struct sway_output *output,
 #endif
 
 static void render_drag_icons(struct sway_output *output,
-		pixman_region32_t *damage, struct wl_list *drag_icons) {
+		const pixman_region32_t *damage, struct wl_list *drag_icons) {
 	struct render_data data = {
 		.damage = damage,
 		.alpha = 1.0f,
@@ -219,7 +219,7 @@ static void render_drag_icons(struct sway_output *output,
 // _box.x and .y are expected to be layout-local
 // _box.width and .height are expected to be output-buffer-local
 void render_rect(struct sway_output *output,
-		pixman_region32_t *output_damage, const struct wlr_box *_box,
+		const pixman_region32_t *output_damage, const struct wlr_box *_box,
 		float color[static 4]) {
 	struct wlr_output *wlr_output = output->wlr_output;
 	struct wlr_renderer *renderer = wlr_output->renderer;
@@ -259,7 +259,7 @@ void premultiply_alpha(float color[4], float opacity) {
 }
 
 static void render_view_toplevels(struct sway_view *view,
-		struct sway_output *output, pixman_region32_t *damage, float alpha) {
+		struct sway_output *output, const pixman_region32_t *damage, float alpha) {
 	struct render_data data = {
 		.damage = damage,
 		.alpha = alpha,
@@ -282,7 +282,7 @@ static void render_view_toplevels(struct sway_view *view,
 }
 
 static void render_view_popups(struct sway_view *view,
-		struct sway_output *output, pixman_region32_t *damage, float alpha) {
+		struct sway_output *output, const pixman_region32_t *damage, float alpha) {
 	struct render_data data = {
 		.damage = damage,
 		.alpha = alpha,
@@ -292,7 +292,7 @@ static void render_view_popups(struct sway_view *view,
 }
 
 static void render_saved_view(struct sway_view *view,
-		struct sway_output *output, pixman_region32_t *damage, float alpha) {
+		struct sway_output *output, const pixman_region32_t *damage, float alpha) {
 	struct wlr_output *wlr_output = output->wlr_output;
 
 	if (wl_list_empty(&view->saved_buffers)) {
@@ -355,7 +355,7 @@ static void render_saved_view(struct sway_view *view,
 /**
  * Render a view's surface and left/bottom/right borders.
  */
-static void render_view(struct sway_output *output, pixman_region32_t *damage,
+static void render_view(struct sway_output *output, const pixman_region32_t *damage,
 		struct sway_container *con, struct border_colors *colors) {
 	struct sway_view *view = con->view;
 	if (!wl_list_empty(&view->saved_buffers)) {
@@ -429,7 +429,7 @@ static void render_view(struct sway_output *output, pixman_region32_t *damage,
  * The left side is: 1px border, 2px padding, title
  */
 static void render_titlebar(struct sway_output *output,
-		pixman_region32_t *output_damage, struct sway_container *con,
+		const pixman_region32_t *output_damage, struct sway_container *con,
 		int x, int y, int width,
 		struct border_colors *colors, struct wlr_texture *title_texture,
 		struct wlr_texture *marks_texture) {
@@ -684,7 +684,7 @@ static void render_titlebar(struct sway_output *output,
  * Render the top border line for a view using "border pixel".
  */
 static void render_top_border(struct sway_output *output,
-		pixman_region32_t *output_damage, struct sway_container *con,
+		const pixman_region32_t *output_damage, struct sway_container *con,
 		struct border_colors *colors) {
 	struct sway_container_state *state = &con->current;
 	if (!state->border_top) {
@@ -714,7 +714,7 @@ struct parent_data {
 };
 
 static void render_container(struct sway_output *output,
-	pixman_region32_t *damage, struct sway_container *con, bool parent_focused);
+	const pixman_region32_t *damage, struct sway_container *con, bool parent_focused);
 
 /**
  * Render a container's children using a L_HORIZ or L_VERT layout.
@@ -723,7 +723,7 @@ static void render_container(struct sway_output *output,
  * they'll apply their own borders to their children.
  */
 static void render_containers_linear(struct sway_output *output,
-		pixman_region32_t *damage, struct parent_data *parent) {
+		const pixman_region32_t *damage, struct parent_data *parent) {
 	for (int i = 0; i < parent->children->length; ++i) {
 		struct sway_container *child = parent->children->items[i];
 
@@ -779,7 +779,7 @@ static bool container_has_focused_child(struct sway_container *con) {
  * Render a container's children using the L_TABBED layout.
  */
 static void render_containers_tabbed(struct sway_output *output,
-		pixman_region32_t *damage, struct parent_data *parent) {
+		const pixman_region32_t *damage, struct parent_data *parent) {
 	if (!parent->children->length) {
 		return;
 	}
@@ -848,7 +848,7 @@ static void render_containers_tabbed(struct sway_output *output,
  * Render a container's children using the L_STACKED layout.
  */
 static void render_containers_stacked(struct sway_output *output,
-		pixman_region32_t *damage, struct parent_data *parent) {
+		const pixman_region32_t *damage, struct parent_data *parent) {
 	if (!parent->children->length) {
 		return;
 	}
@@ -908,7 +908,7 @@ static void render_containers_stacked(struct sway_output *output,
 }
 
 static void render_containers(struct sway_output *output,
-		pixman_region32_t *damage, struct parent_data *parent) {
+		const pixman_region32_t *damage, struct parent_data *parent) {
 	if (config->hide_lone_tab && parent->children->length == 1) {
 		struct sway_container *child = parent->children->items[0];
 		if (child->view) {
@@ -933,7 +933,7 @@ static void render_containers(struct sway_output *output,
 }
 
 static void render_container(struct sway_output *output,
-		pixman_region32_t *damage, struct sway_container *con, bool focused) {
+		const pixman_region32_t *damage, struct sway_container *con, bool focused) {
 	struct parent_data data = {
 		.layout = con->current.layout,
 		.box = {
@@ -950,7 +950,7 @@ static void render_container(struct sway_output *output,
 }
 
 static void render_workspace(struct sway_output *output,
-		pixman_region32_t *damage, struct sway_workspace *ws, bool focused) {
+		const pixman_region32_t *damage, struct sway_workspace *ws, bool focused) {
 	struct parent_data data = {
 		.layout = ws->current.layout,
 		.box = {
@@ -967,7 +967,7 @@ static void render_workspace(struct sway_output *output,
 }
 
 static void render_floating_container(struct sway_output *soutput,
-		pixman_region32_t *damage, struct sway_container *con) {
+		const pixman_region32_t *damage, struct sway_container *con) {
 	if (con->view) {
 		struct sway_view *view = con->view;
 		struct border_colors *colors;
@@ -1002,7 +1002,7 @@ static void render_floating_container(struct sway_output *soutput,
 }
 
 static void render_floating(struct sway_output *soutput,
-		pixman_region32_t *damage) {
+		const pixman_region32_t *damage) {
 	for (int i = 0; i < root->outputs->length; ++i) {
 		struct sway_output *output = root->outputs->items[i];
 		for (int j = 0; j < output->current.workspaces->length; ++j) {
@@ -1022,15 +1022,14 @@ static void render_floating(struct sway_output *soutput,
 }
 
 static void render_seatops(struct sway_output *output,
-		pixman_region32_t *damage) {
+		const pixman_region32_t *damage) {
 	struct sway_seat *seat;
 	wl_list_for_each(seat, &server.input->seats, link) {
 		seatop_render(seat, output, damage);
 	}
 }
 
-void output_render(struct sway_output *output, struct timespec *when,
-		pixman_region32_t *damage) {
+void output_render(struct sway_output *output, pixman_region32_t *damage) {
 	struct wlr_output *wlr_output = output->wlr_output;
 	struct wlr_renderer *renderer = output->server->renderer;
 
@@ -1184,30 +1183,4 @@ renderer_end:
 	wlr_renderer_scissor(renderer, NULL);
 	wlr_output_render_software_cursors(wlr_output, damage);
 	wlr_renderer_end(renderer);
-
-	int width, height;
-	wlr_output_transformed_resolution(wlr_output, &width, &height);
-
-	pixman_region32_t frame_damage;
-	pixman_region32_init(&frame_damage);
-
-	enum wl_output_transform transform =
-		wlr_output_transform_invert(wlr_output->transform);
-	wlr_region_transform(&frame_damage, &output->damage_ring.current,
-		transform, width, height);
-
-	if (debug.damage != DAMAGE_DEFAULT) {
-		pixman_region32_union_rect(&frame_damage, &frame_damage,
-			0, 0, wlr_output->width, wlr_output->height);
-	}
-
-	wlr_output_set_damage(wlr_output, &frame_damage);
-	pixman_region32_fini(&frame_damage);
-
-	if (!wlr_output_commit(wlr_output)) {
-		return;
-	}
-
-	wlr_damage_ring_rotate(&output->damage_ring);
-	output->last_frame = *when;
 }
diff --git a/sway/desktop/surface.c b/sway/desktop/surface.c
index 949cfdc26..68772ee0f 100644
--- a/sway/desktop/surface.c
+++ b/sway/desktop/surface.c
@@ -47,7 +47,7 @@ void handle_compositor_new_surface(struct wl_listener *listener, void *data) {
 	}
 }
 
-static void surface_update_outputs(struct wlr_surface *surface) {
+void surface_update_outputs(struct wlr_surface *surface) {
 	float scale = 1;
 	struct wlr_surface_output *surface_output;
 	wl_list_for_each(surface_output, &surface->current_outputs, link) {
diff --git a/sway/input/cursor.c b/sway/input/cursor.c
index ad69e7c39..15687993b 100644
--- a/sway/input/cursor.c
+++ b/sway/input/cursor.c
@@ -364,7 +364,7 @@ void cursor_unhide(struct sway_cursor *cursor) {
 	wl_event_source_timer_update(cursor->hide_source, cursor_get_timeout(cursor));
 }
 
-static void pointer_motion(struct sway_cursor *cursor, uint32_t time_msec,
+void pointer_motion(struct sway_cursor *cursor, uint32_t time_msec,
 		struct wlr_input_device *device, double dx, double dy,
 		double dx_unaccel, double dy_unaccel) {
 	wlr_relative_pointer_manager_v1_send_relative_motion(
@@ -479,43 +479,16 @@ static void handle_touch_down(struct wl_listener *listener, void *data) {
 	cursor_hide(cursor);
 
 	struct sway_seat *seat = cursor->seat;
-	struct wlr_seat *wlr_seat = seat->wlr_seat;
-	struct wlr_surface *surface = NULL;
 
 	double lx, ly;
 	wlr_cursor_absolute_to_layout_coords(cursor->cursor, &event->touch->base,
 			event->x, event->y, &lx, &ly);
-	double sx, sy;
-	struct sway_node *focused_node = node_at_coords(seat, lx, ly, &surface, &sx, &sy);
 
 	seat->touch_id = event->touch_id;
 	seat->touch_x = lx;
 	seat->touch_y = ly;
 
-	if (surface && wlr_surface_accepts_touch(wlr_seat, surface)) {
-		if (seat_is_input_allowed(seat, surface)) {
-			wlr_seat_touch_notify_down(wlr_seat, surface, event->time_msec,
-					event->touch_id, sx, sy);
-
-			if (focused_node) {
-			    seat_set_focus(seat, focused_node);
-			}
-		}
-	} else if (!cursor->simulating_pointer_from_touch &&
-			(!surface || seat_is_input_allowed(seat, surface))) {
-		// Fallback to cursor simulation.
-		// The pointer_touch_id state is needed, so drags are not aborted when over
-		// a surface supporting touch and multi touch events don't interfere.
-		cursor->simulating_pointer_from_touch = true;
-		cursor->pointer_touch_id = seat->touch_id;
-		double dx, dy;
-		dx = lx - cursor->cursor->x;
-		dy = ly - cursor->cursor->y;
-		pointer_motion(cursor, event->time_msec, &event->touch->base, dx, dy,
-			dx, dy);
-		dispatch_cursor_button(cursor, &event->touch->base, event->time_msec,
-				BTN_LEFT, WLR_BUTTON_PRESSED);
-	}
+	seatop_touch_down(seat, event, lx, ly);
 }
 
 static void handle_touch_up(struct wl_listener *listener, void *data) {
@@ -523,7 +496,7 @@ static void handle_touch_up(struct wl_listener *listener, void *data) {
 	struct wlr_touch_up_event *event = data;
 	cursor_handle_activity_from_device(cursor, &event->touch->base);
 
-	struct wlr_seat *wlr_seat = cursor->seat->wlr_seat;
+	struct sway_seat *seat = cursor->seat;
 
 	if (cursor->simulating_pointer_from_touch) {
 		if (cursor->pointer_touch_id == cursor->seat->touch_id) {
@@ -532,7 +505,7 @@ static void handle_touch_up(struct wl_listener *listener, void *data) {
 				event->time_msec, BTN_LEFT, WLR_BUTTON_RELEASED);
 		}
 	} else {
-		wlr_seat_touch_notify_up(wlr_seat, event->time_msec, event->touch_id);
+		seatop_touch_up(seat, event);
 	}
 }
 
@@ -543,19 +516,14 @@ static void handle_touch_motion(struct wl_listener *listener, void *data) {
 	cursor_handle_activity_from_device(cursor, &event->touch->base);
 
 	struct sway_seat *seat = cursor->seat;
-	struct wlr_seat *wlr_seat = seat->wlr_seat;
-	struct wlr_surface *surface = NULL;
 
 	double lx, ly;
 	wlr_cursor_absolute_to_layout_coords(cursor->cursor, &event->touch->base,
 			event->x, event->y, &lx, &ly);
-	double sx, sy;
-	node_at_coords(cursor->seat, lx, ly, &surface, &sx, &sy);
 
 	if (seat->touch_id == event->touch_id) {
 		seat->touch_x = lx;
 		seat->touch_y = ly;
-
 		struct sway_drag_icon *drag_icon;
 		wl_list_for_each(drag_icon, &root->drag_icons, link) {
 			if (drag_icon->seat == seat) {
@@ -572,9 +540,8 @@ static void handle_touch_motion(struct wl_listener *listener, void *data) {
 			pointer_motion(cursor, event->time_msec, &event->touch->base,
 				dx, dy, dx, dy);
 		}
-	} else if (surface) {
-		wlr_seat_touch_notify_motion(wlr_seat, event->time_msec,
-			event->touch_id, sx, sy);
+	} else {
+		seatop_touch_motion(seat, event, lx, ly);
 	}
 }
 
diff --git a/sway/input/libinput.c b/sway/input/libinput.c
index 10bd0e35a..dd4fc0be9 100644
--- a/sway/input/libinput.c
+++ b/sway/input/libinput.c
@@ -220,7 +220,8 @@ bool sway_input_configure_libinput_device(struct sway_input_device *input_device
 
 	bool changed = false;
 	if (ic->mapped_to_output &&
-			!output_by_name_or_id(ic->mapped_to_output)) {
+		strcmp("*", ic->mapped_to_output) != 0 &&
+		!output_by_name_or_id(ic->mapped_to_output)) {
 		sway_log(SWAY_DEBUG,
 				"%s '%s' is mapped to offline output '%s'; disabling input",
 				ic->input_type, ic->identifier, ic->mapped_to_output);
diff --git a/sway/input/seat.c b/sway/input/seat.c
index 090a4d3ca..14931ce0f 100644
--- a/sway/input/seat.c
+++ b/sway/input/seat.c
@@ -1618,6 +1618,26 @@ void seatop_pointer_axis(struct sway_seat *seat,
 	}
 }
 
+void seatop_touch_motion(struct sway_seat *seat, struct wlr_touch_motion_event *event,
+		double lx, double ly) {
+	if (seat->seatop_impl->touch_motion) {
+		seat->seatop_impl->touch_motion(seat, event, lx, ly);
+	}
+}
+
+void seatop_touch_up(struct sway_seat *seat, struct wlr_touch_up_event *event) {
+	if (seat->seatop_impl->touch_up) {
+		seat->seatop_impl->touch_up(seat, event);
+	}
+}
+
+void seatop_touch_down(struct sway_seat *seat, struct wlr_touch_down_event *event,
+		double lx, double ly) {
+	if (seat->seatop_impl->touch_down) {
+		seat->seatop_impl->touch_down(seat, event, lx, ly);
+	}
+}
+
 void seatop_tablet_tool_tip(struct sway_seat *seat,
 		struct sway_tablet_tool *tool, uint32_t time_msec,
 		enum wlr_tablet_tool_tip_state state) {
@@ -1707,7 +1727,7 @@ void seatop_end(struct sway_seat *seat) {
 }
 
 void seatop_render(struct sway_seat *seat, struct sway_output *output,
-		pixman_region32_t *damage) {
+		const pixman_region32_t *damage) {
 	if (seat->seatop_impl->render) {
 		seat->seatop_impl->render(seat, output, damage);
 	}
diff --git a/sway/input/seatop_default.c b/sway/input/seatop_default.c
index 0dcb87ab0..5a55c1866 100644
--- a/sway/input/seatop_default.c
+++ b/sway/input/seatop_default.c
@@ -261,7 +261,7 @@ static void handle_tablet_tool_tip(struct sway_seat *seat,
 
 		// Handle tapping on a container surface
 		seat_set_focus_container(seat, cont);
-		seatop_begin_down(seat, node->sway_container, time_msec, sx, sy);
+		seatop_begin_down(seat, node->sway_container, sx, sy);
 	}
 #if HAVE_XWAYLAND
 	// Handle tapping on an xwayland unmanaged view
@@ -374,7 +374,7 @@ static void handle_button(struct sway_seat *seat, uint32_t time_msec,
 			transaction_commit_dirty();
 		}
 		if (state == WLR_BUTTON_PRESSED) {
-			seatop_begin_down_on_surface(seat, surface, time_msec, sx, sy);
+			seatop_begin_down_on_surface(seat, surface, sx, sy);
 		}
 		seat_pointer_notify_button(seat, time_msec, button, state);
 		return;
@@ -499,7 +499,7 @@ static void handle_button(struct sway_seat *seat, uint32_t time_msec,
 
 	// Handle mousedown on a container surface
 	if (surface && cont && state == WLR_BUTTON_PRESSED) {
-		seatop_begin_down(seat, cont, time_msec, sx, sy);
+		seatop_begin_down(seat, cont, sx, sy);
 		seat_pointer_notify_button(seat, time_msec, button, WLR_BUTTON_PRESSED);
 		return;
 	}
@@ -649,6 +649,36 @@ static void handle_tablet_tool_motion(struct sway_seat *seat,
 	e->previous_node = node;
 }
 
+static void handle_touch_down(struct sway_seat *seat,
+		struct wlr_touch_down_event *event, double lx, double ly) {
+	struct wlr_surface *surface = NULL;
+	struct wlr_seat *wlr_seat = seat->wlr_seat;
+	struct sway_cursor *cursor = seat->cursor;
+	double sx, sy;
+	node_at_coords(seat, seat->touch_x, seat->touch_y, &surface, &sx, &sy);
+
+	if (surface && wlr_surface_accepts_touch(wlr_seat, surface)) {
+		if (seat_is_input_allowed(seat, surface)) {
+			cursor->simulating_pointer_from_touch = false;
+			seatop_begin_touch_down(seat, surface, event, sx, sy, lx, ly);
+		}
+	} else if (!cursor->simulating_pointer_from_touch &&
+			(!surface || seat_is_input_allowed(seat, surface))) {
+		// Fallback to cursor simulation.
+		// The pointer_touch_id state is needed, so drags are not aborted when over
+		// a surface supporting touch and multi touch events don't interfere.
+		cursor->simulating_pointer_from_touch = true;
+		cursor->pointer_touch_id = seat->touch_id;
+		double dx, dy;
+		dx = seat->touch_x - cursor->cursor->x;
+		dy = seat->touch_y - cursor->cursor->y;
+		pointer_motion(cursor, event->time_msec, &event->touch->base, dx, dy,
+				dx, dy);
+		dispatch_cursor_button(cursor, &event->touch->base, event->time_msec,
+				BTN_LEFT, WLR_BUTTON_PRESSED);
+	}
+}
+
 /*----------------------------------------\
  * Functions used by handle_pointer_axis  /
  *--------------------------------------*/
@@ -1096,6 +1126,7 @@ static const struct sway_seatop_impl seatop_impl = {
 	.swipe_begin = handle_swipe_begin,
 	.swipe_update = handle_swipe_update,
 	.swipe_end = handle_swipe_end,
+	.touch_down = handle_touch_down,
 	.rebase = handle_rebase,
 	.allow_set_cursor = true,
 };
diff --git a/sway/input/seatop_down.c b/sway/input/seatop_down.c
index 3f880ccf9..6447134ec 100644
--- a/sway/input/seatop_down.c
+++ b/sway/input/seatop_down.c
@@ -2,12 +2,20 @@
 #include <float.h>
 #include <wlr/types/wlr_cursor.h>
 #include <wlr/types/wlr_tablet_v2.h>
+#include <wlr/types/wlr_touch.h>
 #include "sway/input/cursor.h"
 #include "sway/input/seat.h"
 #include "sway/tree/view.h"
 #include "sway/desktop/transaction.h"
 #include "log.h"
 
+struct seatop_touch_point_event {
+	double ref_lx, ref_ly;         // touch's x/y at start of op
+	double ref_con_lx, ref_con_ly; // container's x/y at start of op
+	int32_t touch_id;
+	struct wl_list link;
+};
+
 struct seatop_down_event {
 	struct sway_container *con;
 	struct sway_seat *seat;
@@ -15,8 +23,87 @@ struct seatop_down_event {
 	struct wlr_surface *surface;
 	double ref_lx, ref_ly;         // cursor's x/y at start of op
 	double ref_con_lx, ref_con_ly; // container's x/y at start of op
+	struct wl_list point_events;   // seatop_touch_point_event::link
 };
 
+static void handle_touch_motion(struct sway_seat *seat,
+		struct wlr_touch_motion_event *event, double lx, double ly) {
+	struct seatop_down_event *e = seat->seatop_data;
+
+	struct seatop_touch_point_event *point_event;
+	bool found = false;
+	wl_list_for_each(point_event, &e->point_events, link) {
+		if (point_event->touch_id == event->touch_id) {
+			found = true;
+			break;
+		}
+	}
+	if (!found) {
+		return; // Probably not a point_event from this seatop_down
+	}
+
+	double moved_x = lx - point_event->ref_lx;
+	double moved_y = ly - point_event->ref_ly;
+	double sx = point_event->ref_con_lx + moved_x;
+	double sy = point_event->ref_con_ly + moved_y;
+
+	wlr_seat_touch_notify_motion(seat->wlr_seat, event->time_msec,
+		event->touch_id, sx, sy);
+}
+
+static void handle_touch_up(struct sway_seat *seat,
+		struct wlr_touch_up_event *event) {
+	struct seatop_down_event *e = seat->seatop_data;
+	struct seatop_touch_point_event *point_event, *tmp;
+
+	wl_list_for_each_safe(point_event, tmp, &e->point_events, link) {
+		if (point_event->touch_id == event->touch_id) {
+			wl_list_remove(&point_event->link);
+			free(point_event);
+			break;
+		}
+	}
+
+	if (wl_list_empty(&e->point_events)) {
+		seatop_begin_default(seat);
+	}
+
+	wlr_seat_touch_notify_up(seat->wlr_seat, event->time_msec, event->touch_id);
+}
+
+static void handle_touch_down(struct sway_seat *seat,
+		struct wlr_touch_down_event *event, double lx, double ly) {
+	struct seatop_down_event *e = seat->seatop_data;
+	double sx, sy;
+	struct wlr_surface *surface = NULL;
+	struct sway_node *focused_node = node_at_coords(seat, seat->touch_x,
+		seat->touch_y, &surface, &sx, &sy);
+
+	if (!surface || surface != e->surface) { // Must start from the initial surface
+		return;
+	}
+
+	struct seatop_touch_point_event *point_event =
+		calloc(1, sizeof(struct seatop_touch_point_event));
+	if (!sway_assert(point_event, "Unable to allocate point_event")) {
+		return;
+	}
+	point_event->touch_id = event->touch_id;
+	point_event->ref_lx = lx;
+	point_event->ref_ly = ly;
+	point_event->ref_con_lx = sx;
+	point_event->ref_con_ly = sy;
+
+	wl_list_insert(&e->point_events, &point_event->link);
+
+	wlr_seat_touch_notify_down(seat->wlr_seat, surface, event->time_msec,
+			event->touch_id, sx, sy);
+
+	if (focused_node) {
+	    seat_set_focus(seat, focused_node);
+	}
+}
+
 static void handle_pointer_axis(struct sway_seat *seat,
 		struct wlr_pointer_axis_event *event) {
 	struct sway_input_device *input_device =
@@ -99,14 +186,17 @@ static const struct sway_seatop_impl seatop_impl = {
 	.pointer_axis = handle_pointer_axis,
 	.tablet_tool_tip = handle_tablet_tool_tip,
 	.tablet_tool_motion = handle_tablet_tool_motion,
+	.touch_motion = handle_touch_motion,
+	.touch_up = handle_touch_up,
+	.touch_down = handle_touch_down,
 	.unref = handle_unref,
 	.end = handle_end,
 	.allow_set_cursor = true,
 };
 
 void seatop_begin_down(struct sway_seat *seat, struct sway_container *con,
-		uint32_t time_msec, double sx, double sy) {
-	seatop_begin_down_on_surface(seat, con->view->surface, time_msec, sx, sy);
+		double sx, double sy) {
+	seatop_begin_down_on_surface(seat, con->view->surface, sx, sy);
 	struct seatop_down_event *e = seat->seatop_data;
 	e->con = con;
 
@@ -114,13 +204,20 @@ void seatop_begin_down(struct sway_seat *seat, struct sway_container *con,
 	transaction_commit_dirty();
 }
 
+void seatop_begin_touch_down(struct sway_seat *seat,
+		struct wlr_surface *surface, struct wlr_touch_down_event *event,
+		double sx, double sy, double lx, double ly) {
+	seatop_begin_down_on_surface(seat, surface, sx, sy);
+	handle_touch_down(seat, event, lx, ly);
+}
+
 void seatop_begin_down_on_surface(struct sway_seat *seat,
-		struct wlr_surface *surface, uint32_t time_msec, double sx, double sy) {
+		struct wlr_surface *surface, double sx, double sy) {
 	seatop_end(seat);
 
 	struct seatop_down_event *e =
 		calloc(1, sizeof(struct seatop_down_event));
-	if (!e) {
+	if (!sway_assert(e, "Unable to allocate e")) {
 		return;
 	}
 	e->con = NULL;
@@ -132,6 +229,7 @@ void seatop_begin_down_on_surface(struct sway_seat *seat,
 	e->ref_ly = seat->cursor->cursor->y;
 	e->ref_con_lx = sx;
 	e->ref_con_ly = sy;
+	wl_list_init(&e->point_events);
 
 	seat->seatop_impl = &seatop_impl;
 	seat->seatop_data = e;
diff --git a/sway/input/seatop_move_tiling.c b/sway/input/seatop_move_tiling.c
index 223c6c08c..5498e9092 100644
--- a/sway/input/seatop_move_tiling.c
+++ b/sway/input/seatop_move_tiling.c
@@ -32,7 +32,7 @@ struct seatop_move_tiling_event {
 };
 
 static void handle_render(struct sway_seat *seat,
-		struct sway_output *output, pixman_region32_t *damage) {
+		struct sway_output *output, const pixman_region32_t *damage) {
 	struct seatop_move_tiling_event *e = seat->seatop_data;
 	if (!e->threshold_reached) {
 		return;
diff --git a/sway/tree/container.c b/sway/tree/container.c
index 207010814..8222a5069 100644
--- a/sway/tree/container.c
+++ b/sway/tree/container.c
@@ -508,7 +508,6 @@ static void render_titlebar_text_texture(struct sway_output *output,
 	cairo_t *c = cairo_create(dummy_surface);
 	cairo_set_antialias(c, CAIRO_ANTIALIAS_BEST);
 	cairo_font_options_t *fo = cairo_font_options_create();
-	cairo_font_options_set_hint_style(fo, CAIRO_HINT_STYLE_FULL);
 	if (output->wlr_output->subpixel == WL_OUTPUT_SUBPIXEL_NONE) {
 		cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_GRAY);
 	} else {
@@ -1764,3 +1763,177 @@ int container_squash(struct sway_container *con) {
 	}
 	return change;
 }
+
+static void swap_places(struct sway_container *con1,
+		struct sway_container *con2) {
+	struct sway_container *temp = malloc(sizeof(struct sway_container));
+	temp->pending.x = con1->pending.x;
+	temp->pending.y = con1->pending.y;
+	temp->pending.width = con1->pending.width;
+	temp->pending.height = con1->pending.height;
+	temp->width_fraction = con1->width_fraction;
+	temp->height_fraction = con1->height_fraction;
+	temp->pending.parent = con1->pending.parent;
+	temp->pending.workspace = con1->pending.workspace;
+	bool temp_floating = container_is_floating(con1);
+
+	con1->pending.x = con2->pending.x;
+	con1->pending.y = con2->pending.y;
+	con1->pending.width = con2->pending.width;
+	con1->pending.height = con2->pending.height;
+	con1->width_fraction = con2->width_fraction;
+	con1->height_fraction = con2->height_fraction;
+
+	con2->pending.x = temp->pending.x;
+	con2->pending.y = temp->pending.y;
+	con2->pending.width = temp->pending.width;
+	con2->pending.height = temp->pending.height;
+	con2->width_fraction = temp->width_fraction;
+	con2->height_fraction = temp->height_fraction;
+
+	int temp_index = container_sibling_index(con1);
+	if (con2->pending.parent) {
+		container_insert_child(con2->pending.parent, con1,
+				container_sibling_index(con2));
+	} else if (container_is_floating(con2)) {
+		workspace_add_floating(con2->pending.workspace, con1);
+	} else {
+		workspace_insert_tiling(con2->pending.workspace, con1,
+				container_sibling_index(con2));
+	}
+	if (temp->pending.parent) {
+		container_insert_child(temp->pending.parent, con2, temp_index);
+	} else if (temp_floating) {
+		workspace_add_floating(temp->pending.workspace, con2);
+	} else {
+		workspace_insert_tiling(temp->pending.workspace, con2, temp_index);
+	}
+
+	free(temp);
+}
+
+static void swap_focus(struct sway_container *con1,
+		struct sway_container *con2, struct sway_seat *seat,
+		struct sway_container *focus) {
+	if (focus == con1 || focus == con2) {
+		struct sway_workspace *ws1 = con1->pending.workspace;
+		struct sway_workspace *ws2 = con2->pending.workspace;
+		enum sway_container_layout layout1 = container_parent_layout(con1);
+		enum sway_container_layout layout2 = container_parent_layout(con2);
+		if (focus == con1 && (layout2 == L_TABBED || layout2 == L_STACKED)) {
+			if (workspace_is_visible(ws2)) {
+				seat_set_focus(seat, &con2->node);
+			}
+			seat_set_focus_container(seat, ws1 != ws2 ? con2 : con1);
+		} else if (focus == con2 && (layout1 == L_TABBED
+					|| layout1 == L_STACKED)) {
+			if (workspace_is_visible(ws1)) {
+				seat_set_focus(seat, &con1->node);
+			}
+			seat_set_focus_container(seat, ws1 != ws2 ? con1 : con2);
+		} else if (ws1 != ws2) {
+			seat_set_focus_container(seat, focus == con1 ? con2 : con1);
+		} else {
+			seat_set_focus_container(seat, focus);
+		}
+	} else {
+		seat_set_focus_container(seat, focus);
+	}
+
+	if (root->fullscreen_global) {
+		seat_set_focus(seat,
+				seat_get_focus_inactive(seat, &root->fullscreen_global->node));
+	}
+}
+
+void container_swap(struct sway_container *con1, struct sway_container *con2) {
+	if (!sway_assert(con1 && con2, "Cannot swap with nothing")) {
+		return;
+	}
+	if (!sway_assert(!container_has_ancestor(con1, con2)
+				&& !container_has_ancestor(con2, con1),
+				"Cannot swap ancestor and descendant")) {
+		return;
+	}
+
+	sway_log(SWAY_DEBUG, "Swapping containers %zu and %zu",
+			con1->node.id, con2->node.id);
+
+	bool scratch1 = con1->scratchpad;
+	bool hidden1 = container_is_scratchpad_hidden(con1);
+	bool scratch2 = con2->scratchpad;
+	bool hidden2 = container_is_scratchpad_hidden(con2);
+	if (scratch1) {
+		if (hidden1) {
+			root_scratchpad_show(con1);
+		}
+		root_scratchpad_remove_container(con1);
+	}
+	if (scratch2) {
+		if (hidden2) {
+			root_scratchpad_show(con2);
+		}
+		root_scratchpad_remove_container(con2);
+	}
+
+	enum sway_fullscreen_mode fs1 = con1->pending.fullscreen_mode;
+	if (fs1) {
+		container_fullscreen_disable(con1);
+	}
+	enum sway_fullscreen_mode fs2 = con2->pending.fullscreen_mode;
+	if (fs2) {
+		container_fullscreen_disable(con2);
+	}
+
+	struct sway_seat *seat = input_manager_current_seat();
+	struct sway_container *focus = seat_get_focused_container(seat);
+	struct sway_workspace *vis1 =
+		output_get_active_workspace(con1->pending.workspace->output);
+	struct sway_workspace *vis2 =
+		output_get_active_workspace(con2->pending.workspace->output);
+	if (!sway_assert(vis1 && vis2, "con1 or con2 are on an output without a"
+				"workspace. This should not happen")) {
+		return;
+	}
+
+	char *stored_prev_name = NULL;
+	if (seat->prev_workspace_name) {
+		stored_prev_name = strdup(seat->prev_workspace_name);
+	}
+
+	swap_places(con1, con2);
+
+	if (!workspace_is_visible(vis1)) {
+		seat_set_focus(seat, seat_get_focus_inactive(seat, &vis1->node));
+	}
+	if (!workspace_is_visible(vis2)) {
+		seat_set_focus(seat, seat_get_focus_inactive(seat, &vis2->node));
+	}
+
+	swap_focus(con1, con2, seat, focus);
+
+	if (stored_prev_name) {
+		free(seat->prev_workspace_name);
+		seat->prev_workspace_name = stored_prev_name;
+	}
+
+	if (scratch1) {
+		root_scratchpad_add_container(con2, NULL);
+		if (!hidden1) {
+			root_scratchpad_show(con2);
+		}
+	}
+	if (scratch2) {
+		root_scratchpad_add_container(con1, NULL);
+		if (!hidden2) {
+			root_scratchpad_show(con1);
+		}
+	}
+
+	if (fs1) {
+		container_set_fullscreen(con2, fs1);
+	}
+	if (fs2) {
+		container_set_fullscreen(con1, fs2);
+	}
+}
diff --git a/sway/tree/view.c b/sway/tree/view.c
index fcb78de36..ec54fed81 100644
--- a/sway/tree/view.c
+++ b/sway/tree/view.c
@@ -369,16 +369,13 @@ void view_set_activated(struct sway_view *view, bool activated) {
 
 void view_request_activate(struct sway_view *view, struct sway_seat *seat) {
 	struct sway_workspace *ws = view->container->pending.workspace;
-	if (!ws) { // hidden scratchpad container
-		return;
-	}
 	if (!seat) {
 		seat = input_manager_current_seat();
 	}
 
 	switch (config->focus_on_window_activation) {
 	case FOWA_SMART:
-		if (workspace_is_visible(ws)) {
+		if (ws && workspace_is_visible(ws)) {
 			seat_set_focus_container(seat, view->container);
 			container_raise_floating(view->container);
 		} else {
@@ -389,8 +386,12 @@ void view_request_activate(struct sway_view *view, struct sway_seat *seat) {
 		view_set_urgent(view, true);
 		break;
 	case FOWA_FOCUS:
-		seat_set_focus_container(seat, view->container);
-		container_raise_floating(view->container);
+		if (container_is_scratchpad_hidden_or_child(view->container)) {
+			root_scratchpad_show(view->container);
+		} else {
+			seat_set_focus_container(seat, view->container);
+			container_raise_floating(view->container);
+		}
 		break;
 	case FOWA_NONE:
 		break;
diff --git a/swaybar/render.c b/swaybar/render.c
index ccf365637..6a983e97d 100644
--- a/swaybar/render.c
+++ b/swaybar/render.c
@@ -774,14 +774,12 @@ void render_frame(struct swaybar_output *output) {
 	ctx.cairo = cairo;
 
 	cairo_font_options_t *fo = cairo_font_options_create();
-	cairo_font_options_set_hint_style(fo, CAIRO_HINT_STYLE_FULL);
 	cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_GRAY);
 	ctx.textaa_safe = fo;
 	if (output->subpixel == WL_OUTPUT_SUBPIXEL_NONE) {
 		ctx.textaa_sharp = ctx.textaa_safe;
 	} else {
 		fo = cairo_font_options_create();
-		cairo_font_options_set_hint_style(fo, CAIRO_HINT_STYLE_FULL);
 		cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_SUBPIXEL);
 		cairo_font_options_set_subpixel_order(fo,
 			to_cairo_subpixel_order(output->subpixel));
diff --git a/swaymsg/main.c b/swaymsg/main.c
index 4d3fa68c2..db9346c4b 100644
--- a/swaymsg/main.c
+++ b/swaymsg/main.c
@@ -60,7 +60,7 @@ static void pretty_print_cmd(json_object *r) {
 	if (!success_object(r)) {
 		json_object *error;
 		if (!json_object_object_get_ex(r, "error", &error)) {
-			printf("An unknkown error occurred");
+			printf("An unknown error occurred");
 		} else {
 			printf("Error: %s\n", json_object_get_string(error));
 		}