diff --git a/include/ipc-json.h b/include/ipc-json.h
index f90d801e4..02b07a23c 100644
--- a/include/ipc-json.h
+++ b/include/ipc-json.h
@@ -9,5 +9,6 @@ json_object *ipc_json_get_version();
 json_object *ipc_json_describe_bar_config(struct bar_config *bar);
 json_object *ipc_json_describe_container(swayc_t *c);
 json_object *ipc_json_describe_container_recursive(swayc_t *c);
+json_object *ipc_json_describe_window(swayc_t *c);
 
 #endif
diff --git a/include/ipc-server.h b/include/ipc-server.h
index aef3aa078..1d1991345 100644
--- a/include/ipc-server.h
+++ b/include/ipc-server.h
@@ -17,6 +17,10 @@ void ipc_event_barconfig_update(struct bar_config *bar);
  * Send IPC mode event to all listening clients
  */
 void ipc_event_mode(const char *mode);
+/**
+ * Send IPC window change event
+ */
+void ipc_event_window(swayc_t *window, const char *change);
 /**
  * Sends an IPC modifier event to all listening clients.  The modifier event
  * includes a key 'change' with the value of state and a key 'modifier' with
diff --git a/include/layout.h b/include/layout.h
index c05e9e699..b982365c8 100644
--- a/include/layout.h
+++ b/include/layout.h
@@ -59,6 +59,7 @@ void move_workspace_to(swayc_t* workspace, swayc_t* destination);
 void update_layout_geometry(swayc_t *parent, enum swayc_layouts prev_layout);
 void update_geometry(swayc_t *view);
 void arrange_windows(swayc_t *container, double width, double height);
+void arrange_backgrounds(void);
 
 swayc_t *get_focused_container(swayc_t *parent);
 swayc_t *get_swayc_in_direction(swayc_t *container, enum movement_direction dir);
diff --git a/sway/commands.c b/sway/commands.c
index aaacf0fc1..5cf93c53f 100644
--- a/sway/commands.c
+++ b/sway/commands.c
@@ -679,6 +679,7 @@ static struct cmd_results *cmd_floating(int argc, char **argv) {
 		view->width = view->height = 0;
 		arrange_windows(swayc_active_workspace(), -1, -1);
 		remove_view_from_scratchpad(view);
+		ipc_event_window(view, "floating");
 	}
 	set_focused_container(view);
 	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
@@ -2495,6 +2496,7 @@ static struct cmd_results *cmd_fullscreen(int argc, char **argv) {
 		arrange_windows(container, -1, -1);
 		workspace->fullscreen = NULL;
 	}
+	ipc_event_window(container, "fullscreen_mode");
 
 	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
 }
diff --git a/sway/extensions.c b/sway/extensions.c
index 1fe15ac57..4611f33ef 100644
--- a/sway/extensions.c
+++ b/sway/extensions.c
@@ -8,6 +8,7 @@
 #include "log.h"
 #include "input_state.h"
 #include "extensions.h"
+#include "ipc-server.h"
 
 struct desktop_shell_state desktop_shell;
 
@@ -128,6 +129,7 @@ static void set_lock_surface(struct wl_client *client, struct wl_resource *resou
 		}
 		wlc_view_set_state(view->handle, WLC_BIT_FULLSCREEN, true);
 		workspace->fullscreen = view;
+		ipc_event_window(view, "fullscreen_mode");
 		desktop_shell.is_locked = true;
 		// reset input state
 		input_init();
diff --git a/sway/focus.c b/sway/focus.c
index ff064b72e..9974ba6a7 100644
--- a/sway/focus.c
+++ b/sway/focus.c
@@ -118,6 +118,10 @@ bool set_focused_container(swayc_t *c) {
 		c = focused;
 	}
 
+	if (c->type == C_VIEW) {
+		// dispatch a window event
+		ipc_event_window(c, "focus");
+	}
 	// update container focus from here to root, making necessary changes along
 	// the way
 	swayc_t *p = c;
@@ -154,6 +158,7 @@ bool set_focused_container(swayc_t *c) {
 		// rearrange if parent container is tabbed/stacked
 		swayc_t *parent = swayc_tabbed_stacked_ancestor(p);
 		if (parent != NULL) {
+			arrange_backgrounds();
 			arrange_windows(parent, -1, -1);
 		}
 	} else if (p->type == C_WORKSPACE) {
diff --git a/sway/handlers.c b/sway/handlers.c
index b95a6e655..abed8cd7a 100644
--- a/sway/handlers.c
+++ b/sway/handlers.c
@@ -369,6 +369,7 @@ static bool handle_view_created(wlc_handle handle) {
 	suspend_workspace_cleanup = true;
 
 	if (newview) {
+		ipc_event_window(newview, "new");
 		set_focused_container(newview);
 		swayc_t *output = swayc_parent_by_type(newview, C_OUTPUT);
 		arrange_windows(output, -1, -1);
@@ -461,6 +462,7 @@ static void handle_view_destroyed(wlc_handle handle) {
 		}
 
 		arrange_windows(parent, -1, -1);
+		ipc_event_window(parent, "close");
 	} else {
 		// Is it unmanaged?
 		int i;
@@ -555,6 +557,7 @@ static void handle_view_properties_updated(wlc_handle view, uint32_t mask) {
 				} else if (c->border_type == B_NORMAL) {
 					update_view_border(c);
 				}
+				ipc_event_window(c, "title");
 			}
 		}
 	}
diff --git a/sway/ipc-json.c b/sway/ipc-json.c
index ca45557ca..4e5ea896c 100644
--- a/sway/ipc-json.c
+++ b/sway/ipc-json.c
@@ -17,6 +17,17 @@ static json_object *ipc_json_create_rect(swayc_t *c) {
 	return rect;
 }
 
+static json_object *ipc_json_create_rect_from_geometry(struct wlc_geometry g) {
+	json_object *rect = json_object_new_object();
+
+	json_object_object_add(rect, "x", json_object_new_int(g.origin.x));
+	json_object_object_add(rect, "y", json_object_new_int(g.origin.y));
+	json_object_object_add(rect, "width", json_object_new_int(g.size.w));
+	json_object_object_add(rect, "height", json_object_new_int(g.size.h));
+
+	return rect;
+}
+
 static const char *ipc_json_border_description(swayc_t *c) {
 	const char *border;
 
@@ -38,10 +49,10 @@ static const char *ipc_json_border_description(swayc_t *c) {
 	return border;
 }
 
-static const char *ipc_json_layout_description(swayc_t *c) {
+static const char *ipc_json_layout_description(enum swayc_layouts l) {
 	const char *layout;
 
-	switch (c->layout) {
+	switch (l) {
 	case L_VERT:
 		layout = "splitv";
 		break;
@@ -111,7 +122,7 @@ static void ipc_json_describe_output(swayc_t *output, json_object *object) {
 static void ipc_json_describe_workspace(swayc_t *workspace, json_object *object) {
 	int num = (isdigit(workspace->name[0])) ? atoi(workspace->name) : -1;
 	bool focused = root_container.focused == workspace->parent && workspace->parent->focused == workspace;
-	const char *layout = ipc_json_layout_description(workspace);
+	const char *layout = ipc_json_layout_description(workspace->layout);
 
 	json_object_object_add(object, "num", json_object_new_int(num));
 	json_object_object_add(object, "focused", json_object_new_boolean(focused));
@@ -121,26 +132,68 @@ static void ipc_json_describe_workspace(swayc_t *workspace, json_object *object)
 	json_object_object_add(object, "layout", (strcmp(layout, "null") == 0) ? NULL : json_object_new_string(layout));
 }
 
-static void ipc_json_describe_view(swayc_t *view, json_object *object) {
-	float percent = ipc_json_child_percentage(view);
-	const char *layout = ipc_json_layout_description(view);
+// window is in the scratchpad ? changed : none
+static const char *ipc_json_get_scratchpad_state(swayc_t *c) {
+	int i;
+	for (i = 0; i < scratchpad->length; i++) {
+		if (scratchpad->items[i] == c) {
+			return "changed";
+		}
+	}
+	return "none"; // we ignore the fresh value
+}
 
-	json_object_object_add(object, "border", json_object_new_string(ipc_json_border_description(view)));
-	json_object_object_add(object, "current_border_width", json_object_new_int(view->border_thickness));
+static void ipc_json_describe_view(swayc_t *c, json_object *object) {
+	json_object *props = json_object_new_object();
+	float percent = ipc_json_child_percentage(c);
+	const char *layout = (c->parent->type == C_CONTAINER) ?
+		ipc_json_layout_description(c->parent->layout) : "none";
+	const char *last_layout = (c->parent->type == C_CONTAINER) ?
+		ipc_json_layout_description(c->parent->prev_layout) : "none";
+	wlc_handle parent = wlc_view_get_parent(c->handle);
+
+	json_object_object_add(object, "id", json_object_new_int(c->handle));
+	json_object_object_add(object, "type", json_object_new_string((c->is_floating) ? "floating_con" : "con"));
+
+	json_object_object_add(object, "scratchpad_state",
+		json_object_new_string(ipc_json_get_scratchpad_state(c)));
 	json_object_object_add(object, "percent", (percent > 0) ? json_object_new_double(percent) : NULL);
 	// TODO: make urgency actually work once Sway supports it
 	json_object_object_add(object, "urgent", json_object_new_boolean(false));
-	json_object_object_add(object, "focused", json_object_new_boolean(view->is_focused));
-	json_object_object_add(object, "type", json_object_new_string((view->is_floating) ? "floating_con" : "con"));
-	json_object_object_add(object, "layout", (strcmp(layout, "null") == 0) ? NULL : json_object_new_string(layout));
+	json_object_object_add(object, "focused", json_object_new_boolean(c->is_focused));
 
-	if (view->class) {
-		json_object_object_add(object, "class", json_object_new_string(view->class));
-	}
+	json_object_object_add(object, "layout",
+		(strcmp(layout, "null") == 0) ? NULL : json_object_new_string(layout));
+	json_object_object_add(object, "last_split_layout",
+		(strcmp(last_layout, "null") == 0) ? NULL : json_object_new_string(last_layout));
+	json_object_object_add(object, "workspace_layout",
+		json_object_new_string(ipc_json_layout_description(swayc_parent_by_type(c, C_WORKSPACE)->layout)));
 
-	if (view->app_id) {
-		json_object_object_add(object, "app_id", json_object_new_string(view->app_id));
-	}
+	json_object_object_add(object, "border", json_object_new_string(ipc_json_border_description(c)));
+	json_object_object_add(object, "current_border_width", json_object_new_int(c->border_thickness));
+
+	json_object_object_add(object, "rect", ipc_json_create_rect(c));
+	json_object_object_add(object, "deco_rect", ipc_json_create_rect_from_geometry(c->title_bar_geometry));
+	json_object_object_add(object, "geometry", ipc_json_create_rect_from_geometry(c->cached_geometry));
+	json_object_object_add(object, "window_rect", ipc_json_create_rect_from_geometry(c->actual_geometry));
+
+	json_object_object_add(object, "name", (c->name) ? json_object_new_string(c->name) : NULL);
+
+	json_object_object_add(object, "window", json_object_new_int(c->handle)); // for the sake of i3 compat
+	json_object_object_add(props, "class", c->class ? json_object_new_string(c->class) :
+		c->app_id ? json_object_new_string(c->app_id) : NULL);
+	json_object_object_add(props, "title", (c->name) ? json_object_new_string(c->name) : NULL);
+	json_object_object_add(props, "transient_for", parent ? json_object_new_int(parent) : NULL);
+	json_object_object_add(object, "window_properties", props);
+
+	json_object_object_add(object, "fullscreen_mode",
+		json_object_new_int(swayc_is_fullscreen(c) ? 1 : 0));
+	json_object_object_add(object, "sticky", json_object_new_boolean(c->sticky));
+	json_object_object_add(object, "floating", json_object_new_string(
+		c->is_floating ? "auto_on" : "auto_off")); // we can't state the cause
+
+	json_object_object_add(object, "app_id", c->app_id ? json_object_new_string(c->app_id) : NULL);
+	// we do not include children, floating, unmanaged etc. as views have none
 }
 
 json_object *ipc_json_describe_container(swayc_t *c) {
diff --git a/sway/ipc-server.c b/sway/ipc-server.c
index a50b64a24..33963b501 100644
--- a/sway/ipc-server.c
+++ b/sway/ipc-server.c
@@ -54,6 +54,8 @@ bool ipc_send_reply(struct ipc_client *client, const char *payload, uint32_t pay
 void ipc_get_workspaces_callback(swayc_t *workspace, void *data);
 void ipc_get_outputs_callback(swayc_t *container, void *data);
 
+#define event_mask(ev) (1 << (ev & 0x7F))
+
 void ipc_init(void) {
 	ipc_socket = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
 	if (ipc_socket == -1) {
@@ -334,16 +336,18 @@ void ipc_client_handle_command(struct ipc_client *client) {
 		for (int i = 0; i < json_object_array_length(request); i++) {
 			const char *event_type = json_object_get_string(json_object_array_get_idx(request, i));
 			if (strcmp(event_type, "workspace") == 0) {
-				client->subscribed_events |= IPC_EVENT_WORKSPACE;
+				client->subscribed_events |= event_mask(IPC_EVENT_WORKSPACE);
 			} else if (strcmp(event_type, "barconfig_update") == 0) {
-				client->subscribed_events |= IPC_EVENT_BARCONFIG_UPDATE;
+				client->subscribed_events |= event_mask(IPC_EVENT_BARCONFIG_UPDATE);
 			} else if (strcmp(event_type, "mode") == 0) {
-				client->subscribed_events |= IPC_EVENT_MODE;
+				client->subscribed_events |= event_mask(IPC_EVENT_MODE);
+			} else if (strcmp(event_type, "window") == 0) {
+				client->subscribed_events |= event_mask(IPC_EVENT_WINDOW);
 			} else if (strcmp(event_type, "modifier") == 0) {
-				client->subscribed_events |= IPC_EVENT_MODIFIER;
+				client->subscribed_events |= event_mask(IPC_EVENT_MODIFIER);
 #if SWAY_BINDING_EVENT
 			} else if (strcmp(event_type, "binding") == 0) {
-				client->subscribed_events |= IPC_EVENT_BINDING;
+				client->subscribed_events |= event_mask(IPC_EVENT_BINDING);
 #endif
 			} else {
 				ipc_send_reply(client, "{\"success\": false}", 18);
@@ -530,7 +534,7 @@ void ipc_send_event(const char *json_string, enum ipc_command_type event) {
 	struct ipc_client *client;
 	for (i = 0; i < ipc_client_list->length; i++) {
 		client = ipc_client_list->items[i];
-		if ((client->subscribed_events & event) == 0) {
+		if ((client->subscribed_events & event_mask(event)) == 0) {
 			continue;
 		}
 		client->current_command = event;
@@ -564,6 +568,21 @@ void ipc_event_workspace(swayc_t *old, swayc_t *new, const char *change) {
 	json_object_put(obj); // free
 }
 
+void ipc_event_window(swayc_t *window, const char *change) {
+	json_object *obj = json_object_new_object();
+	json_object_object_add(obj, "change", json_object_new_string(change));
+	if (strcmp(change, "close") == 0 || !window) {
+		json_object_object_add(obj, "container", NULL);
+	} else {
+		json_object_object_add(obj, "container", ipc_json_describe_container(window));
+	}
+
+	const char *json_string = json_object_to_json_string(obj);
+	ipc_send_event(json_string, IPC_EVENT_WINDOW);
+
+	json_object_put(obj); // free
+}
+
 void ipc_event_barconfig_update(struct bar_config *bar) {
 	json_object *json = ipc_json_describe_bar_config(bar);
 	const char *json_string = json_object_to_json_string(json);
diff --git a/sway/layout.c b/sway/layout.c
index 2e0bf0bb3..3285560be 100644
--- a/sway/layout.c
+++ b/sway/layout.c
@@ -91,6 +91,7 @@ void add_floating(swayc_t *ws, swayc_t *child) {
 	if (!ws->focused) {
 		ws->focused = child;
 	}
+	ipc_event_window(child, "floating");
 }
 
 swayc_t *add_sibling(swayc_t *fixed, swayc_t *active) {
@@ -305,6 +306,7 @@ void move_container(swayc_t *container, enum movement_direction dir) {
 		parent = child->parent;
 	}
 	arrange_windows(parent->parent, -1, -1);
+	ipc_event_window(container, "move");
 	set_focused_container_for(parent->parent, container);
 }
 
@@ -520,8 +522,8 @@ void update_geometry(swayc_t *container) {
 		return;
 	}
 
-	swayc_t *ws = swayc_parent_by_type(container, C_WORKSPACE);
-	swayc_t *op = ws->parent;
+	swayc_t *workspace = swayc_parent_by_type(container, C_WORKSPACE);
+	swayc_t *op = workspace->parent;
 	swayc_t *parent = container->parent;
 
 	struct wlc_geometry geometry = {
@@ -550,7 +552,7 @@ void update_geometry(swayc_t *container) {
 		geometry.origin.y = 0;
 		geometry.size.w = size->w;
 		geometry.size.h = size->h;
-		if (op->focused == ws) {
+		if (op->focused == workspace) {
 			wlc_view_bring_to_front(container->handle);
 		}
 
@@ -568,30 +570,23 @@ void update_geometry(swayc_t *container) {
 		int border_right = container->border_thickness;
 
 		// handle hide_edge_borders
-		if (config->hide_edge_borders != E_NONE && (gap <= 0 || (config->smart_gaps && ws->children->length == 1))) {
-			swayc_t *output = swayc_parent_by_type(container, C_OUTPUT);
-			const struct wlc_size *size = wlc_output_get_resolution(output->handle);
-
+		if (config->hide_edge_borders != E_NONE && (gap <= 0 || (config->smart_gaps && workspace->children->length == 1))) {
 			if (config->hide_edge_borders == E_HORIZONTAL || config->hide_edge_borders == E_BOTH) {
-				if (geometry.origin.x == 0 || geometry.origin.x == container->x) {
-					// should work for swaybar at left
+				if (geometry.origin.x == workspace->x) {
 					border_left = 0;
 				}
 
-				if (geometry.origin.x + geometry.size.w == size->w || geometry.size.w == container->width) {
-					// should work for swaybar at right
+				if (geometry.origin.x + geometry.size.w == workspace->width) {
 					border_right = 0;
 				}
 			}
 
 			if (config->hide_edge_borders == E_VERTICAL || config->hide_edge_borders == E_BOTH) {
-				if (geometry.origin.y == 0 || geometry.origin.y == container->y) {
-					// this works for swaybar at top
+				if (geometry.origin.y == workspace->y) {
 					border_top = 0;
 				}
 
-				if (geometry.origin.y + geometry.size.h == size->h || geometry.size.h == container->height) {
-					// this works for swaybar at bottom
+				if (geometry.origin.y + geometry.size.h == workspace->height) {
 					border_bottom = 0;
 				}
 			}
@@ -959,9 +954,12 @@ void arrange_windows(swayc_t *container, double width, double height) {
 	update_visibility(container);
 	arrange_windows_r(container, width, height);
 	layout_log(&root_container, 0);
+}
 
+void arrange_backgrounds(void) {
+	struct background_config *bg;
 	for (int i = 0; i < desktop_shell.backgrounds->length; ++i) {
-		struct background_config *bg = desktop_shell.backgrounds->items[i];
+		bg = desktop_shell.backgrounds->items[i];
 		wlc_view_send_to_back(bg->handle);
 	}
 }
diff --git a/sway/workspace.c b/sway/workspace.c
index 913a412c5..9c3e69a95 100644
--- a/sway/workspace.c
+++ b/sway/workspace.c
@@ -308,6 +308,7 @@ bool workspace_switch(swayc_t *workspace) {
 		return false;
 	}
 	swayc_t *output = swayc_parent_by_type(workspace, C_OUTPUT);
+	arrange_backgrounds();
 	arrange_windows(output, -1, -1);
 	return true;
 }