aboutsummaryrefslogtreecommitdiff
path: root/sway/input/seatop_move_tiling.c
blob: c525b77a9e9b22395775d3ead273351bb6414136 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
#include <limits.h>
#include <wlr/types/wlr_cursor.h>
#include <wlr/util/edges.h>
#include "sway/desktop/transaction.h"
#include "sway/input/cursor.h"
#include "sway/input/seat.h"
#include "sway/ipc-server.h"
#include "sway/output.h"
#include "sway/tree/arrange.h"
#include "sway/tree/node.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"

// Thickness of the dropzone when dragging to the edge of a layout container
#define DROP_LAYOUT_BORDER 30

// Thickness of indicator when dropping onto a titlebar.  This should be a
// multiple of 2.
#define DROP_SPLIT_INDICATOR 10

struct seatop_move_tiling_event {
	struct sway_container *con;
	struct sway_node *target_node;
	enum wlr_edges target_edge;
	double ref_lx, ref_ly; // cursor's x/y at start of op
	bool threshold_reached;
	bool split_target;
	bool insert_after_target;
	struct wlr_scene_rect *indicator_rect;
};

static void handle_end(struct sway_seat *seat) {
	struct seatop_move_tiling_event *e = seat->seatop_data;
	wlr_scene_node_destroy(&e->indicator_rect->node);
	e->indicator_rect = NULL;
}

static void handle_motion_prethreshold(struct sway_seat *seat) {
	struct seatop_move_tiling_event *e = seat->seatop_data;
	double cx = seat->cursor->cursor->x;
	double cy = seat->cursor->cursor->y;
	double sx = e->ref_lx;
	double sy = e->ref_ly;

	// Get the scaled threshold for the output. Even if the operation goes
	// across multiple outputs of varying scales, just use the scale for the
	// output that the cursor is currently on for simplicity.
	struct wlr_output *wlr_output = wlr_output_layout_output_at(
			root->output_layout, cx, cy);
	double output_scale = wlr_output ? wlr_output->scale : 1;
	double threshold = config->tiling_drag_threshold * output_scale;
	threshold *= threshold;

	// If the threshold has been exceeded, start the actual drag
	if ((cx - sx) * (cx - sx) + (cy - sy) * (cy - sy) > threshold) {
		wlr_scene_node_set_enabled(&e->indicator_rect->node, true);
		e->threshold_reached = true;
		cursor_set_image(seat->cursor, "grab", NULL);
	}
}

static void resize_box(struct wlr_box *box, enum wlr_edges edge,
		int thickness) {
	switch (edge) {
	case WLR_EDGE_TOP:
		box->height = thickness;
		break;
	case WLR_EDGE_LEFT:
		box->width = thickness;
		break;
	case WLR_EDGE_RIGHT:
		box->x = box->x + box->width - thickness;
		box->width = thickness;
		break;
	case WLR_EDGE_BOTTOM:
		box->y = box->y + box->height - thickness;
		box->height = thickness;
		break;
	case WLR_EDGE_NONE:
		box->x += thickness;
		box->y += thickness;
		box->width -= thickness * 2;
		box->height -= thickness * 2;
		break;
	}
}

static void split_border(double pos, int offset, int len, int n_children,
		int avoid, int *out_pos, bool *out_after) {
	int region = 2 * n_children * (pos - offset) / len;
	// If the cursor is over the right side of a left-adjacent titlebar, or the
	// left side of a right-adjacent titlebar, it's position when dropped will
	// be the same.  To avoid this, shift the region for adjacent containers.
	if (avoid >= 0) {
		if (region == 2 * avoid - 1 || region == 2 * avoid) {
			region--;
		} else if (region == 2 * avoid + 1 || region == 2 * avoid + 2) {
			region++;
		}
	}

	int child_index = (region + 1) / 2;
	*out_after = region % 2;
	// When dropping at the beginning or end of a container, show the drop
	// region within the container boundary, otherwise show it on top of the
	// border between two titlebars.
	if (child_index == 0) {
		*out_pos = offset;
	} else if (child_index == n_children) {
		*out_pos = offset + len - DROP_SPLIT_INDICATOR;
	} else {
		*out_pos = offset + child_index * len / n_children -
			DROP_SPLIT_INDICATOR / 2;
	}
}

