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