feat(mosaic): add native spatial canvas layout
This commit is contained in:
parent
a57e3f2c4d
commit
56c73e5e6e
25 changed files with 2179 additions and 40 deletions
53
PKGBUILD
Normal file
53
PKGBUILD
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Maintainer: fy59 / labfytools
|
||||
# CONTRACT: coexistence avec swayfx ; ce paquet possède uniquement
|
||||
# /usr/bin/swayfx-mosaic et n'annonce aucun remplacement de sway/swayfx.
|
||||
pkgname=swayfx-mosaic
|
||||
pkgver=0.6+mosaic.1
|
||||
pkgrel=1
|
||||
pkgdesc='SwayFX 0.6 with the native Mosaic / Spatial Canvas layout'
|
||||
arch=('x86_64')
|
||||
url='https://git.labfytools.com/fy59/swayfx-mosaic'
|
||||
license=('MIT')
|
||||
options=('!debug')
|
||||
depends=('cairo' 'gdk-pixbuf2' 'libevdev' 'libinput' 'json-c'
|
||||
'systemd-libs' 'wayland' 'libxcb' 'libxkbcommon' 'pango' 'pcre2'
|
||||
'scenefx' 'ttf-font' 'wlroots0.20')
|
||||
makedepends=('git' 'meson' 'ninja' 'pkgconf' 'python' 'wayland-protocols'
|
||||
'libdrm' 'pixman')
|
||||
checkdepends=('kitty' 'grim')
|
||||
_upstream_commit=fd71a6bdc061bd633b488ae7b83e8a6981d22f86
|
||||
source=("swayfx::git+https://github.com/WillPower3309/swayfx.git#commit=${_upstream_commit}"
|
||||
"mosaic.patch::file://${startdir}/patches/0001-mosaic-layout-viewport.patch"
|
||||
"headless-ipc.py::file://${startdir}/tests/headless-ipc.py")
|
||||
sha256sums=('SKIP'
|
||||
'a04cd06a6ef4fc58e3b40333a53226849e82d9a4757b9ccee58ad7b59a23ff4b'
|
||||
'0a16c887ca6a87f0392c2eef498b4fad78b9f32afaf7d2a6c0e5e0e37c3c1744')
|
||||
|
||||
prepare() {
|
||||
cd "$srcdir/swayfx"
|
||||
# No automatic forward port: an upstream commit change must be reviewed.
|
||||
[[ $(git rev-parse HEAD) == "$_upstream_commit" ]] || return 1
|
||||
git apply --check "$srcdir/mosaic.patch" || return 1
|
||||
git apply "$srcdir/mosaic.patch"
|
||||
}
|
||||
|
||||
build() {
|
||||
meson setup "$srcdir/swayfx" "$srcdir/build-mosaic" \
|
||||
--prefix=/usr --bindir=bin \
|
||||
-Dwerror=false -Dsd-bus-provider=libsystemd \
|
||||
-Dmosaic-package=true -Dswaybar=false -Dswaynag=false \
|
||||
-Dman-pages=disabled -Ddefault-wallpaper=false
|
||||
meson compile -C "$srcdir/build-mosaic"
|
||||
}
|
||||
|
||||
check() {
|
||||
meson test -C "$srcdir/build-mosaic" --print-errorlogs
|
||||
MOSAIC_BINARY="$srcdir/build-mosaic/sway/swayfx-mosaic" \
|
||||
python "$srcdir/headless-ipc.py"
|
||||
}
|
||||
|
||||
package() {
|
||||
meson install -C "$srcdir/build-mosaic" --destdir "$pkgdir"
|
||||
[[ -x "$pkgdir/usr/bin/swayfx-mosaic" ]] || return 1
|
||||
[[ $(find "$pkgdir" -type f | wc -l) -eq 1 ]] || return 1
|
||||
}
|
||||
87
docs/STEP20A.md
Normal file
87
docs/STEP20A.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# STEP20A — fondation Spatial Canvas native
|
||||
|
||||
## Base et provenance
|
||||
|
||||
- Installation protégée : paquet Arch `swayfx 0.6-1`, binaire `/usr/bin/sway`, `swayfx version 0.6 (based on sway 1.12.0)`. `pacman -Qkk swayfx` : 58 fichiers, aucun altéré.
|
||||
- Source amont : `https://github.com/WillPower3309/swayfx`, tag `0.6`, commit `fd71a6bdc061bd633b488ae7b83e8a6981d22f86` ; la branche `master` courante n'a pas été utilisée.
|
||||
- Bibliothèques installées : `wlroots0.20 0.20.2-1` (`pkg-config wlroots-0.20` : `0.20.2`), `scenefx 0.5-1` (`pkg-config scenefx-0.5` : `0.5.0`). Le tag demande `wlroots-0.20 >=0.20,<0.21` et `scenefx-0.5`.
|
||||
- Le cache AUR local contient un `PKGBUILD` **0.5.3-4**. Le dépôt AUR distant, commit `55c2e27f83e54eadd9d811bc123b868e6ba56866`, contient **0.6-0**, alors que le paquet local est **0.6-1** ; celui-ci a donc une recette ou révision locale qui n'est plus disponible ici. Les métadonnées du paquet ne contiennent pas son archive source ni son commit. La correspondance binaire exacte au commit du tag n'est donc pas démontrable. Le tag `0.6` est la base vérifiable qui reproduit la version annoncée et se compile avec les dépendances installées.
|
||||
- Options Arch pertinentes reprises : `-Dwerror=false -Dsd-bus-provider=libsystemd`, avec les bibliothèques système ci-dessus. `-Dwerror=true` échoue aussi sur une incompatibilité amont avec `libinput 1.32.0` : le `switch` de `sway/ipc-json.c` ne traite pas `LIBINPUT_CONFIG_SCROLL_CIRCULAR`.
|
||||
|
||||
## Baseline avant modification
|
||||
|
||||
```sh
|
||||
meson setup build-baseline -Dwerror=false -Dsd-bus-provider=libsystemd
|
||||
meson compile -C build-baseline -j 8
|
||||
meson test -C build-baseline --print-errorlogs
|
||||
build-baseline/sway/sway --version
|
||||
```
|
||||
|
||||
Résultat : build PASS, binaire répond `swayfx version 0.6-fd71a6b ... (based on sway 1.12.0)`. Meson indique `No tests defined.` pour l'amont. Aucune installation système.
|
||||
La baseline binaire reste dans `build-baseline/sway/sway`, SHA-256 `5c377fdd5b6f72d343699dd1949956dfa73fe08b7c5dd305c45a5bb020bd8ccb` ; elle a été construite avant la première modification fonctionnelle et n'a pas été reconstruite depuis.
|
||||
|
||||
## Validation du fork
|
||||
|
||||
```sh
|
||||
meson setup build-mosaic -Dwerror=false -Dsd-bus-provider=libsystemd \
|
||||
-Dprefix=/usr -Dbindir=bin -Dswaybar=false -Dswaynag=false \
|
||||
-Dman-pages=disabled -Ddefault-wallpaper=false
|
||||
meson compile -C build-mosaic -j 8
|
||||
meson test -C build-mosaic --print-errorlogs
|
||||
python tests/headless-ipc.py
|
||||
meson install -C build-mosaic --destdir /tmp/step20a-stage
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Résultat : build PASS ; modèle `1/1 PASS` ; headless IPC et six fenêtres Kitty PASS ; installation de staging limitée au binaire ; `git diff --check` PASS. La validation de la configuration quotidienne par `/usr/bin/sway -C -c /home/fy59/.config/sway/config` retourne 0 ; wlroots y écrit deux diagnostics EGL `EGL_BAD_ALLOC`, sans démarrer de session ni invalider la configuration.
|
||||
|
||||
## Carte des points d'insertion
|
||||
|
||||
| Couche | Fichier/fonction | Rôle et point d'insertion |
|
||||
| --- | --- | --- |
|
||||
| Arbre | `include/sway/tree/container.h`, `workspace.h` | Enum de layout, workspace et listes `tiling`/`floating` ; état Mosaic attaché au workspace. |
|
||||
| Insertion/suppression | `sway/tree/workspace.c:workspace_add_tiling`, `sway/tree/view.c:view_map`, `sway/tree/container.c:container_detach` | L'ordre de `workspace->tiling` fait autorité pour Row0. Insertion après le dernier actif, nettoyage de la référence de focus à la sortie. |
|
||||
| Géométrie logique | `sway/tree/arrange.c:arrange_workspace`, `apply_horiz_layout` | Calcule `pending` et les tailles des enfants. En STEP20A, Mosaic emprunte le calcul horizontal standard. |
|
||||
| Transaction/scène | `sway/desktop/transaction.c:arrange_children`, `arrange_workspace_tiling` | Applique l'état `current`, reparent les scènes et positionne les containers. Le nouvel enum doit passer ici également. |
|
||||
| Output/rendu | `sway/desktop/output.c` (`wlr_scene_output_build_state`) | Composition de la scène dans le buffer d'output. |
|
||||
| Clipping | `sway/tree/view.c:view_center_and_clip_surface`, `sway/desktop/transaction.c:arrange_container` | Clip de contenu au rectangle d'une vue, pas un clip général du viewport Mosaic. |
|
||||
| Hit testing | `sway/input/cursor.c:node_at_coords` | Interroge `wlr_scene_node_at` ; toute translation de caméra devra conserver la correspondance avec la scène interactive. |
|
||||
| Focus/move | `sway/commands/focus.c`, `move.c`, `sway/input/seat.c:seat_set_workspace_focus` | Navigation directionnelle, réorganisation et dernier focus par ligne ; aucune nouvelle touche n'est câblée ici. |
|
||||
| IPC | `sway/ipc-json.c:ipc_json_describe_workspace`, `ipc_json_layout_description`, `sway/ipc-server.c` | `get_tree` publie `layout: "mosaic"` ; `get_workspaces` et les événements continuent par les chemins Sway existants. |
|
||||
| Parser | `sway/commands/layout.c:cmd_layout` | `layout mosaic` active explicitement le mode sur le workspace ; les layouts classiques restent disponibles. |
|
||||
|
||||
## État réalisé
|
||||
|
||||
```text
|
||||
Workspace 1 (layout=mosaic, opt-in)
|
||||
└── Row0 (children emprunte workspace->tiling)
|
||||
├── A
|
||||
├── B
|
||||
└── C
|
||||
scroll_x = 1200 (coordonnée logique, non rendue en STEP20A)
|
||||
focused = B
|
||||
active_row = 0
|
||||
```
|
||||
|
||||
`mosaic_workspace_state` possède les objets `mosaic_row`, jamais les containers Sway. Row0 emprunte `workspace->tiling`, sans copie de l'ordre. Les autres lignes ont leur propre liste dans le modèle et sont vérifiées par test unitaire ; leur branchement à l'arbre, aux transactions et à l'IPC attend une tranche ultérieure. Un `scroll_x` négatif ou non fini est refusé. Changer le focus ou `scroll_x` ne réordonne pas les enfants. L'état est libéré avec le workspace et reste dormant si l'on repasse à un layout classique.
|
||||
|
||||
La première ligne reste tiled et non flottante. Le test headless a observé `A B C`, puis `A X B C` après focus de A et création de X, puis `A X B` après fermeture de C. Un `move right` suivi de `move left` garde Mosaic actif et permet ensuite d'insérer Y immédiatement après X. Après retour à `splith`, Mosaic peut être réactivé sur un workspace occupé et la nouvelle fenêtre s'insère encore après A. Le test vérifie aussi des rectangles sans chevauchement et `floating_nodes=[]`. Ce n'est **pas** encore une ligne plus large que le viewport ni un POC graphique scrollable : aucun ratio de largeur de cellule n'a été fixé.
|
||||
|
||||
## Exécution et coexistence
|
||||
|
||||
Le fork produit `build-mosaic/sway/swayfx-mosaic`. L'option Meson `mosaic-package=true` installe seulement ce binaire. La vérification `meson install -C build-mosaic --destdir /tmp/step20a-stage` n'a produit que `/tmp/step20a-stage/usr/bin/swayfx-mosaic`. Le paquet Arch futur pourra dépendre des bibliothèques communes et déclarer ce seul chemin ; il ne doit déclarer ni `provides=sway`, ni `conflicts=swayfx`, ni installer les fichiers `/etc/sway`, `swaymsg`, `swaybar`, les complétions ou la session `sway.desktop` déjà possédés par SwayFX. Une session dédiée, sous un autre nom, sera une étape ultérieure. Aucun fichier de `/usr` n'a été modifié ici.
|
||||
|
||||
Activation dans le seul processus du fork : `swaymsg -s <socket-du-fork> 'layout mosaic'`. Le socket explicite est essentiel en test : une commande envoyée à la session quotidienne n'active rien dans le fork. La session actuelle n'a pas été relancée ou rechargée.
|
||||
|
||||
## Compatibilité et limites
|
||||
|
||||
- IPC : `get_tree` annonce `mosaic` sur le workspace, en conservant les noeuds Sway et leurs identités. `get_workspaces` et les événements utilisent les chemins existants. Les clients qui traitent les layouts comme un enum fermé doivent ajouter `mosaic` avant de restaurer ce workspace.
|
||||
- QuickShell : les vues générales IPC peuvent lire l'arbre ; `session_v2/schema.py` limite explicitement `LAYOUTS` à `splith`, `splitv`, `tabbed`, `stacked`, et `session_v2/snapshot.py` a aussi une condition sur les layouts à un enfant. STEP20E devra représenter les lignes, l'ordre, `active_row`, `scroll_x` et le focus de chaque ligne, avec des identités stables de fenêtres et un ordre de restauration défini. Aucun fichier QuickShell n'a été modifié dans STEP20A.
|
||||
- Scratchpad, floating et fullscreen : les chemins Sway existants sont conservés. Le test vérifie le tiling normal, mais ne constitue pas encore une validation exhaustive des transitions scratchpad, floating et fullscreen sous Mosaic.
|
||||
- Clavier : `$mod+Alt+m` ouvre Applications. `$mod+Alt+l` est libre et réservé au futur sélecteur de layout, sans commande factice. Son ancien usage Applications a été retiré. Le bind `limusic-app` qui occupait `$mod+Alt+m` a été supprimé ; son autostart reste en place. `$mod+s`, `$mod+Shift+w`, `$mod+e` restent inchangés. Aucun reload de la session actuelle n'a été déclenché.
|
||||
|
||||
## Risques et STEP20B
|
||||
|
||||
La scène wlroots peut positionner les noeuds indépendamment de leur ordre logique. Le moteur d'output compose déjà la partie visible dans le buffer physique, mais un vrai viewport Mosaic doit définir une translation de caméra et un clip **par workspace/ligne** pour éviter qu'une portion hors viewport apparaisse sur un autre output. Le clip existant des surfaces ne fournit pas cette frontière. Le hit testing utilise la scène ; l'offset visuel et les rectangles des commandes `focus`/`move` doivent rester cohérents. La transaction distingue `pending` et `current` et possède des animations : le déplacement de caméra doit être appliqué au bon instant, sans saut ni divergence IPC. Les changements d'output, de taille et de workspace doivent recalculer le viewport sans perdre `scroll_x`.
|
||||
|
||||
STEP20B doit livrer une première ligne réellement plus large que son viewport, scrollable par une commande native explicite, avec largeur temporaire isolée et configurable sans imposer de politique UX finale. Il doit relier géométrie logique, translation de scène, clip, input et focus directionnel ; couvrir les transactions et les changements de workspace/output par tests headless. Le modèle des lignes supplémentaires sera raccordé après la preuve d'une ligne utilisable.
|
||||
62
docs/STEP20B.md
Normal file
62
docs/STEP20B.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# STEP20B — première ligne Mosaic scrollable
|
||||
|
||||
## Contrat et base
|
||||
|
||||
Base source : SwayFX tag `0.6`, commit `fd71a6bdc061bd633b488ae7b83e8a6981d22f86`. La correspondance byte pour byte au paquet local `swayfx 0.6-1` reste invérifiable faute de source de sa révision de paquet ; voir [STEP20A](STEP20A.md). Le fork est toujours compilé contre `wlroots-0.20` et `scenefx-0.5`. SwayFX standard conserve `/usr/bin/sway` et son cycle de mise à jour. Aucun paquet système n'a été installé.
|
||||
|
||||
Le dépôt de travail conserve une branche contenant la source complète. Le fichier `patches/0001-mosaic-layout-viewport.patch` rassemble le delta de production à appliquer uniquement sur le commit indiqué. `PKGBUILD` contrôle le commit avant application et ne fournit que `/usr/bin/swayfx-mosaic`. Il ne déclare aucun `provides`, `conflicts` ni `replaces` pour SwayFX. Une nouvelle version upstream exige une adaptation manuelle du patch et une nouvelle validation ; ne jamais réutiliser automatiquement le patch 0.6 sur 0.7.
|
||||
|
||||
## Fonctionnement natif
|
||||
|
||||
- `layout mosaic` active le layout du workspace courant. Le mode reste inactif par défaut.
|
||||
- `layout toggle mosaic` bascule entre Mosaic et le dernier layout standard propre à ce workspace. Les commandes standards `layout ...` restent reconnues. Les raccourcis historiques `$mod+s`, `$mod+Shift+w` et `$mod+e` restent en place.
|
||||
- `layout mosaic scroll <signed pixels>` déplace la caméra logique de Row0 ; la valeur est bornée entre zéro et le dernier pixel du canvas. Le focus directionnel ramène une cellule focalisée dans le viewport. Aucune combinaison clavier supplémentaire de scroll n'a été inventée.
|
||||
- Row0 emprunte directement `workspace->tiling` : les noeuds restent de vrais conteneurs tiled dans l'arbre Sway. `pending`/`current` portent la géométrie logique au delà de l'écran ; la scène du tiling est translatée de `-scroll_x`. Le hit testing suit les coordonnées de la scène. `get_tree` conserve les champs Sway et ajoute `mosaic: {active_row, rows:[{children, scroll_x, focused}]}` seulement en mode Mosaic.
|
||||
- La largeur minimale temporaire d'une cellule est 640 px, isolée dans le modèle (`minimum_cell_width`, zéro signifie 640). L'espace disponible est réparti si cela donne des cellules plus larges. Cette valeur n'est pas une politique UX finale ni une commande publique.
|
||||
- Quand `layout tabbed` ou `layout stacking` a créé un seul conteneur racine autour des fenêtres d'un workspace occupé, l'entrée dans Mosaic promeut ses enfants dans Row0. La sortie restaure le layout standard au niveau du workspace. Les sous-arbres plus profonds restent des conteneurs groupés et sont une unité de Row0.
|
||||
|
||||
Exemple :
|
||||
|
||||
```text
|
||||
Workspace 1 (layout=mosaic, previous_standard_layout=tabbed)
|
||||
└── Row0, active_row=0
|
||||
├── A [0,640)
|
||||
├── B [640,1280)
|
||||
├── C [1280,1920)
|
||||
└── D [1920,2560)
|
||||
scroll_x=400, focused=B
|
||||
```
|
||||
|
||||
Les coordonnées ci-dessus sont illustratives, sans gaps Sway. La sortie physique ne rend que sa surface ; sur la machine mono-écran `eDP-1`, cela fournit une ligne effectivement scrollable. Le test headless capture deux images PPM avant/après `scroll 400` et observe un changement substantiel de pixels sans changement d'ordre ou des rectangles logiques IPC.
|
||||
|
||||
## Clavier et compatibilité standard
|
||||
|
||||
Dans les dotfiles partagés, `$mod+Alt+m` lance `applications-quickshell` et `$mod+Alt+l` appelle `~/.local/bin/labfy-layout-toggle`. Le helper n'a aucun état : il envoie `layout toggle mosaic` au socket du compositor courant. Sous SwayFX standard, la commande non reconnue n'altère pas le workspace et le helper retourne proprement. Le test headless séparé sur `/usr/bin/sway` vérifie le maintien de `splith`. La session actuelle n'a pas été rechargée ; le nouveau bind ne sera pris en compte qu'au prochain chargement normal de configuration.
|
||||
|
||||
QuickShell n'a pas été modifié. `session_v2` a encore un ensemble fermé de layouts standards ; STEP20E devra enregistrer l'ordre des conteneurs, les lignes, `active_row`, chaque `scroll_x`, le focus de chaque ligne et une identité durable pour restituer les fenêtres. L'IPC garde les événements Sway et `get_workspaces` habituels ; les lecteurs stricts de `get_tree.layout` devront reconnaître `mosaic`.
|
||||
|
||||
Floating, fullscreen et scratchpad suivent les chemins Sway existants et ne sont pas déplacés par la caméra du tiling. Le test couvre le tiling, la suppression, l'insertion, `move`, le focus, les transitions standard/Mosaic, ainsi qu'un aller-retour `floating`, `fullscreen` et `move scratchpad`/`scratchpad show`. Les interactions prolongées avec plusieurs outputs demandent encore une campagne dédiée avant publication générale.
|
||||
|
||||
## Limites et risques à traiter avant publication
|
||||
|
||||
- SceneFX 0.5 n'expose pas de clip générique pour un `wlr_scene_tree`. La sortie physique crope le canvas sur cette configuration mono-écran, mais une ligne traversant la frontière d'un output adjacent pourrait y apparaître. Le support multi-écran nécessite un clip de scène par viewport ou une stratégie de rendu équivalente, avec hit testing cohérent. Ce point bloque une publication générale multi-écran.
|
||||
- Le déplacement de caméra est immédiat. L'animation courte et continue demandée pour le produit reste à implémenter sans diverger des transactions `pending/current` ni des animations de workspace.
|
||||
- Le changement de taille et le déplacement de workspace vers un autre output recalculent la limite de scroll via le nouveau viewport ; le modèle garde la coordonnée tant qu'elle reste valide. Une validation de ces chemins et des transitions fullscreen est encore nécessaire.
|
||||
- Les groupes imbriqués restent des cellules entières. La politique finale d'aplatissement, de largeur et de reflow multi-ligne est à fixer après la preuve d'une seule ligne.
|
||||
- `minimum_cell_width` et `scroll_x` restent bornés par les coordonnées `int` de la scène. La capacité pratique au delà d'environ 2 milliards de pixels logiques n'est pas prise en charge par cette tranche.
|
||||
|
||||
## Validation locale
|
||||
|
||||
```sh
|
||||
meson compile -C build-mosaic -j 8
|
||||
meson test -C build-mosaic --print-errorlogs
|
||||
MOSAIC_HELPER=/home/fy59/.local/bin/labfy-layout-toggle python tests/headless-ipc.py
|
||||
meson install -C build-mosaic --destdir /tmp/step20b-stage
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Le build de référence amont et ses résultats sont conservés dans STEP20A. Aucune relance ou reload de la session quotidienne n'a été effectué.
|
||||
|
||||
## STEP20C proposé
|
||||
|
||||
Introduire un clip de viewport correct par workspace, y compris lorsque deux outputs sont adjacents, puis valider rendu, hit testing et drag aux frontières. Ajouter l'animation de caméra et des tests headless couvrant resize d'output, bascule fullscreen, floating, scratchpad et changement de workspace. Définir ensuite la politique UX de largeur sans transformer la valeur temporaire de STEP20B en API durable.
|
||||
|
|
@ -18,6 +18,7 @@ enum sway_container_layout {
|
|||
L_VERT,
|
||||
L_STACKED,
|
||||
L_TABBED,
|
||||
L_MOSAIC,
|
||||
};
|
||||
|
||||
enum sway_container_border {
|
||||
|
|
|
|||
57
include/sway/tree/mosaic.h
Normal file
57
include/sway/tree/mosaic.h
Normal file
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
44
meson.build
44
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(
|
||||
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
|
||||
|
||||
install_data(
|
||||
'sway.desktop',
|
||||
install_dir: join_paths(datadir, 'wayland-sessions')
|
||||
)
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
1130
patches/0001-mosaic-layout-viewport.patch
Normal file
1130
patches/0001-mosaic-layout-viewport.patch
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
155
sway/tree/mosaic.c
Normal file
155
sway/tree/mosaic.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
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);
|
||||
|
|
|
|||
236
tests/headless-ipc.py
Normal file
236
tests/headless-ipc.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test d'intégration isolé : aucun socket ou processus de la session active."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
def wait_for(predicate, seconds=8):
|
||||
deadline = time.monotonic() + seconds
|
||||
while time.monotonic() < deadline:
|
||||
result = predicate()
|
||||
if result:
|
||||
return result
|
||||
time.sleep(0.05)
|
||||
raise RuntimeError("Délai dépassé pendant le test headless")
|
||||
|
||||
|
||||
def stop_group(process):
|
||||
if process.poll() is None:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
process.wait(timeout=5)
|
||||
|
||||
|
||||
def main():
|
||||
if not all(shutil.which(name) for name in ("kitty", "swaymsg", "grim")):
|
||||
raise RuntimeError("kitty, swaymsg et grim sont requis pour ce test")
|
||||
binary = Path(os.environ.get("MOSAIC_BINARY", str(
|
||||
Path(__file__).resolve().parents[1] / "build-mosaic/sway/swayfx-mosaic")))
|
||||
if not binary.is_file():
|
||||
raise RuntimeError(f"Binaire absent : {binary}")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="mosaic-headless-") as root:
|
||||
runtime = Path(root)
|
||||
config = runtime / "config"
|
||||
config.write_text("workspace 1\n", encoding="utf-8")
|
||||
env = os.environ.copy()
|
||||
env.update(XDG_RUNTIME_DIR=root, WLR_BACKENDS="headless",
|
||||
WLR_HEADLESS_OUTPUTS="1", WLR_RENDERER="pixman")
|
||||
env.pop("WAYLAND_DISPLAY", None)
|
||||
env.pop("SWAYSOCK", None)
|
||||
children = []
|
||||
with (runtime / "compositor.log").open("w") as log:
|
||||
compositor = subprocess.Popen([str(binary), "-c", str(config)],
|
||||
env=env, stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True)
|
||||
try:
|
||||
def sockets():
|
||||
ipc = list(runtime.glob("sway-ipc.*.sock"))
|
||||
wayland = [entry for entry in runtime.glob("wayland-*")
|
||||
if not entry.name.endswith(".lock")]
|
||||
return (ipc[0], wayland[0]) if ipc and wayland else None
|
||||
|
||||
socket, wayland = wait_for(sockets)
|
||||
env["WAYLAND_DISPLAY"] = wayland.name
|
||||
env["SWAYSOCK"] = str(socket)
|
||||
|
||||
def ipc(*args):
|
||||
result = subprocess.run(["swaymsg", "-s", str(socket), *args],
|
||||
env=env, text=True, capture_output=True,
|
||||
timeout=5, check=True)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
def workspace():
|
||||
tree = ipc("-t", "get_tree")
|
||||
return next(node for output in tree["nodes"]
|
||||
for node in output["nodes"]
|
||||
if node["type"] == "workspace" and node["name"] == "1")
|
||||
|
||||
assert workspace()["layout"] == "splith"
|
||||
assert ipc("layout mosaic")[0]["success"]
|
||||
assert workspace()["layout"] == "mosaic"
|
||||
assert ipc("-t", "get_workspaces")[0]["name"] == "1"
|
||||
|
||||
def launch(index, expected):
|
||||
child = subprocess.Popen(
|
||||
["kitty", "--class", f"mosaic-step20a-{index}",
|
||||
"/usr/bin/sleep", "30"], env=env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
children.append(child)
|
||||
return wait_for(lambda: workspace()["nodes"]
|
||||
if len(workspace()["nodes"]) == expected else None)
|
||||
|
||||
a = launch(0, 1)[0]["id"]
|
||||
b = launch(1, 2)[1]["id"]
|
||||
c = launch(2, 3)[2]["id"]
|
||||
assert ipc(f"[con_id={a}] focus")[0]["success"]
|
||||
nodes = launch(3, 4)
|
||||
ids = [node["id"] for node in nodes]
|
||||
assert ids[0] == a and ids[2:] == [b, c], ids
|
||||
assert all(node["type"] == "con" for node in nodes)
|
||||
assert workspace()["floating_nodes"] == []
|
||||
# Les rectangles tiled ne se chevauchent pas ; les gaps Sway
|
||||
# configurés restent autorisés.
|
||||
assert all(left["rect"]["x"] + left["rect"]["width"] <=
|
||||
right["rect"]["x"]
|
||||
for left, right in zip(nodes, nodes[1:]))
|
||||
print("Insertion native :", ids, "(A, X, B, C)")
|
||||
|
||||
assert ipc("focus right")[0]["success"]
|
||||
assert workspace()["focus"][0] == b
|
||||
assert ipc("focus left")[0]["success"]
|
||||
assert workspace()["focus"][0] == ids[1]
|
||||
assert [node["id"] for node in workspace()["nodes"]] == ids
|
||||
|
||||
stop_group(children[2])
|
||||
nodes = wait_for(lambda: workspace()["nodes"]
|
||||
if len(workspace()["nodes"]) == 3 else None)
|
||||
assert [node["id"] for node in nodes] == ids[:3]
|
||||
print("Suppression native :", [node["id"] for node in nodes])
|
||||
assert ipc("move right")[0]["success"]
|
||||
assert workspace()["layout"] == "mosaic"
|
||||
moved = [node["id"] for node in workspace()["nodes"]]
|
||||
assert len(moved) == 3 and set(moved) == set(ids[:3]), moved
|
||||
print("Déplacement native :", moved)
|
||||
assert ipc("move left")[0]["success"]
|
||||
assert [node["id"] for node in workspace()["nodes"]] == ids[:3]
|
||||
after_move = [node["id"] for node in launch(4, 4)]
|
||||
assert after_move[:2] == ids[:2] and after_move[3] == b
|
||||
print("Insertion après déplacement :", after_move)
|
||||
assert ipc("layout splith")[0]["success"]
|
||||
assert workspace()["layout"] == "splith"
|
||||
assert [node["id"] for node in workspace()["nodes"]] == after_move
|
||||
assert ipc(f"[con_id={a}] focus")[0]["success"]
|
||||
assert ipc("layout mosaic")[0]["success"]
|
||||
assert workspace()["layout"] == "mosaic"
|
||||
reenabled = [node["id"] for node in launch(5, 5)]
|
||||
assert reenabled[0] == a and reenabled[2:] == after_move[1:]
|
||||
assert workspace()["floating_nodes"] == []
|
||||
print("Réactivation sur workspace occupé :", reenabled)
|
||||
|
||||
ws = workspace()
|
||||
assert ws["nodes"][-1]["rect"]["x"] + ws["nodes"][-1]["rect"]["width"] > ws["rect"]["x"] + ws["rect"]["width"]
|
||||
assert ws["mosaic"]["rows"][0]["children"] == reenabled
|
||||
assert ipc(f"[con_id={a}] focus")[0]["success"]
|
||||
assert workspace()["mosaic"]["rows"][0]["scroll_x"] == 0
|
||||
time.sleep(0.2) # Laisse finir la transaction Wayland précédente.
|
||||
def frame():
|
||||
screenshot = subprocess.run(["grim", "-t", "ppm", "-"], env=env,
|
||||
capture_output=True, timeout=5,
|
||||
check=True)
|
||||
return screenshot.stdout
|
||||
|
||||
before_scroll = frame()
|
||||
logical_rects = [node["rect"] for node in workspace()["nodes"]]
|
||||
assert ipc("layout mosaic scroll 400")[0]["success"]
|
||||
assert workspace()["mosaic"]["rows"][0]["scroll_x"] == 400
|
||||
time.sleep(0.2)
|
||||
after_scroll = frame()
|
||||
assert len(after_scroll) == len(before_scroll)
|
||||
# A translated tiled scene changes a substantial number of
|
||||
# pixels; a blinking cursor alone cannot satisfy this bound.
|
||||
changed = sum(left != right for left, right in
|
||||
zip(before_scroll, after_scroll))
|
||||
print("Composantes de couleur modifiées par scroll :", changed)
|
||||
assert changed > 1000
|
||||
assert [node["rect"] for node in workspace()["nodes"]] == logical_rects
|
||||
assert ipc("layout mosaic scroll -200")[0]["success"]
|
||||
assert workspace()["mosaic"]["rows"][0]["scroll_x"] == 200
|
||||
assert ipc(f"[con_id={reenabled[-1]}] focus")[0]["success"]
|
||||
assert workspace()["mosaic"]["rows"][0]["scroll_x"] > 200
|
||||
assert workspace()["mosaic"]["rows"][0]["focused"] == reenabled[-1]
|
||||
assert ipc(f"[con_id={a}] focus")[0]["success"]
|
||||
assert workspace()["mosaic"]["rows"][0]["scroll_x"] == 0
|
||||
print("Canvas tiled débordant et caméra logique : PASS")
|
||||
|
||||
assert ipc("layout toggle mosaic")[0]["success"]
|
||||
assert workspace()["layout"] == "splith"
|
||||
assert ipc("layout tabbed")[0]["success"]
|
||||
assert ipc("layout toggle mosaic")[0]["success"]
|
||||
assert workspace()["layout"] == "mosaic"
|
||||
assert len(workspace()["nodes"]) == 5
|
||||
assert ipc("layout toggle mosaic")[0]["success"]
|
||||
assert workspace()["layout"] == "tabbed"
|
||||
print("Retour au layout tabbed choisi sur workspace occupé : PASS")
|
||||
assert ipc("workspace 2")[0]["success"]
|
||||
assert ipc("layout tabbed")[0]["success"]
|
||||
tree = ipc("-t", "get_tree")
|
||||
ws2 = next(node for output in tree["nodes"]
|
||||
for node in output["nodes"]
|
||||
if node["type"] == "workspace" and node["name"] == "2")
|
||||
assert ws2["layout"] == "tabbed"
|
||||
assert ipc("layout toggle mosaic")[0]["success"]
|
||||
assert ipc("layout toggle mosaic")[0]["success"]
|
||||
tree = ipc("-t", "get_tree")
|
||||
ws2 = next(node for output in tree["nodes"]
|
||||
for node in output["nodes"]
|
||||
if node["type"] == "workspace" and node["name"] == "2")
|
||||
assert ws2["layout"] == "tabbed"
|
||||
assert ipc("workspace 1")[0]["success"]
|
||||
assert ipc("layout toggle mosaic")[0]["success"]
|
||||
assert workspace()["layout"] == "mosaic"
|
||||
assert ipc("layout toggle mosaic")[0]["success"]
|
||||
assert workspace()["layout"] == "tabbed"
|
||||
helper = os.environ.get("MOSAIC_HELPER")
|
||||
if helper:
|
||||
subprocess.run([helper], env=env, check=True, timeout=5)
|
||||
assert workspace()["layout"] == "mosaic"
|
||||
subprocess.run([helper], env=env, check=True, timeout=5)
|
||||
assert workspace()["layout"] == "tabbed"
|
||||
print("Toggle Standard↔Mosaic par workspace : PASS")
|
||||
assert ipc("layout toggle mosaic")[0]["success"]
|
||||
assert ipc(f"[con_id={a}] focus")[0]["success"]
|
||||
assert ipc("floating enable")[0]["success"]
|
||||
wait_for(lambda: workspace()["floating_nodes"]
|
||||
if len(workspace()["floating_nodes"]) == 1 else None)
|
||||
assert workspace()["layout"] == "mosaic"
|
||||
assert ipc("floating disable")[0]["success"]
|
||||
wait_for(lambda: workspace()["nodes"]
|
||||
if len(workspace()["nodes"]) == 5 else None)
|
||||
assert ipc("fullscreen enable")[0]["success"]
|
||||
assert workspace()["layout"] == "mosaic"
|
||||
assert ipc("fullscreen disable")[0]["success"]
|
||||
assert ipc("move scratchpad")[0]["success"]
|
||||
wait_for(lambda: workspace()["nodes"]
|
||||
if len(workspace()["nodes"]) == 4 else None)
|
||||
assert ipc("scratchpad show")[0]["success"]
|
||||
wait_for(lambda: workspace()["floating_nodes"]
|
||||
if len(workspace()["floating_nodes"]) == 1 else None)
|
||||
print("Floating, fullscreen et scratchpad : PASS")
|
||||
print("IPC et bascules de layout : PASS")
|
||||
finally:
|
||||
for child in children:
|
||||
stop_group(child)
|
||||
stop_group(compositor)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
58
tests/mosaic-model.c
Normal file
58
tests/mosaic-model.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
13
tooling/export-patch.sh
Executable file
13
tooling/export-patch.sh
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
# Rebuild the distributable delta with a private Git index; the real index and
|
||||
# upstream commit stay untouched. Run only after validating this source tree.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
temporary=$(mktemp -d)
|
||||
trap 'rm -rf "$temporary"' EXIT HUP INT TERM
|
||||
export GIT_INDEX_FILE="$temporary/index"
|
||||
git read-tree HEAD
|
||||
git add -A -- include meson.build meson_options.txt sway tests/mosaic-model.c
|
||||
git diff --cached --binary --full-index -- include meson.build meson_options.txt sway tests/mosaic-model.c \
|
||||
> patches/0001-mosaic-layout-viewport.patch
|
||||
git apply --check --reverse patches/0001-mosaic-layout-viewport.patch
|
||||
Loading…
Reference in a new issue