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 +#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 +#include +#include +#include #include #include #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 ' 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 #include +#include +#include #include #include #include @@ -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 #include +#include +#include #include #include #include #include #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 +#include +#include +#include +#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 +#include +#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; +}