static bool split_titlebar(struct sway_node *node, struct sway_container *avoid,
		struct wlr_cursor *cursor, struct wlr_box *title_box, bool *after) {
	struct sway_container *con = node->sway_container;
	struct sway_node *parent = &con->pending.parent->node;
	int title_height = container_titlebar_height();
	struct wlr_box box;
	int n_children, avoid_index;
	enum sway_container_layout layout =
		parent ? node_get_layout(parent) : L_NONE;
	if (layout == L_TABBED || layout == L_STACKED) {
		node_get_box(parent, &box);
		n_children = node_get_children(parent)->length;
		avoid_index = list_find(node_get_children(parent), avoid);
	} else {
		node_get_box(node, &box);
		n_children = 1;
		avoid_index = -1;
	}
	if (layout == L_STACKED && cursor->y < box.y + title_height * n_children) {
		// Drop into stacked titlebars.
		title_box->width = box.width;
		title_box->height = DROP_SPLIT_INDICATOR;
		title_box->x = box.x;
		split_border(cursor->y, box.y, title_height * n_children,
			n_children, avoid_index, &title_box->y, after);
		return true;
	} else if (layout != L_STACKED && cursor->y < box.y + title_height) {
		// Drop into side-by-side titlebars.
		title_box->width = DROP_SPLIT_INDICATOR;
		title_box->height = title_height;
		title_box->y = box.y;
		split_border(cursor->x, box.x, box.width, n_children,
			avoid_index, &title_box->x, after);
		return true;
	}
	return false;
}

static void update_indicator(struct seatop_move_tiling_event *e, struct wlr_box *box) {
	wlr_scene_node_set_position(&e->indicator_rect->node, box->x, box->y);
	wlr_scene_rect_set_size(e->indicator_rect, box->width, box->height);
}

static void handle_motion_postthreshold(struct sway_seat *seat) {
	struct seatop_move_tiling_event *e = seat->seatop_data;
	e->split_target = false;
	struct wlr_surface *surface = NULL;
	double sx, sy;
	struct sway_cursor *cursor = seat->cursor;
	struct sway_node *node = node_at_coords(seat,
			cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy);

	if (!node) {
		// Eg. hovered over a layer surface such as swaybar
		e->target_node = NULL;
		e->target_edge = WLR_EDGE_NONE;
		return;
	}

	if (node->type == N_WORKSPACE) {
		// Empty workspace
		e->target_node = node;
		e->target_edge = WLR_EDGE_NONE;

		struct wlr_box drop_box;
		workspace_get_box(node->sway_workspace, &drop_box);
		update_indicator(e, &drop_box);
		return;
	}

	// Deny moving within own workspace if this is the only child
	struct sway_container *con = node->sway_container;
	if (workspace_num_tiling_views(e->con->pending.workspace) == 1 &&
			con->pending.workspace == e->con->pending.workspace) {
		e->target_node = NULL;
		e->target_edge = WLR_EDGE_NONE;
		return;
	}

	struct wlr_box drop_box = {
		.x = con->pending.content_x,
		.y = con->pending.content_y,
		.width = con->pending.content_width,
		.height = con->pending.content_height,
	};

	// Check if the cursor is over a tilebar only if the destination
	// container is not a descendant of the source container.
	if (!surface && !container_has_ancestor(con, e->con) &&
			split_titlebar(node, e->con, cursor->cursor,
				&drop_box, &e->insert_after_target)) {
		// Don't allow dropping over the source container's titlebar
		// to give users a chance to cancel a drag operation.
		if (con == e->con) {
			e->target_node = NULL;
		} else {
			e->target_node = node;
			e->split_target = true;
		}
		e->target_edge = WLR_EDGE_NONE;
		update_indicator(e, &drop_box);
		return;
	}

	// Traverse the ancestors, trying to find a layout container perpendicular
	// to the edge. Eg. close to the top or bottom of a horiz layout.
	int thresh_top = con->pending.content_y + DROP_LAYOUT_BORDER;
	int thresh_bottom = con->pending.content_y +
		con->pending.content_height - DROP_LAYOUT_BORDER;
	int thresh_left = con->pending.content_x + DROP_LAYOUT_BORDER;
	int thresh_right = con->pending.content_x +
		con->pending.content_width - DROP_LAYOUT_BORDER;
	while (con) {
		enum wlr_edges edge = WLR_EDGE_NONE;
		enum sway_container_layout layout = container_parent_layout(con);
		struct wlr_box box;
		node_get_box(node_get_parent(&con->node), &box);
		if (layout == L_HORIZ || layout == L_TABBED) {
			if (cursor->cursor->y < thresh_top) {
				edge = WLR_EDGE_TOP;
				box.height = thresh_top - box.y;
			} else if (cursor->cursor->y > thresh_bottom) {
				edge = WLR_EDGE_BOTTOM;
				box.height = box.y + box.height - thresh_bottom;
				box.y = thresh_bottom;
			}
		} else if (layout == L_VERT || layout == L_STACKED) {
			if (cursor->cursor->x < thresh_left) {
				edge = WLR_EDGE_LEFT;
				box.width = thresh_left - box.x;
			} else if (cursor->cursor->x > thresh_right) {
				edge = WLR_EDGE_RIGHT;
				box.width = box.x + box.width - thresh_right;
				box.x = thresh_right;
			}
		}
		if (edge) {
			e->target_node = node_get_parent(&con->node);
			if (e->target_node == &e->con->node) {
				e->target_node = node_get_parent(e->target_node);
			}
			e->target_edge = edge;
			update_indicator(e, &box);
			return;
		}
		con = con->pending.parent;
	}

	// Use the hovered view - but we must be over the actual surface
	con = node->sway_container;
	if (!con->view || !con->view->surface || node == &e->con->node
			|| node_has_ancestor(node, &e->con->node)) {
		e->target_node = NULL;
		e->target_edge = WLR_EDGE_NONE;
		return;
	}

	// Find the closest edge
	size_t thickness = fmin(con->pending.content_width, con->pending.content_height) * 0.3;
	size_t closest_dist = INT_MAX;
	size_t dist;
	e->target_edge = WLR_EDGE_NONE;
	if ((dist = cursor->cursor->y - con->pending.y) < closest_dist) {
		closest_dist = dist;
		e->target_edge = WLR_EDGE_TOP;
	}
	if ((dist = cursor->cursor->x - con->pending.x) < closest_dist) {
		closest_dist = dist;
		e->target_edge = WLR_EDGE_LEFT;
	}
	if ((dist = con->pending.x + con->pending.width - cursor->cursor->x) < closest_dist) {
		closest_dist = dist;
		e->target_edge = WLR_EDGE_RIGHT;
	}
	if ((dist = con->pending.y + con->pending.height - cursor->cursor->y) < closest_dist) {
		closest_dist = dist;
		e->target_edge = WLR_EDGE_BOTTOM;
	}

	if (closest_dist > thickness) {
		e->target_edge = WLR_EDGE_NONE;
	}

	e->target_node = node;
	resize_box(&drop_box, e->target_edge, thickness);
	update_indicator(e, &drop_box);
}

