1 #include <errno.h>
2 #include <limits.h>
3 #include <math.h>
4 #include <stdbool.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <strings.h>
8 #include <wlr/util/edges.h>
9 #include "sway/commands.h"
10 #include "sway/tree/arrange.h"
11 #include "sway/tree/view.h"
12 #include "sway/tree/workspace.h"
13 #include "log.h"
14 
15 #define AXIS_HORIZONTAL (WLR_EDGE_LEFT | WLR_EDGE_RIGHT)
16 #define AXIS_VERTICAL   (WLR_EDGE_TOP | WLR_EDGE_BOTTOM)
17 
18 enum resize_unit {
19 	RESIZE_UNIT_PX,
20 	RESIZE_UNIT_PPT,
21 	RESIZE_UNIT_DEFAULT,
22 	RESIZE_UNIT_INVALID,
23 };
24 
25 struct resize_amount {
26 	int amount;
27 	enum resize_unit unit;
28 };
29 
parse_resize_unit(const char * unit)30 static enum resize_unit parse_resize_unit(const char *unit) {
31 	if (strcasecmp(unit, "px") == 0) {
32 		return RESIZE_UNIT_PX;
33 	}
34 	if (strcasecmp(unit, "ppt") == 0) {
35 		return RESIZE_UNIT_PPT;
36 	}
37 	if (strcasecmp(unit, "default") == 0) {
38 		return RESIZE_UNIT_DEFAULT;
39 	}
40 	return RESIZE_UNIT_INVALID;
41 }
42 
43 // Parse arguments such as "10", "10px" or "10 px".
44 // Returns the number of arguments consumed.
parse_resize_amount(int argc,char ** argv,struct resize_amount * amount)45 static int parse_resize_amount(int argc, char **argv,
46 		struct resize_amount *amount) {
47 	char *err;
48 	amount->amount = (int)strtol(argv[0], &err, 10);
49 	if (*err) {
50 		// e.g. 10px
51 		amount->unit = parse_resize_unit(err);
52 		return 1;
53 	}
54 	if (argc == 1) {
55 		amount->unit = RESIZE_UNIT_DEFAULT;
56 		return 1;
57 	}
58 	// Try the second argument
59 	amount->unit = parse_resize_unit(argv[1]);
60 	if (amount->unit == RESIZE_UNIT_INVALID) {
61 		amount->unit = RESIZE_UNIT_DEFAULT;
62 		return 1;
63 	}
64 	return 2;
65 }
66 
parse_resize_axis(const char * axis)67 static uint32_t parse_resize_axis(const char *axis) {
68 	if (strcasecmp(axis, "width") == 0 || strcasecmp(axis, "horizontal") == 0) {
69 		return AXIS_HORIZONTAL;
70 	}
71 	if (strcasecmp(axis, "height") == 0 || strcasecmp(axis, "vertical") == 0) {
72 		return AXIS_VERTICAL;
73 	}
74 	if (strcasecmp(axis, "up") == 0) {
75 		return WLR_EDGE_TOP;
76 	}
77 	if (strcasecmp(axis, "down") == 0) {
78 		return WLR_EDGE_BOTTOM;
79 	}
80 	if (strcasecmp(axis, "left") == 0) {
81 		return WLR_EDGE_LEFT;
82 	}
83 	if (strcasecmp(axis, "right") == 0) {
84 		return WLR_EDGE_RIGHT;
85 	}
86 	return WLR_EDGE_NONE;
87 }
88 
is_horizontal(uint32_t axis)89 static bool is_horizontal(uint32_t axis) {
90 	return axis & (WLR_EDGE_LEFT | WLR_EDGE_RIGHT);
91 }
92 
container_find_resize_parent(struct sway_container * con,uint32_t axis)93 struct sway_container *container_find_resize_parent(struct sway_container *con,
94 		uint32_t axis) {
95 	enum sway_container_layout parallel_layout =
96 		is_horizontal(axis) ? L_HORIZ : L_VERT;
97 	bool allow_first = axis != WLR_EDGE_TOP && axis != WLR_EDGE_LEFT;
98 	bool allow_last = axis != WLR_EDGE_RIGHT && axis != WLR_EDGE_BOTTOM;
99 
100 	while (con) {
101 		list_t *siblings = container_get_siblings(con);
102 		int index = container_sibling_index(con);
103 		if (container_parent_layout(con) == parallel_layout &&
104 				siblings->length > 1 && (allow_first || index > 0) &&
105 				(allow_last || index < siblings->length - 1)) {
106 			return con;
107 		}
108 		con = con->parent;
109 	}
110 
111 	return NULL;
112 }
113 
container_resize_tiled(struct sway_container * con,uint32_t axis,int amount)114 void container_resize_tiled(struct sway_container *con,
115 		uint32_t axis, int amount) {
116 	if (!con) {
117 		return;
118 	}
119 
120 	con = container_find_resize_parent(con, axis);
121 	if (!con) {
122 		// Can't resize in this direction
123 		return;
124 	}
125 
126 	// For HORIZONTAL or VERTICAL, we are growing in two directions so select
127 	// both adjacent siblings. For RIGHT or DOWN, just select the next sibling.
128 	// For LEFT or UP, convert it to a RIGHT or DOWN resize and reassign con to
129 	// the previous sibling.
130 	struct sway_container *prev = NULL;
131 	struct sway_container *next = NULL;
132 	list_t *siblings = container_get_siblings(con);
133 	int index = container_sibling_index(con);
134 
135 	if (axis == AXIS_HORIZONTAL || axis == AXIS_VERTICAL) {
136 		if (index == 0) {
137 			next = siblings->items[1];
138 		} else if (index == siblings->length - 1) {
139 			// Convert edge to top/left
140 			next = con;
141 			con = siblings->items[index - 1];
142 			amount = -amount;
143 		} else {
144 			prev = siblings->items[index - 1];
145 			next = siblings->items[index + 1];
146 		}
147 	} else if (axis == WLR_EDGE_TOP || axis == WLR_EDGE_LEFT) {
148 		if (!sway_assert(index > 0, "Didn't expect first child")) {
149 			return;
150 		}
151 		next = con;
152 		con = siblings->items[index - 1];
153 		amount = -amount;
154 	} else {
155 		if (!sway_assert(index < siblings->length - 1,
156 					"Didn't expect last child")) {
157 			return;
158 		}
159 		next = siblings->items[index + 1];
160 	}
161 
162 	// Apply new dimensions
163 	int sibling_amount = prev ? ceil((double)amount / 2.0) : amount;
164 
165 	if (is_horizontal(axis)) {
166 		if (con->width + amount < MIN_SANE_W) {
167 			return;
168 		}
169 		if (next->width - sibling_amount < MIN_SANE_W) {
170 			return;
171 		}
172 		if (prev && prev->width - sibling_amount < MIN_SANE_W) {
173 			return;
174 		}
175 		if (con->child_total_width <= 0) {
176 			return;
177 		}
178 
179 		// We're going to resize so snap all the width fractions to full pixels
180 		// to avoid rounding issues
181 		list_t *siblings = container_get_siblings(con);
182 		for (int i = 0; i < siblings->length; ++i) {
183 			struct sway_container *con = siblings->items[i];
184 			con->width_fraction = con->width / con->child_total_width;
185 		}
186 
187 		double amount_fraction = (double)amount / con->child_total_width;
188 		double sibling_amount_fraction =
189 			prev ? amount_fraction / 2.0 : amount_fraction;
190 
191 		con->width_fraction += amount_fraction;
192 		next->width_fraction -= sibling_amount_fraction;
193 		if (prev) {
194 			prev->width_fraction -= sibling_amount_fraction;
195 		}
196 	} else {
197 		if (con->height + amount < MIN_SANE_H) {
198 			return;
199 		}
200 		if (next->height - sibling_amount < MIN_SANE_H) {
201 			return;
202 		}
203 		if (prev && prev->height - sibling_amount < MIN_SANE_H) {
204 			return;
205 		}
206 		if (con->child_total_height <= 0) {
207 			return;
208 		}
209 
210 		// We're going to resize so snap all the height fractions to full pixels
211 		// to avoid rounding issues
212 		list_t *siblings = container_get_siblings(con);
213 		for (int i = 0; i < siblings->length; ++i) {
214 			struct sway_container *con = siblings->items[i];
215 			con->height_fraction = con->height / con->child_total_height;
216 		}
217 
218 		double amount_fraction = (double)amount / con->child_total_height;
219 		double sibling_amount_fraction =
220 			prev ? amount_fraction / 2.0 : amount_fraction;
221 
222 		con->height_fraction += amount_fraction;
223 		next->height_fraction -= sibling_amount_fraction;
224 		if (prev) {
225 			prev->height_fraction -= sibling_amount_fraction;
226 		}
227 	}
228 
229 	if (con->parent) {
230 		arrange_container(con->parent);
231 	} else {
232 		arrange_workspace(con->workspace);
233 	}
234 }
235 
236 /**
237  * Implement `resize <grow|shrink>` for a floating container.
238  */
resize_adjust_floating(uint32_t axis,struct resize_amount * amount)239 static struct cmd_results *resize_adjust_floating(uint32_t axis,
240 		struct resize_amount *amount) {
241 	struct sway_container *con = config->handler_context.container;
242 	int grow_width = 0, grow_height = 0;
243 
244 	if (is_horizontal(axis)) {
245 		grow_width = amount->amount;
246 	} else {
247 		grow_height = amount->amount;
248 	}
249 
250 	// Make sure we're not adjusting beyond floating min/max size
251 	int min_width, max_width, min_height, max_height;
252 	floating_calculate_constraints(&min_width, &max_width,
253 			&min_height, &max_height);
254 	if (con->width + grow_width < min_width) {
255 		grow_width = min_width - con->width;
256 	} else if (con->width + grow_width > max_width) {
257 		grow_width = max_width - con->width;
258 	}
259 	if (con->height + grow_height < min_height) {
260 		grow_height = min_height - con->height;
261 	} else if (con->height + grow_height > max_height) {
262 		grow_height = max_height - con->height;
263 	}
264 	int grow_x = 0, grow_y = 0;
265 
266 	if (axis == AXIS_HORIZONTAL) {
267 		grow_x = -grow_width / 2;
268 	} else if (axis == AXIS_VERTICAL) {
269 		grow_y = -grow_height / 2;
270 	} else if (axis == WLR_EDGE_TOP) {
271 		grow_y = -grow_height;
272 	} else if (axis == WLR_EDGE_LEFT) {
273 		grow_x = -grow_width;
274 	}
275 	if (grow_x == 0 && grow_y == 0) {
276 		return cmd_results_new(CMD_INVALID, "Cannot resize any further");
277 	}
278 	con->x += grow_x;
279 	con->y += grow_y;
280 	con->width += grow_width;
281 	con->height += grow_height;
282 
283 	con->content_x += grow_x;
284 	con->content_y += grow_y;
285 	con->content_width += grow_width;
286 	con->content_height += grow_height;
287 
288 	arrange_container(con);
289 
290 	return cmd_results_new(CMD_SUCCESS, NULL);
291 }
292 
293 /**
294  * Implement `resize <grow|shrink>` for a tiled container.
295  */
resize_adjust_tiled(uint32_t axis,struct resize_amount * amount)296 static struct cmd_results *resize_adjust_tiled(uint32_t axis,
297 		struct resize_amount *amount) {
298 	struct sway_container *current = config->handler_context.container;
299 
300 	if (amount->unit == RESIZE_UNIT_DEFAULT) {
301 		amount->unit = RESIZE_UNIT_PPT;
302 	}
303 	if (amount->unit == RESIZE_UNIT_PPT) {
304 		float pct = amount->amount / 100.0f;
305 
306 		if (is_horizontal(axis)) {
307 			amount->amount = (float)current->width * pct;
308 		} else {
309 			amount->amount = (float)current->height * pct;
310 		}
311 	}
312 
313 	double old_width = current->width_fraction;
314 	double old_height = current->height_fraction;
315 	container_resize_tiled(current, axis, amount->amount);
316 	if (current->width_fraction == old_width &&
317 			current->height_fraction == old_height) {
318 		return cmd_results_new(CMD_INVALID, "Cannot resize any further");
319 	}
320 	return cmd_results_new(CMD_SUCCESS, NULL);
321 }
322 
323 /**
324  * Implement `resize set` for a tiled container.
325  */
resize_set_tiled(struct sway_container * con,struct resize_amount * width,struct resize_amount * height)326 static struct cmd_results *resize_set_tiled(struct sway_container *con,
327 		struct resize_amount *width, struct resize_amount *height) {
328 	if (width->amount) {
329 		if (width->unit == RESIZE_UNIT_PPT ||
330 				width->unit == RESIZE_UNIT_DEFAULT) {
331 			// Convert to px
332 			struct sway_container *parent = con->parent;
333 			while (parent && parent->layout != L_HORIZ) {
334 				parent = parent->parent;
335 			}
336 			if (parent) {
337 				width->amount = parent->width * width->amount / 100;
338 			} else {
339 				width->amount = con->workspace->width * width->amount / 100;
340 			}
341 			width->unit = RESIZE_UNIT_PX;
342 		}
343 		if (width->unit == RESIZE_UNIT_PX) {
344 			container_resize_tiled(con, AXIS_HORIZONTAL,
345 					width->amount - con->width);
346 		}
347 	}
348 
349 	if (height->amount) {
350 		if (height->unit == RESIZE_UNIT_PPT ||
351 				height->unit == RESIZE_UNIT_DEFAULT) {
352 			// Convert to px
353 			struct sway_container *parent = con->parent;
354 			while (parent && parent->layout != L_VERT) {
355 				parent = parent->parent;
356 			}
357 			if (parent) {
358 				height->amount = parent->height * height->amount / 100;
359 			} else {
360 				height->amount = con->workspace->height * height->amount / 100;
361 			}
362 			height->unit = RESIZE_UNIT_PX;
363 		}
364 		if (height->unit == RESIZE_UNIT_PX) {
365 			container_resize_tiled(con, AXIS_VERTICAL,
366 					height->amount - con->height);
367 		}
368 	}
369 
370 	return cmd_results_new(CMD_SUCCESS, NULL);
371 }
372 
373 /**
374  * Implement `resize set` for a floating container.
375  */
resize_set_floating(struct sway_container * con,struct resize_amount * width,struct resize_amount * height)376 static struct cmd_results *resize_set_floating(struct sway_container *con,
377 		struct resize_amount *width, struct resize_amount *height) {
378 	int min_width, max_width, min_height, max_height, grow_width = 0, grow_height = 0;
379 	floating_calculate_constraints(&min_width, &max_width,
380 			&min_height, &max_height);
381 
382 	if (width->amount) {
383 		switch (width->unit) {
384 		case RESIZE_UNIT_PPT:
385 			if (container_is_scratchpad_hidden(con)) {
386 				return cmd_results_new(CMD_FAILURE,
387 						"Cannot resize a hidden scratchpad container by ppt");
388 			}
389 			// Convert to px
390 			width->amount = con->workspace->width * width->amount / 100;
391 			width->unit = RESIZE_UNIT_PX;
392 			// Falls through
393 		case RESIZE_UNIT_PX:
394 		case RESIZE_UNIT_DEFAULT:
395 			width->amount = fmax(min_width, fmin(width->amount, max_width));
396 			grow_width = width->amount - con->width;
397 			con->x -= grow_width / 2;
398 			con->width = width->amount;
399 			break;
400 		case RESIZE_UNIT_INVALID:
401 			sway_assert(false, "invalid width unit");
402 			break;
403 		}
404 	}
405 
406 	if (height->amount) {
407 		switch (height->unit) {
408 		case RESIZE_UNIT_PPT:
409 			if (container_is_scratchpad_hidden(con)) {
410 				return cmd_results_new(CMD_FAILURE,
411 						"Cannot resize a hidden scratchpad container by ppt");
412 			}
413 			// Convert to px
414 			height->amount = con->workspace->height * height->amount / 100;
415 			height->unit = RESIZE_UNIT_PX;
416 			// Falls through
417 		case RESIZE_UNIT_PX:
418 		case RESIZE_UNIT_DEFAULT:
419 			height->amount = fmax(min_height, fmin(height->amount, max_height));
420 			grow_height = height->amount - con->height;
421 			con->y -= grow_height / 2;
422 			con->height = height->amount;
423 			break;
424 		case RESIZE_UNIT_INVALID:
425 			sway_assert(false, "invalid height unit");
426 			break;
427 		}
428 	}
429 
430 	con->content_x -= grow_width / 2;
431 	con->content_y -= grow_height / 2;
432 	con->content_width += grow_width;
433 	con->content_height += grow_height;
434 
435 	arrange_container(con);
436 
437 	return cmd_results_new(CMD_SUCCESS, NULL);
438 }
439 
440 /**
441  * resize set <args>
442  *
443  * args: [width] <width> [px|ppt]
444  *     : height <height> [px|ppt]
445  *     : [width] <width> [px|ppt] [height] <height> [px|ppt]
446  */
cmd_resize_set(int argc,char ** argv)447 static struct cmd_results *cmd_resize_set(int argc, char **argv) {
448 	struct cmd_results *error;
449 	if ((error = checkarg(argc, "resize", EXPECTED_AT_LEAST, 1))) {
450 		return error;
451 	}
452 	const char usage[] = "Expected 'resize set [width] <width> [px|ppt]' or "
453 		"'resize set height <height> [px|ppt]' or "
454 		"'resize set [width] <width> [px|ppt] [height] <height> [px|ppt]'";
455 
456 	// Width
457 	struct resize_amount width = {0};
458 	if (argc >= 2 && !strcmp(argv[0], "width") && strcmp(argv[1], "height")) {
459 		argc--; argv++;
460 	}
461 	if (strcmp(argv[0], "height")) {
462 		int num_consumed_args = parse_resize_amount(argc, argv, &width);
463 		argc -= num_consumed_args;
464 		argv += num_consumed_args;
465 		if (width.unit == RESIZE_UNIT_INVALID) {
466 			return cmd_results_new(CMD_INVALID, usage);
467 		}
468 	}
469 
470 	// Height
471 	struct resize_amount height = {0};
472 	if (argc) {
473 		if (argc >= 2 && !strcmp(argv[0], "height")) {
474 			argc--; argv++;
475 		}
476 		int num_consumed_args = parse_resize_amount(argc, argv, &height);
477 		if (argc > num_consumed_args) {
478 			return cmd_results_new(CMD_INVALID, usage);
479 		}
480 		if (width.unit == RESIZE_UNIT_INVALID) {
481 			return cmd_results_new(CMD_INVALID, usage);
482 		}
483 	}
484 
485 	// If 0, don't resize that dimension
486 	struct sway_container *con = config->handler_context.container;
487 	if (width.amount <= 0) {
488 		width.amount = con->width;
489 	}
490 	if (height.amount <= 0) {
491 		height.amount = con->height;
492 	}
493 
494 	if (container_is_floating(con)) {
495 		return resize_set_floating(con, &width, &height);
496 	}
497 	return resize_set_tiled(con, &width, &height);
498 }
499 
500 /**
501  * resize <grow|shrink> <args>
502  *
503  * args: <direction>
504  * args: <direction> <amount> <unit>
505  * args: <direction> <amount> <unit> or <amount> <other_unit>
506  */
cmd_resize_adjust(int argc,char ** argv,int multiplier)507 static struct cmd_results *cmd_resize_adjust(int argc, char **argv,
508 		int multiplier) {
509 	const char usage[] = "Expected 'resize grow|shrink <direction> "
510 		"[<amount> px|ppt [or <amount> px|ppt]]'";
511 	uint32_t axis = parse_resize_axis(*argv);
512 	if (axis == WLR_EDGE_NONE) {
513 		return cmd_results_new(CMD_INVALID, usage);
514 	}
515 	--argc; ++argv;
516 
517 	// First amount
518 	struct resize_amount first_amount;
519 	if (argc) {
520 		int num_consumed_args = parse_resize_amount(argc, argv, &first_amount);
521 		argc -= num_consumed_args;
522 		argv += num_consumed_args;
523 		if (first_amount.unit == RESIZE_UNIT_INVALID) {
524 			return cmd_results_new(CMD_INVALID, usage);
525 		}
526 	} else {
527 		first_amount.amount = 10;
528 		first_amount.unit = RESIZE_UNIT_DEFAULT;
529 	}
530 
531 	// "or"
532 	if (argc) {
533 		if (strcmp(*argv, "or") != 0) {
534 			return cmd_results_new(CMD_INVALID, usage);
535 		}
536 		--argc; ++argv;
537 	}
538 
539 	// Second amount
540 	struct resize_amount second_amount;
541 	if (argc) {
542 		int num_consumed_args = parse_resize_amount(argc, argv, &second_amount);
543 		if (argc > num_consumed_args) {
544 			return cmd_results_new(CMD_INVALID, usage);
545 		}
546 		if (second_amount.unit == RESIZE_UNIT_INVALID) {
547 			return cmd_results_new(CMD_INVALID, usage);
548 		}
549 	} else {
550 		second_amount.amount = 0;
551 		second_amount.unit = RESIZE_UNIT_INVALID;
552 	}
553 
554 	first_amount.amount *= multiplier;
555 	second_amount.amount *= multiplier;
556 
557 	struct sway_container *con = config->handler_context.container;
558 	if (container_is_floating(con)) {
559 		// Floating containers can only resize in px. Choose an amount which
560 		// uses px, with fallback to an amount that specified no unit.
561 		if (first_amount.unit == RESIZE_UNIT_PX) {
562 			return resize_adjust_floating(axis, &first_amount);
563 		} else if (second_amount.unit == RESIZE_UNIT_PX) {
564 			return resize_adjust_floating(axis, &second_amount);
565 		} else if (first_amount.unit == RESIZE_UNIT_DEFAULT) {
566 			return resize_adjust_floating(axis, &first_amount);
567 		} else if (second_amount.unit == RESIZE_UNIT_DEFAULT) {
568 			return resize_adjust_floating(axis, &second_amount);
569 		} else {
570 			return cmd_results_new(CMD_INVALID,
571 					"Floating containers cannot use ppt measurements");
572 		}
573 	}
574 
575 	// For tiling, prefer ppt -> default -> px
576 	if (first_amount.unit == RESIZE_UNIT_PPT) {
577 		return resize_adjust_tiled(axis, &first_amount);
578 	} else if (second_amount.unit == RESIZE_UNIT_PPT) {
579 		return resize_adjust_tiled(axis, &second_amount);
580 	} else if (first_amount.unit == RESIZE_UNIT_DEFAULT) {
581 		return resize_adjust_tiled(axis, &first_amount);
582 	} else if (second_amount.unit == RESIZE_UNIT_DEFAULT) {
583 		return resize_adjust_tiled(axis, &second_amount);
584 	} else {
585 		return resize_adjust_tiled(axis, &first_amount);
586 	}
587 }
588 
cmd_resize(int argc,char ** argv)589 struct cmd_results *cmd_resize(int argc, char **argv) {
590 	if (!root->outputs->length) {
591 		return cmd_results_new(CMD_INVALID,
592 				"Can't run this command while there's no outputs connected.");
593 	}
594 	struct sway_container *current = config->handler_context.container;
595 	if (!current) {
596 		return cmd_results_new(CMD_INVALID, "Cannot resize nothing");
597 	}
598 
599 	struct cmd_results *error;
600 	if ((error = checkarg(argc, "resize", EXPECTED_AT_LEAST, 2))) {
601 		return error;
602 	}
603 
604 	if (strcasecmp(argv[0], "set") == 0) {
605 		return cmd_resize_set(argc - 1, &argv[1]);
606 	}
607 	if (strcasecmp(argv[0], "grow") == 0) {
608 		return cmd_resize_adjust(argc - 1, &argv[1], 1);
609 	}
610 	if (strcasecmp(argv[0], "shrink") == 0) {
611 		return cmd_resize_adjust(argc - 1, &argv[1], -1);
612 	}
613 
614 	const char usage[] = "Expected 'resize <shrink|grow> "
615 		"<width|height|up|down|left|right> [<amount>] [px|ppt]'";
616 
617 	return cmd_results_new(CMD_INVALID, usage);
618 }
619