swayfx-mosaic/patches/0001-mosaic-layout-viewport.patch

1130 lines
42 KiB
Diff

diff --git a/include/sway/tree/container.h b/include/sway/tree/container.h
index 5a36df85160c0cd2392efe62abeb93666cfaa926..cefbcac534450b3857319d2640191b03e06ac4f7 100644
--- a/include/sway/tree/container.h
+++ b/include/sway/tree/container.h
@@ -18,6 +18,7 @@ enum sway_container_layout {
L_VERT,
L_STACKED,
L_TABBED,
+ L_MOSAIC,
};
enum sway_container_border {
diff --git a/include/sway/tree/mosaic.h b/include/sway/tree/mosaic.h
new file mode 100644
index 0000000000000000000000000000000000000000..fe24490141d83bca8a245d6871aa0c65497af189
--- /dev/null
+++ b/include/sway/tree/mosaic.h
@@ -0,0 +1,57 @@
+#ifndef _SWAY_MOSAIC_H
+#define _SWAY_MOSAIC_H
+
+#include <stdbool.h>
+#include "list.h"
+
+struct sway_container;
+
+// CONTRACT: children are ordered Sway tiling containers; scroll_x is a
+// logical canvas coordinate, independent of focus and rendered geometry.
+struct mosaic_row_state {
+ double scroll_x;
+ // STEP20B temporary sizing policy. Zero selects the current 640 px
+ // minimum; this is not a stable Mosaic layout API.
+ int minimum_cell_width;
+ struct sway_container *focused;
+};
+
+struct mosaic_row {
+ list_t *children;
+ bool owns_children;
+ struct mosaic_row_state state;
+};
+
+struct mosaic_workspace_state {
+ list_t *rows; // struct mosaic_row; owns rows, never their containers.
+ int active_row;
+};
+
+// The first row may borrow workspace->tiling. The caller keeps that list alive
+// until mosaic_state_destroy; later rows own only their lists, not children.
+// NULL input/allocation failure returns NULL. Destroy accepts NULL.
+struct mosaic_workspace_state *mosaic_state_create(list_t *first_children);
+void mosaic_state_destroy(struct mosaic_workspace_state *state);
+// Model operation only: wiring another row into Sway's tree and scene is later
+// work. NULL/allocation failure returns NULL; child ownership stays with Sway.
+struct mosaic_row *mosaic_state_add_row(struct mosaic_workspace_state *state);
+// Rejects an invalid row index without changing active_row.
+bool mosaic_state_select_row(struct mosaic_workspace_state *state, int index);
+// Insertion/removal must occur from a Sway tree mutation path, which also
+// maintains parent/workspace pointers, representation and transaction dirtiness.
+int mosaic_row_insertion_index(const struct mosaic_row *row);
+void mosaic_row_insert_after_focus(struct mosaic_row *row,
+ struct sway_container *child);
+void mosaic_row_remove(struct mosaic_row *row, struct sway_container *child);
+// Rejects a child not present in this row, preserving the last valid focus.
+bool mosaic_row_focus(struct mosaic_row *row, struct sway_container *child);
+// Rejects negative/NaN/infinite logical positions without changing the row.
+bool mosaic_row_set_scroll(struct mosaic_row *row, double scroll_x);
+// Clamp the logical camera to the currently arranged canvas. The viewport and
+// row contents remain owned by Sway; this helper never mutates child order.
+int mosaic_row_max_scroll(const struct mosaic_row *row, int viewport_width,
+ int gaps);
+void mosaic_row_clamp_scroll(struct mosaic_row *row, int viewport_width,
+ int gaps);
+
+#endif
diff --git a/include/sway/tree/workspace.h b/include/sway/tree/workspace.h
index a7da62204636c268503b77305576799c0c63716b..955214ba9128e4c22afd433d09ba5cb235ff0834 100644
--- a/include/sway/tree/workspace.h
+++ b/include/sway/tree/workspace.h
@@ -10,6 +10,7 @@
#include "sway/tree/node.h"
struct sway_view;
+struct mosaic_workspace_state;
struct sway_workspace_state {
struct sway_container *fullscreen;
@@ -41,6 +42,9 @@ struct sway_workspace {
int width, height;
enum sway_container_layout layout;
enum sway_container_layout prev_split_layout;
+ // CONTRACT: workspace-local target restored by `layout toggle mosaic`.
+ // This is distinct from Sway's prev_split_layout toggle history.
+ enum sway_container_layout mosaic_previous_standard_layout;
struct side_gaps current_gaps;
int gaps_inner;
@@ -49,6 +53,8 @@ struct sway_workspace {
struct sway_output *output; // NULL if no outputs are connected
list_t *floating; // struct sway_container
list_t *tiling; // struct sway_container
+ // Mosaic row zero borrows tiling: Sway's tree remains the ordering authority.
+ struct mosaic_workspace_state *mosaic;
list_t *output_priority;
bool urgent;
@@ -130,6 +136,10 @@ void workspace_detach(struct sway_workspace *workspace);
struct sway_container *workspace_add_tiling(struct sway_workspace *workspace,
struct sway_container *con);
+// Reconcile the active Mosaic row after Sway reattaches a focused container.
+// No-op outside Mosaic or when the active seat has no tiled focus in this row.
+void workspace_mosaic_sync_focus(struct sway_workspace *workspace);
+
void workspace_add_floating(struct sway_workspace *workspace,
struct sway_container *con);
diff --git a/meson.build b/meson.build
index dad186a52e40ac41a22dbe48134bdf7a3033fb69..adeb50b10949831cf32feb411d1088aea2011e59 100644
--- a/meson.build
+++ b/meson.build
@@ -123,8 +123,9 @@ foreach sym : ['LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM', 'LIBINPUT_CONFIG_DRAG_LOC
conf_data.set10('HAVE_' + sym, cc.has_header_symbol('libinput.h', sym, dependencies: libinput))
endforeach
-scdoc = dependency('scdoc', version: '>=1.9.2', native: true, required: get_option('man-pages'))
-if scdoc.found()
+scdoc = dependency('scdoc', version: '>=1.9.2', native: true,
+ required: get_option('mosaic-package') ? false : get_option('man-pages'))
+if scdoc.found() and not get_option('mosaic-package')
scdoc_prog = find_program(scdoc.get_variable(pkgconfig: 'scdoc'), native: true)
mandir = get_option('mandir')
man_files = [
@@ -205,15 +206,23 @@ subdir('include')
subdir('protocols')
subdir('common')
subdir('sway')
-subdir('swaymsg')
+model_test = executable('mosaic-model-test',
+ ['tests/mosaic-model.c', 'sway/tree/mosaic.c'],
+ include_directories: sway_inc,
+ link_with: lib_sway_common,
+ dependencies: [cairo, pango, pangocairo, wayland_client])
+test('mosaic-model', model_test)
+if not get_option('mosaic-package')
+ subdir('swaymsg')
+endif
-if get_option('swaybar') or get_option('swaynag')
+if not get_option('mosaic-package') and (get_option('swaybar') or get_option('swaynag'))
subdir('client')
endif
-if get_option('swaybar')
+if not get_option('mosaic-package') and get_option('swaybar')
subdir('swaybar')
endif
-if get_option('swaynag')
+if not get_option('mosaic-package') and get_option('swaynag')
subdir('swaynag')
endif
@@ -222,19 +231,18 @@ config.set('datadir', join_paths(prefix, datadir))
config.set('prefix', prefix)
config.set('sysconfdir', join_paths(prefix, sysconfdir))
-configure_file(
- configuration: config,
- input: 'config.in',
- output: '@BASENAME@',
- install_dir: join_paths(sysconfdir, 'sway')
-)
-
-install_data(
- 'sway.desktop',
- install_dir: join_paths(datadir, 'wayland-sessions')
-)
+if not get_option('mosaic-package')
+ configure_file(
+ configuration: config,
+ input: 'config.in',
+ output: '@BASENAME@',
+ install_dir: join_paths(sysconfdir, 'sway')
+ )
+ install_data('sway.desktop',
+ install_dir: join_paths(datadir, 'wayland-sessions'))
+endif
-if get_option('default-wallpaper')
+if get_option('default-wallpaper') and not get_option('mosaic-package')
wallpaper_files = files(
'assets/Sway_Wallpaper_Blue_768x1024.png',
'assets/Sway_Wallpaper_Blue_768x1024_Portrait.png',
@@ -250,10 +258,12 @@ if get_option('default-wallpaper')
install_data(wallpaper_files, install_dir: wallpaper_install_dir)
endif
-subdir('completions')
+if not get_option('mosaic-package')
+ subdir('completions')
+endif
summary({
'gdk-pixbuf': gdk_pixbuf.found(),
- 'tray': have_tray,
- 'man-pages': scdoc.found(),
+ 'tray': have_tray and not get_option('mosaic-package') and get_option('swaybar'),
+ 'man-pages': scdoc.found() and not get_option('mosaic-package'),
}, bool_yn: true)
diff --git a/meson_options.txt b/meson_options.txt
index 506ecc9a69018cd13d6be40d81167a2fc1aab232..351f859a0d94ec6b212b7aff86dbeba4b90615da 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,4 +1,5 @@
option('default-wallpaper', type: 'boolean', value: true, description: 'Install the default wallpaper.')
+option('mosaic-package', type: 'boolean', value: true, description: 'Install only the coexistence-safe swayfx-mosaic compositor binary.')
option('zsh-completions', type: 'boolean', value: true, description: 'Install zsh shell completions.')
option('bash-completions', type: 'boolean', value: true, description: 'Install bash shell completions.')
option('fish-completions', type: 'boolean', value: true, description: 'Install fish shell completions.')
diff --git a/sway/commands/focus.c b/sway/commands/focus.c
index facd82de77443839cb9c0e529383b5bfc51faf5d..e3aa1f3ac29d49ec89aa92bf545eeb831c078dd7 100644
--- a/sway/commands/focus.c
+++ b/sway/commands/focus.c
@@ -24,6 +24,7 @@ static bool get_direction_from_next_prev(struct sway_container *container,
if (strcasecmp(name, "prev") == 0) {
switch (parent_layout) {
case L_HORIZ:
+ case L_MOSAIC:
case L_TABBED:
*out = WLR_DIRECTION_LEFT;
break;
@@ -39,6 +40,7 @@ static bool get_direction_from_next_prev(struct sway_container *container,
} else if (strcasecmp(name, "next") == 0) {
switch (parent_layout) {
case L_HORIZ:
+ case L_MOSAIC:
case L_TABBED:
*out = WLR_DIRECTION_RIGHT;
break;
@@ -93,7 +95,8 @@ static struct sway_node *get_node_in_output_direction(
if (ws->tiling->length > 0) {
switch (dir) {
case WLR_DIRECTION_LEFT:
- if (ws->layout == L_HORIZ || ws->layout == L_TABBED) {
+ if (ws->layout == L_HORIZ || ws->layout == L_MOSAIC ||
+ ws->layout == L_TABBED) {
// get most right child of new output
container = ws->tiling->items[ws->tiling->length-1];
} else {
@@ -101,7 +104,8 @@ static struct sway_node *get_node_in_output_direction(
}
break;
case WLR_DIRECTION_RIGHT:
- if (ws->layout == L_HORIZ || ws->layout == L_TABBED) {
+ if (ws->layout == L_HORIZ || ws->layout == L_MOSAIC ||
+ ws->layout == L_TABBED) {
// get most left child of new output
container = ws->tiling->items[0];
} else {
@@ -163,7 +167,8 @@ static struct sway_node *node_get_in_direction_tiling(
list_t *siblings = container_get_siblings(current);
if (dir == WLR_DIRECTION_LEFT || dir == WLR_DIRECTION_RIGHT) {
- if (parent_layout == L_HORIZ || parent_layout == L_TABBED) {
+ if (parent_layout == L_HORIZ || parent_layout == L_MOSAIC ||
+ parent_layout == L_TABBED) {
can_move = true;
desired = idx + (dir == WLR_DIRECTION_LEFT ? -1 : 1);
}
diff --git a/sway/commands/layout.c b/sway/commands/layout.c
index 22dfdf3d7cec5a18d6b861d99d5cb0c3ce238e00..0d48ee81b70afebef164c90a1a3b62e1f91da972 100644
--- a/sway/commands/layout.c
+++ b/sway/commands/layout.c
@@ -1,10 +1,14 @@
#include <stdbool.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "sway/commands.h"
#include "sway/output.h"
#include "sway/tree/arrange.h"
#include "sway/tree/container.h"
+#include "sway/tree/mosaic.h"
#include "sway/tree/workspace.h"
#include "log.h"
@@ -22,7 +26,9 @@ static enum sway_container_layout parse_layout_string(char *s) {
}
static const char expected_syntax[] =
- "Expected 'layout default|tabbed|stacking|splitv|splith' or "
+ "Expected 'layout default|tabbed|stacking|splitv|splith|mosaic' or "
+ "'layout toggle mosaic' or "
+ "'layout mosaic scroll <signed pixels>' or "
"'layout toggle [split|all]' or "
"'layout toggle [split|tabbed|stacking|splitv|splith] [split|tabbed|stacking|splitv|splith]...'";
@@ -97,6 +103,10 @@ static enum sway_container_layout get_layout(int argc, char **argv,
enum sway_container_layout layout,
enum sway_container_layout prev_split_layout,
struct sway_output *output) {
+ // Mosaic is an explicit workspace mode, never part of the legacy toggle cycle.
+ if (argc == 1 && strcasecmp(argv[0], "mosaic") == 0) {
+ return L_MOSAIC;
+ }
// Check if assigned directly
enum sway_container_layout parsed = parse_layout_string(argv[0]);
if (parsed != L_NONE) {
@@ -125,6 +135,37 @@ struct cmd_results *cmd_layout(int argc, char **argv) {
}
struct sway_container *container = config->handler_context.container;
struct sway_workspace *workspace = config->handler_context.workspace;
+ if (argc == 3 && strcasecmp(argv[0], "mosaic") == 0 &&
+ strcasecmp(argv[1], "scroll") == 0) {
+ if (!workspace || workspace->layout != L_MOSAIC || !workspace->mosaic) {
+ return cmd_results_new(CMD_FAILURE, "Mosaic est inactif sur ce workspace");
+ }
+ errno = 0;
+ char *end = NULL;
+ long delta = strtol(argv[2], &end, 10);
+ if (errno || end == argv[2] || *end || delta < -INT_MAX ||
+ delta > INT_MAX) {
+ return cmd_results_new(CMD_INVALID, "%s", expected_syntax);
+ }
+ struct mosaic_row *row = workspace->mosaic->rows->items[0];
+ double next = row->state.scroll_x + delta;
+ mosaic_row_set_scroll(row, next < 0 ? 0 : next);
+ struct wlr_box viewport;
+ workspace_get_box(workspace, &viewport);
+ mosaic_row_clamp_scroll(row, viewport.width, workspace->gaps_inner);
+ // The transaction updates the workspace tiling scene position; the
+ // ordered Sway children and their logical rectangles stay intact.
+ arrange_workspace(workspace);
+ return cmd_results_new(CMD_SUCCESS, NULL);
+ }
+ bool mosaic_command = argc == 1 && strcasecmp(argv[0], "mosaic") == 0;
+ bool mosaic_toggle = argc == 2 && strcasecmp(argv[0], "toggle") == 0 &&
+ strcasecmp(argv[1], "mosaic") == 0;
+ // CONTRACT: Mosaic belongs to the workspace, even when a view has focus.
+ if (mosaic_command || mosaic_toggle ||
+ (workspace && workspace->layout == L_MOSAIC)) {
+ container = NULL;
+ }
if (container && container_is_floating(container)) {
return cmd_results_new(CMD_FAILURE,
@@ -161,28 +202,74 @@ struct cmd_results *cmd_layout(int argc, char **argv) {
container->pending.workspace->output);
} else {
old_layout = workspace->layout;
- new_layout = get_layout(argc, argv,
- workspace->layout, workspace->prev_split_layout,
- workspace->output);
+ if (mosaic_toggle) {
+ new_layout = old_layout == L_MOSAIC ?
+ workspace->mosaic_previous_standard_layout : L_MOSAIC;
+ } else {
+ new_layout = get_layout(argc, argv,
+ workspace->layout, workspace->prev_split_layout,
+ workspace->output);
+ }
}
if (new_layout == L_NONE) {
return cmd_results_new(CMD_INVALID, "%s", expected_syntax);
}
+ if (new_layout == L_MOSAIC && old_layout != L_MOSAIC &&
+ workspace->mosaic_previous_standard_layout == L_NONE) {
+ // A defensive fallback for workspaces created before layout state init.
+ workspace->mosaic_previous_standard_layout = old_layout;
+ }
+ if (new_layout == L_MOSAIC && !workspace->mosaic) {
+ workspace->mosaic = mosaic_state_create(workspace->tiling);
+ if (!workspace->mosaic) {
+ return cmd_results_new(CMD_FAILURE,
+ "Impossible d'allouer l'état Mosaic");
+ }
+ }
+ if (new_layout == L_MOSAIC && old_layout != L_MOSAIC &&
+ workspace->tiling->length == 1) {
+ struct sway_container *wrap = workspace->tiling->items[0];
+ // Sway applies `layout tabbed/stacking` on an occupied workspace by
+ // wrapping its children. Promote that root wrapper's tabs to Row0 so
+ // Mosaic receives the same windows, then restore the remembered
+ // standard layout at the workspace level on the next toggle.
+ if (!wrap->view && wrap->pending.children->length > 0 &&
+ (wrap->pending.layout == L_TABBED ||
+ wrap->pending.layout == L_STACKED) &&
+ wrap->pending.layout == workspace->mosaic_previous_standard_layout) {
+ container_detach(wrap);
+ workspace_unwrap_children(workspace, wrap);
+ container_reap_empty(wrap);
+ }
+ }
+ if (new_layout == L_MOSAIC && config->handler_context.container) {
+ struct mosaic_row *row = workspace->mosaic->rows->items[0];
+ struct sway_container *focused = container_toplevel_ancestor(
+ config->handler_context.container);
+ mosaic_row_focus(row, focused);
+ }
if (new_layout != old_layout) {
+ // The remembered standard layout belongs to the workspace, regardless
+ // of whether i3 applies the command to a focused parent container.
+ if (new_layout != L_MOSAIC) {
+ workspace->mosaic_previous_standard_layout = new_layout;
+ }
if (container) {
if (old_layout != L_TABBED && old_layout != L_STACKED) {
container->prev_split_layout = old_layout;
}
container->pending.layout = new_layout;
container_update_representation(container);
- } else if (config->handler_context.container) {
+ } else if (config->handler_context.container &&
+ old_layout != L_MOSAIC && new_layout != L_MOSAIC) {
// i3 avoids changing workspace layouts with a new container
// https://github.com/i3/i3/blob/3cd1c45eba6de073bc4300eebb4e1cc1a0c4479a/src/con.c#L1817
container = workspace_wrap_children(workspace);
container->pending.layout = new_layout;
container_update_representation(container);
} else {
- if (old_layout != L_TABBED && old_layout != L_STACKED) {
+ if (old_layout != L_TABBED && old_layout != L_STACKED &&
+ old_layout != L_MOSAIC) {
workspace->prev_split_layout = old_layout;
}
workspace->layout = new_layout;
diff --git a/sway/commands/move.c b/sway/commands/move.c
index 43fce0d6db755ea3123b84567db19488bae84cfb..1ecca7846bfee7f355241c1b3a235489c80122d6 100644
--- a/sway/commands/move.c
+++ b/sway/commands/move.c
@@ -81,6 +81,8 @@ static bool is_parallel(enum sway_container_layout layout,
switch (layout) {
case L_TABBED:
case L_HORIZ:
+ case L_MOSAIC:
+ // Horizontal moves within Row0 must not reorient the workspace.
return dir == WLR_DIRECTION_LEFT || dir == WLR_DIRECTION_RIGHT;
case L_STACKED:
case L_VERT:
diff --git a/sway/desktop/transaction.c b/sway/desktop/transaction.c
index 3a37b40f6b0ad9f5281101e55c259de03eb80ec9..47e3e179b492e3c6d1ea1e2e10492a2da886c1b0 100644
--- a/sway/desktop/transaction.c
+++ b/sway/desktop/transaction.c
@@ -1,5 +1,7 @@
#include <assert.h>
#include <stdbool.h>
+#include <limits.h>
+#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
@@ -15,6 +17,7 @@
#include "sway/output.h"
#include "sway/server.h"
#include "sway/tree/container.h"
+#include "sway/tree/mosaic.h"
#include "sway/tree/node.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
@@ -504,8 +507,10 @@ static void arrange_children(enum sway_container_layout layout, list_t *children
disable_container(child);
}
}
- } else if (layout == L_HORIZ) {
- int off = 0;
+ } else if (layout == L_HORIZ || layout == L_MOSAIC) {
+ // Scene node positions are int; keep the logical canvas accumulator
+ // wide so a very large Mosaic row cannot overflow signed arithmetic.
+ int64_t off = 0;
for (int i = 0; i < children->length; i++) {
struct sway_container *child = children->items[i];
int cwidth = child->current.width;
@@ -515,9 +520,9 @@ static void arrange_children(enum sway_container_layout layout, list_t *children
wlr_scene_node_set_enabled(&child->shadow->node,
container_has_shadow(child) && child->view);
wlr_scene_node_reparent(&child->scene_tree->node, content);
- if (cwidth > 0 && height > 0) {
- arrange_container(child, cwidth, height, off, 0, true, gaps);
- off += cwidth + gaps;
+ if (cwidth > 0 && height > 0 && off <= INT_MAX) {
+ arrange_container(child, cwidth, height, (int)off, 0, true, gaps);
+ off += (int64_t)cwidth + gaps;
} else {
disable_container(child);
}
@@ -864,6 +869,18 @@ static void arrange_workspace_tiling(struct sway_workspace *ws,
width, height, ws->gaps_inner);
}
+static int workspace_tiling_scene_x(struct sway_workspace *ws,
+ struct wlr_box *area, struct side_gaps *gaps) {
+ int64_t x = (int64_t)area->x + gaps->left;
+ if (ws->current.layout == L_MOSAIC && ws->mosaic) {
+ struct mosaic_row *row = ws->mosaic->rows->items[0];
+ x -= (int64_t)row->state.scroll_x;
+ }
+ // The camera is kept in logical pixels; this conversion is the sole
+ // output-scene mapping and must never overflow wlroots' int positions.
+ return x < INT_MIN ? INT_MIN : x > INT_MAX ? INT_MAX : (int)x;
+}
+
static void disable_workspace(struct sway_workspace *ws) {
// if any containers were just moved to a disabled workspace it will
// have the parent of the old workspace. Move the workspace so that it won't
@@ -955,7 +972,8 @@ static void arrange_output(struct sway_output *output, int width, int height) {
struct side_gaps *gaps = &child->current_gaps;
wlr_scene_node_set_position(&child->layers.tiling->node,
- gaps->left + area->x, gaps->top + area->y);
+ workspace_tiling_scene_x(child, area, gaps),
+ gaps->top + area->y);
arrange_workspace_tiling(child,
area->width - gaps->left - gaps->right,
@@ -971,7 +989,8 @@ static void arrange_output(struct sway_output *output, int width, int height) {
wlr_scene_node_set_enabled(&child->layers.fullscreen->node, false);
wlr_scene_node_set_position(&child->layers.tiling->node,
- gaps->left + area->x, gaps->top + area->y);
+ workspace_tiling_scene_x(child, area, gaps),
+ gaps->top + area->y);
arrange_workspace_tiling(child,
area->width - gaps->left - gaps->right,
diff --git a/sway/input/seat.c b/sway/input/seat.c
index 0434d637c37930b171a3a508c41a3f991cea3c09..d11f6fedeb1b8cc4672c9a0b66ae91b7cfc7a31a 100644
--- a/sway/input/seat.c
+++ b/sway/input/seat.c
@@ -31,6 +31,7 @@
#include "sway/server.h"
#include "sway/tree/arrange.h"
#include "sway/tree/container.h"
+#include "sway/tree/mosaic.h"
#include "sway/tree/root.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
@@ -1189,6 +1190,28 @@ static void seat_set_workspace_focus(struct sway_seat *seat, struct sway_node *n
if (container) {
seat_set_raw_focus(seat, &container->node);
seat_send_focus(&container->node, seat);
+ // Focus changes update row context without changing Sway tiling order.
+ if (new_workspace && new_workspace->layout == L_MOSAIC &&
+ new_workspace->mosaic) {
+ struct mosaic_row *row = new_workspace->mosaic->rows->items[0];
+ struct sway_container *top = container_toplevel_ancestor(container);
+ if (mosaic_row_focus(row, top) && top) {
+ // Keep focus separate from order. Only move the logical camera
+ // when the focused tiled cell falls outside the viewport.
+ struct wlr_box viewport;
+ workspace_get_box(new_workspace, &viewport);
+ double left = top->pending.x - viewport.x;
+ double right = left + top->pending.width;
+ if (left < row->state.scroll_x) {
+ row->state.scroll_x = left < 0 ? 0 : left;
+ } else if (right > row->state.scroll_x + viewport.width) {
+ row->state.scroll_x = right - viewport.width;
+ }
+ mosaic_row_clamp_scroll(row, viewport.width,
+ new_workspace->gaps_inner);
+ arrange_workspace(new_workspace);
+ }
+ }
}
// emit ipc events
diff --git a/sway/ipc-json.c b/sway/ipc-json.c
index bb0566ab68d492f5f46324828a75425f7af8372d..525849848d3d23f338c96b5f96b2a17dd66ad229 100644
--- a/sway/ipc-json.c
+++ b/sway/ipc-json.c
@@ -16,6 +16,7 @@
#include "sway/scene_descriptor.h"
#include "sway/server.h"
#include "sway/tree/container.h"
+#include "sway/tree/mosaic.h"
#include "sway/tree/root.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
@@ -54,6 +55,8 @@ static const char *ipc_json_layout_description(enum sway_container_layout l) {
return "splitv";
case L_HORIZ:
return "splith";
+ case L_MOSAIC:
+ return "mosaic";
case L_TABBED:
return "tabbed";
case L_STACKED:
@@ -69,6 +72,7 @@ static const char *ipc_json_orientation_description(enum sway_container_layout l
case L_VERT:
return "vertical";
case L_HORIZ:
+ case L_MOSAIC:
return "horizontal";
default:
return "none";
@@ -611,6 +615,32 @@ static void ipc_json_describe_workspace(struct sway_workspace *workspace,
json_object_object_add(object, "orientation",
json_object_new_string(
ipc_json_orientation_description(workspace->layout)));
+ if (workspace->layout == L_MOSAIC && workspace->mosaic) {
+ // Optional extension: legacy IPC fields keep their Sway shapes.
+ // Row zero borrows the real workspace tiling order.
+ json_object *mosaic = json_object_new_object();
+ json_object *rows = json_object_new_array();
+ for (int i = 0; i < workspace->mosaic->rows->length; ++i) {
+ struct mosaic_row *row = workspace->mosaic->rows->items[i];
+ json_object *entry = json_object_new_object();
+ json_object *ids = json_object_new_array();
+ for (int j = 0; j < row->children->length; ++j) {
+ struct sway_container *child = row->children->items[j];
+ json_object_array_add(ids, json_object_new_int64(child->node.id));
+ }
+ json_object_object_add(entry, "children", ids);
+ json_object_object_add(entry, "scroll_x",
+ json_object_new_double(row->state.scroll_x));
+ json_object_object_add(entry, "focused",
+ row->state.focused ? json_object_new_int64(
+ row->state.focused->node.id) : NULL);
+ json_object_array_add(rows, entry);
+ }
+ json_object_object_add(mosaic, "rows", rows);
+ json_object_object_add(mosaic, "active_row",
+ json_object_new_int(workspace->mosaic->active_row));
+ json_object_object_add(object, "mosaic", mosaic);
+ }
// Floating
json_object *floating_array = json_object_new_array();
diff --git a/sway/main.c b/sway/main.c
index ab1d05e89bb7a882afe6f7ec95e2d4cfc9bad517..e7fc09b01fef1db7c5b03efa20d78cca7ea29f07 100644
--- a/sway/main.c
+++ b/sway/main.c
@@ -257,7 +257,7 @@ int main(int argc, char **argv) {
allow_unsupported_gpu = true;
break;
case 'v': // version
- printf("swayfx version " SWAY_VERSION " (based on sway " SWAY_ORIGINAL_VERSION ")\n");
+ printf("swayfx-mosaic version " SWAY_VERSION " (based on sway " SWAY_ORIGINAL_VERSION ")\n");
exit(EXIT_SUCCESS);
break;
case 'V': // verbose
diff --git a/sway/meson.build b/sway/meson.build
index ecd2865be64ac90dc8651c881c6ddb3cca33d931..41119f20f0fd29a0f49f6d2f405ccfd5f7c4688b 100644
--- a/sway/meson.build
+++ b/sway/meson.build
@@ -236,6 +236,7 @@ sway_sources = files(
'tree/arrange.c',
'tree/container.c',
+ 'tree/mosaic.c',
'tree/node.c',
'tree/root.c',
'tree/view.c',
@@ -272,7 +273,7 @@ if wlroots_features['libinput_backend']
endif
executable(
- 'sway',
+ 'swayfx-mosaic',
sway_sources + wl_protos_src,
include_directories: [sway_inc],
dependencies: sway_deps,
diff --git a/sway/tree/arrange.c b/sway/tree/arrange.c
index 3b85376c1e101bdb70837182852d56c94ed30125..4155e153668b199179800cff940276f04ba0dac1 100644
--- a/sway/tree/arrange.c
+++ b/sway/tree/arrange.c
@@ -1,11 +1,14 @@
#include <ctype.h>
#include <stdbool.h>
+#include <limits.h>
+#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <wlr/types/wlr_output.h>
#include <wlr/types/wlr_output_layout.h>
#include "sway/tree/arrange.h"
#include "sway/tree/container.h"
+#include "sway/tree/mosaic.h"
#include "sway/output.h"
#include "sway/tree/workspace.h"
#include "sway/tree/view.h"
@@ -97,6 +100,37 @@ static void apply_horiz_layout(list_t *children, struct wlr_box *parent) {
}
}
+static void apply_mosaic_layout(list_t *children, struct wlr_box *viewport,
+ struct sway_workspace *ws) {
+ if (!children->length || !ws->mosaic) {
+ return;
+ }
+ struct mosaic_row *row = ws->mosaic->rows->items[0];
+ int count = children->length;
+ int gap = ws->gaps_inner > 0 ? ws->gaps_inner : 0;
+ int64_t gap_total = (int64_t)(count - 1) * gap;
+ int64_t fitting = ((int64_t)viewport->width - gap_total) / count;
+ // STEP20B temporary sizing policy: 640 px minimum demonstrates an
+ // overflowing tiled row. A later slice will define the UX cell sizing.
+ int cell = row->state.minimum_cell_width > 0 ?
+ row->state.minimum_cell_width : 640;
+ if (fitting > cell) {
+ cell = fitting > INT_MAX ? INT_MAX : (int)fitting;
+ }
+ int64_t x = viewport->x;
+ for (int i = 0; i < count; ++i) {
+ struct sway_container *child = children->items[i];
+ child->pending.x = x;
+ child->pending.y = viewport->y;
+ child->pending.width = cell;
+ child->pending.height = viewport->height;
+ child->child_total_width = (int64_t)count * cell > INT_MAX ?
+ INT_MAX : count * cell;
+ x += (int64_t)cell + gap;
+ }
+ mosaic_row_clamp_scroll(row, viewport->width, gap);
+}
+
static void apply_vert_layout(list_t *children, struct wlr_box *parent) {
if (!children->length) {
return;
@@ -225,6 +259,19 @@ static void arrange_children(list_t *children,
case L_HORIZ:
apply_horiz_layout(children, parent);
break;
+ case L_MOSAIC:
+ // Only a workspace owns a Mosaic row. Nested containers retain Sway's
+ // normal tiling rules while row zero remains the real workspace tree.
+ if (children->length) {
+ struct sway_container *first = children->items[0];
+ struct sway_workspace *ws = first->pending.workspace;
+ if (ws && ws->tiling == children) {
+ apply_mosaic_layout(children, parent, ws);
+ } else {
+ apply_horiz_layout(children, parent);
+ }
+ }
+ break;
case L_VERT:
apply_vert_layout(children, parent);
break;
diff --git a/sway/tree/container.c b/sway/tree/container.c
index 6fe1206499abc428517f4bc0cfc33c25a1c0e0ad..e6cad85806b3a42a18f73c51044130fc94d4e90e 100644
--- a/sway/tree/container.c
+++ b/sway/tree/container.c
@@ -17,6 +17,7 @@
#include "sway/output.h"
#include "sway/server.h"
#include "sway/tree/arrange.h"
+#include "sway/tree/mosaic.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
#include "sway/xdg_decoration.h"
@@ -829,6 +830,9 @@ size_t container_build_representation(enum sway_container_layout layout,
case L_HORIZ:
lenient_strcat(buffer, "H[");
break;
+ case L_MOSAIC:
+ lenient_strcat(buffer, "M[");
+ break;
case L_TABBED:
lenient_strcat(buffer, "T[");
break;
@@ -1528,6 +1532,7 @@ void container_insert_child(struct sway_container *parent,
child->pending.parent = parent;
child->pending.workspace = parent->pending.workspace;
container_for_each_child(child, set_workspace, NULL);
+ workspace_mosaic_sync_focus(child->pending.workspace);
container_handle_fullscreen_reparent(child);
container_update_representation(parent);
}
@@ -1543,6 +1548,7 @@ void container_add_sibling(struct sway_container *fixed,
active->pending.parent = fixed->pending.parent;
active->pending.workspace = fixed->pending.workspace;
container_for_each_child(active, set_workspace, NULL);
+ workspace_mosaic_sync_focus(active->pending.workspace);
container_handle_fullscreen_reparent(active);
container_update_representation(active);
}
@@ -1556,6 +1562,7 @@ void container_add_child(struct sway_container *parent,
child->pending.parent = parent;
child->pending.workspace = parent->pending.workspace;
container_for_each_child(child, set_workspace, NULL);
+ workspace_mosaic_sync_focus(child->pending.workspace);
container_handle_fullscreen_reparent(child);
container_update_representation(parent);
node_set_dirty(&child->node);
@@ -1572,6 +1579,16 @@ void container_detach(struct sway_container *child) {
struct sway_container *old_parent = child->pending.parent;
struct sway_workspace *old_workspace = child->pending.workspace;
+ // A Mosaic focus reference must not outlive a detached Sway container.
+ if (old_workspace && old_workspace->mosaic) {
+ struct mosaic_workspace_state *mosaic = old_workspace->mosaic;
+ for (int i = 0; i < mosaic->rows->length; ++i) {
+ struct mosaic_row *row = mosaic->rows->items[i];
+ if (row->state.focused == child) {
+ row->state.focused = NULL;
+ }
+ }
+ }
list_t *siblings = container_get_siblings(child);
if (siblings) {
int index = list_find(siblings, child);
diff --git a/sway/tree/mosaic.c b/sway/tree/mosaic.c
new file mode 100644
index 0000000000000000000000000000000000000000..a31f37e7dea3906a27df76648380b8d876f68ee9
--- /dev/null
+++ b/sway/tree/mosaic.c
@@ -0,0 +1,155 @@
+#include <math.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include "sway/tree/mosaic.h"
+
+static struct mosaic_row *row_create(list_t *children, bool owns_children) {
+ struct mosaic_row *row = calloc(1, sizeof(*row));
+ if (!row) {
+ return NULL;
+ }
+ row->children = children;
+ row->owns_children = owns_children;
+ return row;
+}
+
+struct mosaic_workspace_state *mosaic_state_create(list_t *first_children) {
+ if (!first_children) {
+ return NULL;
+ }
+ struct mosaic_workspace_state *state = calloc(1, sizeof(*state));
+ if (!state) {
+ return NULL;
+ }
+ state->rows = create_list();
+ if (!state->rows) {
+ free(state);
+ return NULL;
+ }
+ struct mosaic_row *row = row_create(first_children, false);
+ if (!row) {
+ list_free(state->rows);
+ free(state);
+ return NULL;
+ }
+ list_add(state->rows, row);
+ return state;
+}
+
+void mosaic_state_destroy(struct mosaic_workspace_state *state) {
+ if (!state) {
+ return;
+ }
+ for (int i = 0; i < state->rows->length; ++i) {
+ struct mosaic_row *row = state->rows->items[i];
+ if (row->owns_children) {
+ list_free(row->children);
+ }
+ free(row);
+ }
+ list_free(state->rows);
+ free(state);
+}
+
+struct mosaic_row *mosaic_state_add_row(struct mosaic_workspace_state *state) {
+ if (!state) {
+ return NULL;
+ }
+ list_t *children = create_list();
+ if (!children) {
+ return NULL;
+ }
+ struct mosaic_row *row = row_create(children, true);
+ if (!row) {
+ list_free(children);
+ return NULL;
+ }
+ list_add(state->rows, row);
+ return row;
+}
+
+bool mosaic_state_select_row(struct mosaic_workspace_state *state, int index) {
+ if (!state || index < 0 || index >= state->rows->length) {
+ return false;
+ }
+ state->active_row = index;
+ return true;
+}
+
+int mosaic_row_insertion_index(const struct mosaic_row *row) {
+ if (!row || !row->children) {
+ return 0;
+ }
+ int index = list_find(row->children, row->state.focused);
+ return index < 0 ? row->children->length : index + 1;
+}
+
+void mosaic_row_insert_after_focus(struct mosaic_row *row,
+ struct sway_container *child) {
+ if (row && child) {
+ list_insert(row->children, mosaic_row_insertion_index(row), child);
+ }
+}
+
+void mosaic_row_remove(struct mosaic_row *row, struct sway_container *child) {
+ if (!row || !child) {
+ return;
+ }
+ int index = list_find(row->children, child);
+ if (index >= 0) {
+ list_del(row->children, index);
+ }
+ // INVARIANT: no row retains focus on a detached or destroyed container.
+ if (row->state.focused == child) {
+ row->state.focused = NULL;
+ }
+}
+
+bool mosaic_row_focus(struct mosaic_row *row, struct sway_container *child) {
+ if (!row || (child && list_find(row->children, child) < 0)) {
+ return false;
+ }
+ row->state.focused = child;
+ return true;
+}
+
+bool mosaic_row_set_scroll(struct mosaic_row *row, double scroll_x) {
+ if (!row || !isfinite(scroll_x) || scroll_x < 0) {
+ return false;
+ }
+ row->state.scroll_x = scroll_x;
+ return true;
+}
+
+int mosaic_row_max_scroll(const struct mosaic_row *row, int viewport_width,
+ int gaps) {
+ if (!row || !row->children || viewport_width <= 0 ||
+ row->children->length == 0) {
+ return 0;
+ }
+ int count = row->children->length;
+ int cell = row->state.minimum_cell_width > 0 ?
+ row->state.minimum_cell_width : 640;
+ if (gaps < 0) {
+ gaps = 0;
+ }
+ int64_t occupied_gaps = (int64_t)(count - 1) * gaps;
+ int64_t fitting = ((int64_t)viewport_width - occupied_gaps) / count;
+ if (fitting > cell) {
+ cell = fitting > INT_MAX ? INT_MAX : (int)fitting;
+ }
+ int64_t overflow = (int64_t)count * cell + occupied_gaps - viewport_width;
+ return overflow <= 0 ? 0 : overflow > INT_MAX ? INT_MAX : (int)overflow;
+}
+
+void mosaic_row_clamp_scroll(struct mosaic_row *row, int viewport_width,
+ int gaps) {
+ if (!row) {
+ return;
+ }
+ int maximum = mosaic_row_max_scroll(row, viewport_width, gaps);
+ if (row->state.scroll_x > maximum) {
+ row->state.scroll_x = maximum;
+ }
+}
diff --git a/sway/tree/view.c b/sway/tree/view.c
index 764e56de419d48b9b99dc6cf6fe920773147b309..2079d0834065c9430099992c9841127d9ea0f1fc 100644
--- a/sway/tree/view.c
+++ b/sway/tree/view.c
@@ -32,6 +32,7 @@
#include "sway/sway_text_node.h"
#include "sway/tree/arrange.h"
#include "sway/tree/container.h"
+#include "sway/tree/mosaic.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
#include "sway/config.h"
@@ -896,6 +897,15 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
target_sibling = node->sway_container;
}
}
+ if (ws && ws->layout == L_MOSAIC && ws->mosaic) {
+ // New tiled views join after the row's active top-level container;
+ // workspace->tiling remains the sole ordering authority.
+ struct mosaic_row *row = ws->mosaic->rows->items[0];
+ target_sibling = row->state.focused;
+ if (target_sibling && list_find(ws->tiling, target_sibling) < 0) {
+ target_sibling = NULL;
+ }
+ }
struct wlr_ext_foreign_toplevel_handle_v1_state foreign_toplevel_state = {
.app_id = view_get_app_id(view),
diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c
index 401e3e7ec3e741810a3e062ae99c028d09879905..ca91a6a7d2d05afa155ac4efe04c62b35deefd33 100644
--- a/sway/tree/workspace.c
+++ b/sway/tree/workspace.c
@@ -17,6 +17,7 @@
#include "sway/server.h"
#include "sway/tree/arrange.h"
#include "sway/tree/container.h"
+#include "sway/tree/mosaic.h"
#include "sway/tree/node.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
@@ -217,6 +218,7 @@ struct sway_workspace *workspace_create(struct sway_output *output,
ws->name = strdup(name);
ws->prev_split_layout = L_NONE;
ws->layout = output_get_default_layout(output);
+ ws->mosaic_previous_standard_layout = ws->layout;
ws->floating = create_list();
ws->tiling = create_list();
ws->output_priority = create_list();
@@ -294,6 +296,8 @@ void workspace_destroy(struct sway_workspace *workspace) {
free(workspace->representation);
list_free_items_and_destroy(workspace->output_priority);
list_free(workspace->floating);
+ // Row zero borrows workspace->tiling; release the state before that list.
+ mosaic_state_destroy(workspace->mosaic);
list_free(workspace->tiling);
list_free(workspace->current.floating);
list_free(workspace->current.tiling);
@@ -955,9 +959,15 @@ struct sway_container *workspace_add_tiling(struct sway_workspace *workspace,
if (config->default_layout != L_NONE) {
con = container_split(con, config->default_layout);
}
- list_add(workspace->tiling, con);
+ if (workspace->layout == L_MOSAIC && workspace->mosaic) {
+ struct mosaic_row *row = workspace->mosaic->rows->items[0];
+ mosaic_row_insert_after_focus(row, con);
+ } else {
+ list_add(workspace->tiling, con);
+ }
con->pending.workspace = workspace;
container_for_each_child(con, set_workspace, NULL);
+ workspace_mosaic_sync_focus(workspace);
container_handle_fullscreen_reparent(con);
workspace_update_representation(workspace);
node_set_dirty(&workspace->node);
@@ -965,6 +975,20 @@ struct sway_container *workspace_add_tiling(struct sway_workspace *workspace,
return con;
}
+void workspace_mosaic_sync_focus(struct sway_workspace *workspace) {
+ if (!workspace || workspace->layout != L_MOSAIC || !workspace->mosaic) {
+ return;
+ }
+ struct sway_seat *seat = input_manager_current_seat();
+ struct sway_container *focused = seat_get_focused_container(seat);
+ if (!focused || focused->pending.workspace != workspace) {
+ return;
+ }
+ struct sway_container *top = container_toplevel_ancestor(focused);
+ struct mosaic_row *row = workspace->mosaic->rows->items[0];
+ mosaic_row_focus(row, top);
+}
+
void workspace_add_floating(struct sway_workspace *workspace,
struct sway_container *con) {
if (con->pending.workspace) {
@@ -983,6 +1007,7 @@ void workspace_insert_tiling_direct(struct sway_workspace *workspace,
list_insert(workspace->tiling, index, con);
con->pending.workspace = workspace;
container_for_each_child(con, set_workspace, NULL);
+ workspace_mosaic_sync_focus(workspace);
container_handle_fullscreen_reparent(con);
workspace_update_representation(workspace);
node_set_dirty(&workspace->node);
diff --git a/tests/mosaic-model.c b/tests/mosaic-model.c
new file mode 100644
index 0000000000000000000000000000000000000000..ddff5b0787ea25e067befb50bba0b0242d41e2b2
--- /dev/null
+++ b/tests/mosaic-model.c
@@ -0,0 +1,58 @@
+#include <assert.h>
+#include <stddef.h>
+#include "sway/tree/mosaic.h"
+
+// Distinct addresses stand for containers; the model must never dereference
+// them or infer geometry from their identity.
+static int a, b, c, d, x, f;
+#define CON(v) ((struct sway_container *)&(v))
+
+int main(void) {
+ list_t *tiling = create_list();
+ assert(tiling);
+ list_add(tiling, CON(a));
+ list_add(tiling, CON(b));
+ list_add(tiling, CON(c));
+ list_add(tiling, CON(d));
+ struct mosaic_workspace_state *state = mosaic_state_create(tiling);
+ assert(state && state->rows->length == 1);
+ struct mosaic_row *row0 = state->rows->items[0];
+ assert(row0->children == tiling && !row0->owns_children);
+
+ assert(mosaic_row_focus(row0, CON(a)));
+ mosaic_row_insert_after_focus(row0, CON(x));
+ assert(tiling->length == 5 && tiling->items[0] == CON(a) &&
+ tiling->items[1] == CON(x) && tiling->items[2] == CON(b) &&
+ tiling->items[3] == CON(c) && tiling->items[4] == CON(d));
+ mosaic_row_remove(row0, CON(c));
+ assert(tiling->length == 4 && tiling->items[3] == CON(d));
+ assert(mosaic_row_focus(row0, CON(b)));
+ assert(tiling->items[0] == CON(a) && tiling->items[1] == CON(x));
+ assert(mosaic_row_set_scroll(row0, 1200));
+ assert(!mosaic_row_set_scroll(row0, -1));
+ assert(tiling->items[2] == CON(b));
+ assert(mosaic_row_max_scroll(row0, 1280, 0) == 1280);
+ mosaic_row_clamp_scroll(row0, 1280, 0);
+ assert(row0->state.scroll_x == 1200);
+ assert(mosaic_row_max_scroll(row0, 2560, 0) == 0);
+ // Clamping the camera does not change the borrowed Sway tiling order.
+ assert(tiling->items[2] == CON(b));
+
+ struct mosaic_row *row1 = mosaic_state_add_row(state);
+ assert(row1 && row1->owns_children);
+ list_add(row1->children, CON(f));
+ assert(mosaic_row_max_scroll(row1, 300, 0) == 340);
+ assert(mosaic_row_focus(row1, CON(f)));
+ assert(mosaic_row_set_scroll(row1, 400));
+ assert(mosaic_state_select_row(state, 1));
+ assert(mosaic_state_select_row(state, 0));
+ assert(row0->state.scroll_x == 1200 && row0->state.focused == CON(b));
+ assert(row1->state.scroll_x == 400 && row1->state.focused == CON(f));
+ mosaic_row_remove(row0, CON(b));
+ assert(row0->state.focused == NULL);
+ assert(!mosaic_state_select_row(state, 2));
+ mosaic_state_destroy(state);
+ assert(tiling->length == 3); // Borrowed Sway list survives state destruction.
+ list_free(tiling);
+ return 0;
+}