static void handle_pointer_motion(struct sway_seat *seat, uint32_t time_msec) {
	struct seatop_move_tiling_event *e = seat->seatop_data;
	if (e->threshold_reached) {
		handle_motion_postthreshold(seat);
	} else {
		handle_motion_prethreshold(seat);
	}
	transaction_commit_dirty();
}

static bool is_parallel(enum sway_container_layout layout,
		enum wlr_edges edge) {
	bool layout_is_horiz = layout == L_HORIZ || layout == L_TABBED;
	bool edge_is_horiz = edge == WLR_EDGE_LEFT || edge == WLR_EDGE_RIGHT;
	return layout_is_horiz == edge_is_horiz;
}

static void finalize_move(struct sway_seat *seat) {
	struct seatop_move_tiling_event *e = seat->seatop_data;

	if (!e->target_node) {
		seatop_begin_default(seat);
		return;
	}

	struct sway_container *con = e->con;
	struct sway_container *old_parent = con->pending.parent;
	struct sway_workspace *old_ws = con->pending.workspace;
	struct sway_node *target_node = e->target_node;
	struct sway_workspace *new_ws = target_node->type == N_WORKSPACE ?
		target_node->sway_workspace : target_node->sway_container->pending.workspace;
	enum wlr_edges edge = e->target_edge;
	int after = edge != WLR_EDGE_TOP && edge != WLR_EDGE_LEFT;
	bool swap = edge == WLR_EDGE_NONE && target_node->type == N_CONTAINER &&
		!e->split_target;

	if (!swap) {
		container_detach(con);
	}

	// Moving container into empty workspace
	if (target_node->type == N_WORKSPACE && edge == WLR_EDGE_NONE) {
		con = workspace_add_tiling(new_ws, con);
	} else if (e->split_target) {
		struct sway_container *target = target_node->sway_container;
		enum sway_container_layout layout = container_parent_layout(target);
		if (layout != L_TABBED && layout != L_STACKED) {
			container_split(target, L_TABBED);
		}
		container_add_sibling(target, con, e->insert_after_target);
		ipc_event_window(con, "move");
	} else if (target_node->type == N_CONTAINER) {
		// Moving container before/after another
		struct sway_container *target = target_node->sway_container;
		if (swap) {
			container_swap(target_node->sway_container, con);
		} else {
			enum sway_container_layout layout = container_parent_layout(target);
			if (edge && !is_parallel(layout, edge)) {
				enum sway_container_layout new_layout = edge == WLR_EDGE_TOP ||
					edge == WLR_EDGE_BOTTOM ? L_VERT : L_HORIZ;
				container_split(target, new_layout);
			}
			container_add_sibling(target, con, after);
			ipc_event_window(con, "move");
		}
	} else {
		// Target is a workspace which requires splitting
		enum sway_container_layout new_layout = edge == WLR_EDGE_TOP ||
			edge == WLR_EDGE_BOTTOM ? L_VERT : L_HORIZ;
		workspace_split(new_ws, new_layout);
		workspace_insert_tiling(new_ws, con, after);
	}

	if (old_parent) {
		container_reap_empty(old_parent);
	}

	// This is a bit dirty, but we'll set the dimensions to that of a sibling.
	// I don't think there's any other way to make it consistent without
	// changing how we auto-size containers.
	list_t *siblings = container_get_siblings(con);
	if (siblings->length > 1) {
		int index = list_find(siblings, con);
		struct sway_container *sibling = index == 0 ?
			siblings->items[1] : siblings->items[index - 1];
		con->pending.width = sibling->pending.width;
		con->pending.height = sibling->pending.height;
		con->width_fraction = sibling->width_fraction;
		con->height_fraction = sibling->height_fraction;
	}

	arrange_workspace(old_ws);
	if (new_ws != old_ws) {
		arrange_workspace(new_ws);
	}

	transaction_commit_dirty();
	seatop_begin_default(seat);
}

static void handle_button(struct sway_seat *seat, uint32_t time_msec,
		struct wlr_input_device *device, uint32_t button,
		enum wl_pointer_button_state state) {
	if (seat->cursor->pressed_button_count == 0) {
		finalize_move(seat);
	}
}

static void handle_tablet_tool_tip(struct sway_seat *seat,
		struct sway_tablet_tool *tool, uint32_t time_msec,
		enum wlr_tablet_tool_tip_state state) {
	if (state == WLR_TABLET_TOOL_TIP_UP) {
		finalize_move(seat);
	}
}

static void handle_unref(struct sway_seat *seat, struct sway_container *con) {
	struct seatop_move_tiling_event *e = seat->seatop_data;
	if (e->target_node == &con->node) { // Drop target
		e->target_node = NULL;
	}
	if (e->con == con) { // The container being moved
		seatop_begin_default(seat);
	}
}

static const struct sway_seatop_impl seatop_impl = {
	.button = handle_button,
	.pointer_motion = handle_pointer_motion,
	.tablet_tool_tip = handle_tablet_tool_tip,
	.unref = handle_unref,
	.end = handle_end,
};

void seatop_begin_move_tiling_threshold(struct sway_seat *seat,
		struct sway_container *con) {
	seatop_end(seat);

	struct seatop_move_tiling_event *e =
		calloc(1, sizeof(struct seatop_move_tiling_event));
	if (!e) {
		return;
	}

	const float *indicator = config->border_colors.focused.indicator;
	float color[4] = {
		indicator[0] * .5,
		indicator[1] * .5,
		indicator[2] * .5,
		indicator[3] * .5,
	};
	e->indicator_rect = wlr_scene_rect_create(seat->scene_tree, 0, 0, color);
	if (!e->indicator_rect) {
		free(e);
		return;
	}

	e->con = con;
	e->ref_lx = seat->cursor->cursor->x;
	e->ref_ly = seat->cursor->cursor->y;

	seat->seatop_impl = &seatop_impl;
	seat->seatop_data = e;

	container_raise_floating(con);
	transaction_commit_dirty();
	wlr_seat_pointer_notify_clear_focus(seat->wlr_seat);
}

void seatop_begin_move_tiling(struct sway_seat *seat,
		struct sway_container *con) {
	seatop_begin_move_tiling_threshold(seat, con);
	struct seatop_move_tiling_event *e = seat->seatop_data;
	if (e) {
		e->threshold_reached = true;
		cursor_set_image(seat->cursor, "grab", NULL);
	}
}