perf(quickshell): smooth dock hover and soften floating shadow

This commit is contained in:
fy59 2026-10-22 11:12:55 +02:00
parent d4e2a0013e
commit 91c39ecf9b
31 changed files with 2280 additions and 32 deletions

View file

@ -0,0 +1,137 @@
# QuickShell Dock Tela — Polish V2
Livraison isolée du 22 octobre 2026, base `d4e2a001`, branche
`quickshell-dock-tela-polish-v2`, worktree
`/home/fy59/.dotfiles-worktrees/quickshell-dock-tela-polish-v2`.
La configuration du bureau actif n’a pas été modifiée ou rechargée.
## Cause et correction du hover
La V1 animait la largeur de chaque case de la `Row` et celle de chaque
`ApplicationIcon`. Chaque déplacement du pointeur modifiait donc les positions
suivantes, la largeur de la rangée et potentiellement celle de la surface.
De plus, `ApplicationIcon` calcule le `sourceSize` de son image depuis sa largeur :
la variation de taille demandait de nouvelles résolutions de rasterisation pour
les SVG Tela asynchrones. Ces deux chemins constituent les causes structurelles
identifiées des saccades et du risque de clignotement. Aucun profilage GPU du
bureau physique n’est revendiqué.
Les cases gardent désormais une largeur de 54 px (plus le séparateur éventuel),
un pas de 57 px et des cibles souris fixes. Les images restent à 46 × 46 px,
avec une résolution demandée fixe de 92 × 92 px. Seul leur `scale` varie de
1 à 1,39, ancré en bas, avec une interpolation OutCubic de 80 ms. La taille
visuelle maximale est ainsi d’environ 64 px. Les deux marges de 12 px à
l’intérieur du viewport protègent aussi les icônes agrandies aux extrémités,
y compris après défilement sur un petit écran.
L’influence du pointeur est une courbe smoothstep, symétrique, de rayon 76 px.
Ce rayon est inférieur à un pas et demi : seules la case la plus proche et ses
voisines immédiates peuvent recevoir une cible de grossissement non nulle.
Les positions de référence restent indépendantes de la transformation.
Les transitions peuvent brièvement finir leur interpolation après la sortie
du rayon. Aucun IPC, blur, chargement d’image ni relayout n’est déclenché par
l’animation elle-même. Les petits calculs d’influence restent liés au pointeur ;
aucun timer de polling ou délai de regroupement supplémentaire n’est nécessaire.
## Fond flottant
Le voile principal passe de 89 % à **16 % d’opacité**, sa hauteur de 70 à 58 px,
avec une bordure de 1 px à 9 % d’opacité. Une `RectangularShadow` QtQuick.Effects,
de géométrie fixe, produit l’ombre diffuse (blur 24, spread 2, noir à 32 %).
Elle ne capture ni les icônes ni le bureau. Le mode de contraste élevé conserve
un fond plus soutenu à 94 % et une bordure à 50 % pour la lisibilité.
La règle SwayFX du seul namespace `labfy-sway-dock` désactive le flou du fond.
Le compromis retenu est un verre teinté très transparent, sans réfraction ni
flou des fenêtres derrière. Le rendu ne dépend donc plus d’un flou compositeur
recalculé pendant le hover. Les effets de la barre restent inchangés.
`RectangularShadow` nécessite Qt ≥ 6.9 ; l’hôte testé utilise Qt 6.12.0,
QuickShell 0.3.2 et SwayFX 0.6 (Sway 1.12.0).
## Contrats préservés
La machine auto-hide conserve sa bande de 4 px, son armement de 180 ms,
sa grâce de sortie de 450 ms et son animation verticale de 210 ms.
`WlrKeyboardFocus.None`, couche Top, `exclusiveZone: 0` et le masque d’entrée
restent en place. Aucune place permanente n’est réservée à l’écran.
Le menu, `FavoritesService`, la résolution Tela et le format des favoris
partagés sont inchangés. Aucun fichier Session V2, Keyring, PAM, greetd ou
logique de session n’a été modifié.
## Validation
Les tests s’exécutent dans un Sway privé headless, avec HOME, XDG,
sockets Wayland/Sway et profil Firefox privés, sans bus de session hérité. Les enfants sont arrêtés par
le laboratoire. Les journaux et résultats sont conservés dans
[validation/quickshell-dock-tela-polish-v2](validation/quickshell-dock-tela-polish-v2/).
Commandes reproductibles depuis le worktree :
```sh
python -m unittest discover -s tests/applications -v
node tests/applications/test_model.js
python -m unittest discover -s tests/dock -v
node tests/dock/test_models.js
python -m unittest discover -s tests/session_v2 -v
python tests/dock/headless.py
python tests/dock/shell_load.py
python tests/dock/session_compat.py
python tests/session_v2/validate_keyring.py --sway /usr/bin/sway
git diff --check
```
Résultats : 7 tests applications, 6 tests dock, 93 tests Session V2 backend,
les deux suites JavaScript, le chargement du shell complet, le scénario dock
et les scénarios Session V2/Keyring avec dock passent. Le validateur Keyring
complet termine avec ses onze contrôles à zéro (succès).
Le test dock vérifie notamment :
- 73 positions latérales sans changement de géométrie, sans changement d’état
des images et avec au plus trois influences non nulles ;
- 4,5 secondes de balayage continu, un mouvement souris par frame Qt,
sans changement de géométrie ni d’état de chargement des images ;
- extrémités non rognées après défilement sur une sortie de 400 px ;
- auto-hide, maintien pendant le menu contextuel, drag réel et redémarrage vide ;
- synchronisation menu/dock/client externe et réordonnancement durable ;
- absence de vol de focus vers le dock, fenêtres Kitty/Firefox privées,
reconnexion IPC et changements de résolution ;
- navigation clavier du menu et présence de la règle SwayFX sans blur du dock.
Le dernier passage mesure 281 intervalles : moyenne 16.00 ms,
95e percentile 16.46 ms, maximum 16.62 ms.
Les intervalles d’animation sont conservés dans `result.json` ; ce sont des
mesures de cadence Qt avec rendu logiciel, pas des timestamps de présentation
matérielle. Ils établissent la stabilité du laboratoire privé. La perception
sur le GPU, le fond d’écran et les sorties physiques de la session active reste
à confirmer après intégration, sans prétendre l’avoir observée ici.
Captures issues de `grim`, sans retouche :
- [Dock fermé](validation/quickshell-dock-tela-polish-v2/closed.png).
- [Dock ouvert](validation/quickshell-dock-tela-polish-v2/open.png).
- [Dock survolé](validation/quickshell-dock-tela-polish-v2/zoom.png).
- [Survol sur fond clair : ombre et transparence](validation/quickshell-dock-tela-polish-v2/zoom-light.png).
## Intégration
Le patch complet, fichiers nouveaux et PNG compris, se trouve à côté du worktree :
`/home/fy59/.dotfiles-worktrees/quickshell-dock-tela-polish-v2.patch`.
Les changements restent non commités et aucun fichier de favoris n’est livré.
Après revue, depuis le dépôt principal à la base compatible :
```sh
cd /home/fy59/.dotfiles
git apply --check /home/fy59/.dotfiles-worktrees/quickshell-dock-tela-polish-v2.patch
git apply /home/fy59/.dotfiles-worktrees/quickshell-dock-tela-polish-v2.patch
swaymsg reload
systemctl --user restart quickshell-labfy-sway.service
```
La vérification `git apply --check` est effectuée à la livraison. Les trois
commandes d’application/rechargement restent à exécuter lors de l’intégration.
Le redémarrage QuickShell recharge aussi les autres composants du shell.
Après intégration, vérifier visuellement un balayage latéral, l’auto-hide et
le menu sur les sorties physiques. Il n’est pas nécessaire de réécrire les
favoris ou de redémarrer la session utilisateur.

View file

@ -0,0 +1,12 @@
test_bad_state_does_not_get_overwritten (test_backend.FavoritesTests.test_bad_state_does_not_get_overwritten) ... ok
test_seed_order_atomic_update_and_missing_entry (test_backend.FavoritesTests.test_seed_order_atomic_update_and_missing_entry) ... ok
test_graphical_uses_desktop_id_and_uwsm_service (test_backend.LaunchTests.test_graphical_uses_desktop_id_and_uwsm_service) ... ok
test_invalid_id_and_missing_entry (test_backend.LaunchTests.test_invalid_id_and_missing_entry) ... ok
test_nodisplay_entry_is_refused_before_launch (test_backend.LaunchTests.test_nodisplay_entry_is_refused_before_launch) ... ok
test_terminal_entry_already_running_kitty_is_not_wrapped_twice (test_backend.LaunchTests.test_terminal_entry_already_running_kitty_is_not_wrapped_twice) ... ok
test_terminal_preserves_desktop_exec_fields_and_kitty (test_backend.LaunchTests.test_terminal_preserves_desktop_exec_fields_and_kitty) ... ok
----------------------------------------------------------------------
Ran 7 tests in 0.004s
OK

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,18 @@
/tmp/labfy-session-v2-TEST_ONLY-s23v41ci
ready PASS
accept PASS
cancel PASS
timeout PASS
absent PASS
still-locked PASS
different PASS
quickshell-restart-during-unlock PASS
keyring_ui WITH_DOCK PASS
success PASS
failure PASS
invalid PASS
new PASS
claimed PASS
real PASS
deferred-real PASS
phase3b_ui WITH_DOCK PASS

View file

@ -0,0 +1 @@
Dock headless: PASS /tmp/labfy-session-v2-TEST_ONLY-dlggtemr

View file

@ -0,0 +1,2 @@
Dock state and window models: PASS
ApplicationsModel: PASS

View file

@ -0,0 +1,59 @@
............................/usr/lib/python3.14/site-packages/gi/events.py:803: DeprecationWarning: 'asyncio.AbstractEventLoopPolicy' is deprecated and slated for removal in Python 3.16
AbstractEventLoopPolicy = getattr(asyncio, "AbstractEventLoopPolicy", object)
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
../usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
./usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
../usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
../usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
..........................................................
----------------------------------------------------------------------
Ran 93 tests in 8.443s
OK

View file

@ -0,0 +1,5 @@
.............................
----------------------------------------------------------------------
Ran 29 tests in 0.129s
OK

View file

@ -0,0 +1,4 @@
/tmp/labfy-session-v2-TEST_ONLY-0p63m0if
stable-final-focus PASS
/tmp/labfy-session-v2-TEST_ONLY-vn3vcup_
late-refused PASS

View file

@ -0,0 +1,9 @@
/tmp/labfy-session-v2-TEST_ONLY-p4jqsmpt
ready PASS
accept PASS
cancel PASS
timeout PASS
absent PASS
still-locked PASS
different PASS
quickshell-restart-during-unlock PASS

View file

@ -0,0 +1,5 @@
guard-replacement PASS APPLICATION_MEMBERSHIP_CHANGED
guard-extra PASS APPLICATION_MEMBERSHIP_CHANGED
last-capture-layout PASS FINAL_TREE_MISMATCH
last-capture-swap PASS RECOVERY_ORDER_CHANGED
guard-finish-layout PASS FINAL_TREE_MISMATCH

View file

@ -0,0 +1,5 @@
pendant-guard SUCCÈS : quatre swaps, quatre slots, arbre exact, focus vérifié
apres-phase0 SUCCÈS : quatre swaps, quatre slots, arbre exact, focus vérifié
deux-frontieres SUCCÈS : quatre swaps, quatre slots, arbre exact, focus vérifié
autotiling-guard SUCCÈS : quatre swaps, quatre slots, arbre exact, focus vérifié
provider-unaire SUCCÈS : changement externe refusé avant IPC suivant

View file

@ -0,0 +1,7 @@
success PASS
failure PASS
invalid PASS
new PASS
claimed PASS
real PASS
deferred-real PASS

View file

@ -0,0 +1,10 @@
********* Start testing of qmltestrunner *********
Config: Using QtTest library 6.12.0, Qt 6.12.0 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 16.2.1 20260810), arch unknown
PASS : qmltestrunner::SessionExitGateV2::initTestCase()
PASS : qmltestrunner::SessionExitGateV2::test_backend_reason_is_visible_and_logout_stays_gated()
PASS : qmltestrunner::SessionExitGateV2::test_error_or_legacy_success_never_exits()
PASS : qmltestrunner::SessionExitGateV2::test_quit_without_saving_requires_explicit_call()
PASS : qmltestrunner::SessionExitGateV2::test_saved_releases_only_confirmed_action()
PASS : qmltestrunner::SessionExitGateV2::cleanupTestCase()
Totals: 6 passed, 0 failed, 0 skipped, 0 blacklisted, 5ms
********* Finished testing of qmltestrunner *********

View file

@ -0,0 +1,5 @@
...............
----------------------------------------------------------------------
Ran 15 tests in 20.330s
OK

View file

@ -0,0 +1,13 @@
{
"backend": 0,
"focus": 0,
"qml-unit": 0,
"qml-qt": 0,
"keyring-ui": 0,
"keyring-runtime": 0,
"scratchpad-runtime": 0,
"phase3b-ui": 0,
"p0-runtime": 0,
"path-runtime": 0,
"diff-check": 0
}

View file

@ -0,0 +1,2 @@
/tmp/labfy-session-v2-TEST_ONLY-1lfda8ji
PASS save/store/restore/show/hide/new-membership/all-hidden/cold-kitty/no-duplicate

View file

@ -0,0 +1,12 @@
/tmp/labfy-keyring-validation-ibq2bukh
backend 0
focus 0
qml-unit 0
qml-qt 0
keyring-ui 0
keyring-runtime 0
scratchpad-runtime 0
phase3b-ui 0
p0-runtime 0
path-runtime 0
diff-check 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,152 @@
test_absent_last_existing_directory (test_bootstrap.BootstrapTests.test_absent_last_existing_directory) ... ok
test_absent_no_creation (test_bootstrap.BootstrapTests.test_absent_no_creation) ... ok
test_allowlist_denied (test_bootstrap.BootstrapTests.test_allowlist_denied) ... ok
test_checkpoint_created_between_observations (test_bootstrap.BootstrapTests.test_checkpoint_created_between_observations) ... ok
test_empty_sessions_with_or_without_automatic_allows_without_creation (test_bootstrap.BootstrapTests.test_empty_sessions_with_or_without_automatic_allows_without_creation) ... ok
test_envelope_and_framing_rejected (test_bootstrap.BootstrapTests.test_envelope_and_framing_rejected) ... ok
test_home_fallback_matches_canonical_location (test_bootstrap.BootstrapTests.test_home_fallback_matches_canonical_location) ... ok
test_interleaved_operations_map_unchanged (test_bootstrap.BootstrapTests.test_interleaved_operations_map_unchanged) ... ok
test_last_with_sessions_absent_refuses (test_bootstrap.BootstrapTests.test_last_with_sessions_absent_refuses) ... ok
test_named_only_without_automatic_refuses (test_bootstrap.BootstrapTests.test_named_only_without_automatic_refuses) ... ok
test_os_error_is_not_absence (test_bootstrap.BootstrapTests.test_os_error_is_not_absence) ... ok
test_present_and_corrupt_always_refuse (test_bootstrap.BootstrapTests.test_present_and_corrupt_always_refuse) ... ok
test_sessions_any_entry_refuses_without_content_read (test_bootstrap.BootstrapTests.test_sessions_any_entry_refuses_without_content_read) ... ok
test_sessions_list_error_refuses (test_bootstrap.BootstrapTests.test_sessions_list_error_refuses) ... ok
test_sessions_unsafe_directory_refuses (test_bootstrap.BootstrapTests.test_sessions_unsafe_directory_refuses) ... ok
test_socket_authority_failure_refuses_absence (test_bootstrap.BootstrapTests.test_socket_authority_failure_refuses_absence) ... ok
test_state_symlink_and_relative_state_refuse (test_bootstrap.BootstrapTests.test_state_symlink_and_relative_state_refuse) ... ok
test_symlinks_and_special_last_refuse_without_open (test_bootstrap.BootstrapTests.test_symlinks_and_special_last_refuse_without_open) ... ok
test_unreadable_and_nonprivate_directory_refuse (test_bootstrap.BootstrapTests.test_unreadable_and_nonprivate_directory_refuse) ... ok
test_valid_store_checkpoint_and_unreadable_file_refuse (test_bootstrap.BootstrapTests.test_valid_store_checkpoint_and_unreadable_file_refuse) ... ok
test_equivalent_unary_paths_are_not_identity_changes (test_focus_recovery.FocusRecoveryTests.test_equivalent_unary_paths_are_not_identity_changes) ... ok
test_firefox_command_proven_then_kitty_late_conflict_is_preserved (test_focus_recovery.FocusRecoveryTests.test_firefox_command_proven_then_kitty_late_conflict_is_preserved) ... ok
test_interrupted_intention_preserves_all_evidence (test_focus_recovery.FocusRecoveryTests.test_interrupted_intention_preserves_all_evidence) ... ok
test_layout_change_with_identical_raw_paths_still_refuses (test_focus_recovery.FocusRecoveryTests.test_layout_change_with_identical_raw_paths_still_refuses) ... ok
test_path_evidence_limit_refuses_without_overwriting_history (test_focus_recovery.FocusRecoveryTests.test_path_evidence_limit_refuses_without_overwriting_history) ... ok
test_real_identity_order_workspace_and_branch_changes_still_refuse (test_focus_recovery.FocusRecoveryTests.test_real_identity_order_workspace_and_branch_changes_still_refuse) ... ok
test_prepare_quits_before_resume_and_finish_has_no_delayed_exit (test_guard_protocol.GuardProtocolTests.test_prepare_quits_before_resume_and_finish_has_no_delayed_exit) ... ok
test_resume_without_prepare_refuses_and_still_cleans_owned_helper (test_guard_protocol.GuardProtocolTests.test_resume_without_prepare_refuses_and_still_cleans_owned_helper) ... ok
test_actual_dbus_states (test_keyring.KeyringTests.test_actual_dbus_states) ... /usr/lib/python3.14/site-packages/gi/events.py:803: DeprecationWarning: 'asyncio.AbstractEventLoopPolicy' is deprecated and slated for removal in Python 3.16
AbstractEventLoopPolicy = getattr(asyncio, "AbstractEventLoopPolicy", object)
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
ok
test_apply_is_excluded_during_unlock (test_keyring.KeyringTests.test_apply_is_excluded_during_unlock) ... ok
test_apply_last_cannot_bypass_gate (test_keyring.KeyringTests.test_apply_last_cannot_bypass_gate) ... /usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
ok
test_ready_preserves_executor_result_and_recovery_required (test_keyring.KeyringTests.test_ready_preserves_executor_result_and_recovery_required) ... /usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
ok
test_remote_error_text_is_never_logged_or_persisted (test_keyring.KeyringTests.test_remote_error_text_is_never_logged_or_persisted) ... ok
test_transaction_prevents_prompt (test_keyring.KeyringTests.test_transaction_prevents_prompt) ... /usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
ok
test_two_processes_and_restart (test_keyring.KeyringTests.test_two_processes_and_restart) ... ok
test_unlock_never_restores (test_keyring.KeyringTests.test_unlock_never_restores) ... /usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
/usr/lib/python3.14/site-packages/gi/events.py:890: DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16
if not isinstance(_get_event_loop_policy(), GLibEventLoopPolicy):
ok
test_complete_refuses_full_identity_placement_and_pending (test_p0_boundaries.BoundaryTests.test_complete_refuses_full_identity_placement_and_pending) ... ok
test_extra_after_first_move_refuses_before_second_staging (test_p0_boundaries.BoundaryTests.test_extra_after_first_move_refuses_before_second_staging) ... ok
test_guard_replacement_and_extra_refuse_before_any_mutation (test_p0_boundaries.BoundaryTests.test_guard_replacement_and_extra_refuse_before_any_mutation) ... ok
test_last_provider_capture_cannot_hide_structure_or_identity_change (test_p0_boundaries.BoundaryTests.test_last_provider_capture_cannot_hide_structure_or_identity_change) ... ok
test_layout_change_in_second_split_focus_observation_refuses (test_p0_boundaries.BoundaryTests.test_layout_change_in_second_split_focus_observation_refuses) ... ok
test_unary_parent_normalization_preserves_canonical_layout (test_p0_boundaries.BoundaryTests.test_unary_parent_normalization_preserves_canonical_layout) ... ok
test_unchanged_final_observation_completes (test_p0_boundaries.BoundaryTests.test_unchanged_final_observation_completes) ... ok
test_changement_apres_capture_canonique_refuse_avant_deplacement (test_path_stability.PathTests.test_changement_apres_capture_canonique_refuse_avant_deplacement) ... ok
test_guard_refuse_les_divergences_avant_tout_ipc (test_path_stability.PathTests.test_guard_refuse_les_divergences_avant_tout_ipc) ... ok
test_ack_requires_verified_current_transaction (test_phase3b.DeploymentTests.test_ack_requires_verified_current_transaction) ... ok
test_limusic_never_launched (test_phase3b.DeploymentTests.test_limusic_never_launched) ... ok
test_async_mapping_completes_before_apply_and_focus_proof (test_prewarm.PrewarmTests.test_async_mapping_completes_before_apply_and_focus_proof) ... ok
test_deferred_marker_and_cli_preserve_cause_for_explicit_retry (test_prewarm.PrewarmTests.test_deferred_marker_and_cli_preserve_cause_for_explicit_retry) ... ok
test_existing_required_journal_refuses_before_launch (test_prewarm.PrewarmTests.test_existing_required_journal_refuses_before_launch) ... ok
test_focus_change_after_proof_refuses_without_publication (test_prewarm.PrewarmTests.test_focus_change_after_proof_refuses_without_publication) ... ok
test_intruder_extra_identity_session_and_owner_refuse_without_publication (test_prewarm.PrewarmTests.test_intruder_extra_identity_session_and_owner_refuse_without_publication) ... ok
test_launch_failure_preserves_clients_and_retry_does_not_duplicate (test_prewarm.PrewarmTests.test_launch_failure_preserves_clients_and_retry_does_not_duplicate) ... ok
test_missing_managed_autostart_refuses_before_launch (test_prewarm.PrewarmTests.test_missing_managed_autostart_refuses_before_launch) ... ok
test_missing_collision_and_live_publisher (test_provider.ProviderTests.test_missing_collision_and_live_publisher) ... ok
test_capture_is_deterministic_for_a_stable_tree (test_scratchpad.ScratchpadTests.test_capture_is_deterministic_for_a_stable_tree) ... ok
test_capture_store_reload_and_no_internal_workspace (test_scratchpad.ScratchpadTests.test_capture_store_reload_and_no_internal_workspace) ... ok
test_change_between_reads_and_ambiguous_focus_refuse (test_scratchpad.ScratchpadTests.test_change_between_reads_and_ambiguous_focus_refuse) ... ok
test_empty_focus_workspace_is_persisted (test_scratchpad.ScratchpadTests.test_empty_focus_workspace_is_persisted) ... ok
test_group_ancestor_is_not_admitted_by_leaf_identity (test_scratchpad.ScratchpadTests.test_group_ancestor_is_not_admitted_by_leaf_identity) ... ok
test_hidden_focus_is_rejected_before_command (test_scratchpad.ScratchpadTests.test_hidden_focus_is_rejected_before_command) ... ok
test_initial_scratchpad_add_predicts_source_workspace_focus (test_scratchpad.ScratchpadTests.test_initial_scratchpad_add_predicts_source_workspace_focus) ... ok
test_journal_detects_identity_output_scratchpad_and_interruption (test_scratchpad.ScratchpadTests.test_journal_detects_identity_output_scratchpad_and_interruption) ... ok
test_layer_shell_can_leave_no_sway_focus (test_scratchpad.ScratchpadTests.test_layer_shell_can_leave_no_sway_focus) ... ok
test_legacy_journal_not_reinterpreted (test_scratchpad.ScratchpadTests.test_legacy_journal_not_reinterpreted) ... ok
test_multiple_focused_nodes_are_still_refused (test_scratchpad.ScratchpadTests.test_multiple_focused_nodes_are_still_refused) ... ok
test_runtime_visibility_assignment_never_exposes_hidden_peer (test_scratchpad.ScratchpadTests.test_runtime_visibility_assignment_never_exposes_hidden_peer) ... ok
test_shown_and_multiple_hidden (test_scratchpad.ScratchpadTests.test_shown_and_multiple_hidden) ... ok
test_strict_versions_and_scratchpad_references (test_scratchpad.ScratchpadTests.test_strict_versions_and_scratchpad_references) ... ok
test_checkpoint_failed_validation_preserves_old (test_unit.BackendTests.test_checkpoint_failed_validation_preserves_old) ... ok
test_checkpoint_permissions (test_unit.BackendTests.test_checkpoint_permissions) ... ok
test_cli_execute_required (test_unit.BackendTests.test_cli_execute_required) ... ok
test_cli_storage_checkpoint_and_startup_mock (test_unit.BackendTests.test_cli_storage_checkpoint_and_startup_mock) ... ok
test_coexistence_and_migration (test_unit.BackendTests.test_coexistence_and_migration) ... ok
test_command_injection (test_unit.BackendTests.test_command_injection) ... ok
test_counts (test_unit.BackendTests.test_counts) ... ok
test_exact_inverted (test_unit.BackendTests.test_exact_inverted) ... ok
test_interchangeable_best_effort (test_unit.BackendTests.test_interchangeable_best_effort) ... ok
test_intermediate_directory_permissions (test_unit.BackendTests.test_intermediate_directory_permissions) ... ok
test_json_schema_matches_fixture (test_unit.BackendTests.test_json_schema_matches_fixture) ... ok
test_json_strict (test_unit.BackendTests.test_json_strict) ... ok
test_launch_policy (test_unit.BackendTests.test_launch_policy) ... ok
test_lock (test_unit.BackendTests.test_lock) ... ok
test_multi_output_preflight_synthetic (test_unit.BackendTests.test_multi_output_preflight_synthetic) ... ok
test_native_strict (test_unit.BackendTests.test_native_strict) ... ok
test_paths_links (test_unit.BackendTests.test_paths_links) ... ok
test_plan_pure (test_unit.BackendTests.test_plan_pure) ... ok
test_references (test_unit.BackendTests.test_references) ... ok
test_report_rejects_private_fields_and_bool_version (test_unit.BackendTests.test_report_rejects_private_fields_and_bool_version) ... ok
test_resume_attempts_all_owned_helpers (test_unit.BackendTests.test_resume_attempts_all_owned_helpers) ... ok
test_schema_exact (test_unit.BackendTests.test_schema_exact) ... ok
test_types_version_and_identity (test_unit.BackendTests.test_types_version_and_identity) ... ok
test_unsupported_states (test_unit.BackendTests.test_unsupported_states) ... ok
----------------------------------------------------------------------
Ran 93 tests in 8.405s
OK

View file

@ -0,0 +1 @@
Full shell private-compositor load: PASS /tmp/labfy-session-v2-TEST_ONLY-vx5tlv5i

View file

@ -0,0 +1,11 @@
test_inheritance_from_a_private_test_theme (test_shared.Icons.test_inheritance_from_a_private_test_theme) ... ok
test_tela_desktop_icon_and_absolute (test_shared.Icons.test_tela_desktop_icon_and_absolute) ... ok
test_nine_move_missing_anchor_and_empty (test_shared.Persistence.test_nine_move_missing_anchor_and_empty) ... ok
test_real_concurrent_processes_keep_all_additions_during_moves (test_shared.Persistence.test_real_concurrent_processes_keep_all_additions_during_moves) ... ok
test_toggle_is_atomic_and_does_not_depend_on_a_stale_ui_list (test_shared.Persistence.test_toggle_is_atomic_and_does_not_depend_on_a_stale_ui_list) ... ok
test_wayland_xwayland_scratchpad_and_explicit_focus (test_shared.Sway.test_wayland_xwayland_scratchpad_and_explicit_focus) ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.735s
OK

View file

@ -0,0 +1,39 @@
{
"status": "PASS",
"base": "d4e2a0013e7a9452add80dddc110e036ebe4ee96",
"backendTests": {
"applications": 7,
"dock": 6,
"sessionV2": 93
},
"javascript": [
"ApplicationsModel: PASS",
"Dock state and window models: PASS"
],
"graphical": [
"private Sway dock scenario",
"complete shell load",
"Session V2 and Keyring with dock"
],
"keyring": {
"backend": 0,
"focus": 0,
"qml-unit": 0,
"qml-qt": 0,
"keyring-ui": 0,
"keyring-runtime": 0,
"scratchpad-runtime": 0,
"phase3b-ui": 0,
"p0-runtime": 0,
"path-runtime": 0,
"diff-check": 0
},
"finalChecks": [
"git diff --check: PASS",
"git apply --check against unchanged main checkout: PASS",
"no Binding loop, assignment, ReferenceError or TypeError in final dock lab logs",
"main checkout git status --short: empty"
],
"finalShellLab": "/tmp/labfy-session-v2-TEST_ONLY-ywdppzn5",
"activeSessionModified": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -17,5 +17,9 @@ function next(state, event, input) {
}
function influence(distance) {
return Math.max(0, 1 - Math.abs(distance) / 115);
// CONTRACT: 57 px resting pitch; a radius below 1.5 pitches affects
// at most the nearest slot and its immediate neighbours. Smoothstep has
// zero slope at both ends, avoiding a kink as an icon enters the halo.
const t = Math.max(0, 1 - Math.abs(distance) / 76);
return t * t * (3 - 2 * t);
}

View file

@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import "../applications"
@ -21,7 +22,7 @@ PanelWindow {
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "labfy-sway-dock"
color: "transparent"
implicitWidth: Math.max(96, Math.min(screen.width - 24, items.width + 28))
implicitWidth: Math.max(96, Math.min(screen.width - 24, items.width + 52))
implicitHeight: 360
property string phase: "hidden"
readonly property bool revealed: phase === "shown" || phase === "waiting" || phase === "held"
@ -96,27 +97,33 @@ PanelWindow {
onHoveredChanged: { dock.reconcile(); if (!hovered) dock.pointerX = -1000; }
onPointChanged: dock.pointerX = point.position.x
}
Rectangle {
// Discreet local shadow, no screen capture or blur sampling.
x: 2; y: 34; width: parent.width - 4; height: 70
radius: 26; color: Theme.shadow
// WHY: an analytic shadow needs neither a captured backdrop nor a
// moving blur source. Its geometry stays fixed throughout hover.
RectangularShadow {
x: 16; y: 43; width: parent.width - 32; height: 48
radius: 20; blur: 24; spread: 2
color: Qt.rgba(0, 0, 0, 0.32)
}
Rectangle {
x: 0; y: 29; width: parent.width; height: 70
radius: 25
color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, Theme.highContrast ? 1 : 0.89)
border.color: Theme.strongBorder; border.width: 1
x: 14; y: 36; width: parent.width - 28; height: 58
radius: 20
color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, Theme.highContrast ? 0.94 : 0.16)
border.color: Qt.rgba(Theme.text.r, Theme.text.g, Theme.text.b, Theme.highContrast ? 0.5 : 0.09)
border.width: 1
}
Flickable {
id: viewport
x: 12; y: 0; width: parent.width - 24; height: 105
contentWidth: items.width; contentHeight: height
// CONTRACT: reserve visual overflow inside the clipped viewport,
// including at both scroll limits, for the 1.39x end icons.
contentWidth: items.width + 24; contentHeight: height
clip: true
interactive: dock.draggingId === ""
boundsBehavior: Flickable.StopAtBounds
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded }
Row {
id: items
x: 12
height: viewport.height
spacing: 3
move: Transition { NumberAnimation { properties: "x"; duration: 110; easing.type: Easing.OutCubic } }
@ -128,29 +135,32 @@ PanelWindow {
required property var modelData
required property int index
readonly property bool separator: index === dock.pinnedCount && dock.pinnedCount > 0
readonly property real center: viewport.x + x + width / 2 - viewport.contentX
// WHY: measuring against the animated x/width feeds
// scale back into its own binding. Stable resting slots
// keep magnification deterministic and free of loops.
readonly property real baseCenter: viewport.x + index * 57 + 27
readonly property real center: viewport.x + items.x + x + width / 2 - viewport.contentX
// INVARIANT: pointer influence uses fixed layout slots.
// Only the visual transform changes: Row, hit targets,
// window width and SVG raster size never follow hover.
readonly property real baseCenter: viewport.x + items.x + index * 57 + 27
+ (index >= dock.pinnedCount && dock.pinnedCount > 0 ? 13 : 0) - viewport.contentX
readonly property real growth: dock.revealed && bodyHover.hovered
? AutoHide.influence(baseCenter - dock.pointerX) : 0
width: 54 + growth * 19 + (separator ? 13 : 0)
width: 54 + (separator ? 13 : 0)
height: 105
Behavior on width { NumberAnimation { duration: 95; easing.type: Easing.OutCubic } }
Rectangle {
visible: tile.separator
x: 2; y: 45; width: 1; height: 32; color: Theme.strongBorder
}
ApplicationIcon {
id: icon
width: 46 + tile.growth * 21; height: width
// WHY: ApplicationIcon derives sourceSize from width.
// Fixed dimensions avoid async SVG reload/fallback flashes.
width: 46; height: 46
x: (parent.width - width + (tile.separator ? 12 : 0)) / 2
y: 83 - height
y: 37
transformOrigin: Item.Bottom
scale: 1 + tile.growth * 0.39
Behavior on scale { NumberAnimation { duration: 80; easing.type: Easing.OutCubic } }
iconSource: tile.modelData.iconSource || ""
opacity: dock.draggingId === tile.modelData.id ? 0.5 : 1
Behavior on width { NumberAnimation { duration: 95; easing.type: Easing.OutCubic } }
}
Rectangle {
anchors.horizontalCenter: icon.horizontalCenter

View file

@ -27,10 +27,11 @@ layer_effects "labfy-sway-bar" {
corner_radius 4
}
# CONTRACT: only the dock's layer surface receives blur; no window rule or
# focus/restore policy is changed. Its transparent input strip stays clear.
# CONTRACT: dock glass uses a light tint and analytic QML shadow. Disabling
# backdrop blur avoids compositor blur work on every magnification frame.
# No window rule or focus/restore policy is changed.
layer_effects "labfy-sway-dock" {
blur $bar_blur
blur disable
blur_xray disable
blur_ignore_transparent enable
shadows disable

View file

@ -13,7 +13,7 @@ from headless import Lab
from ipc_proxy import IpcProxy
SOURCE = REPO / 'quickshell/.config/quickshell/labfy-sway'
ARTIFACTS = REPO / 'docs/validation/quickshell-dock-tela'
ARTIFACTS = REPO / 'docs/validation/quickshell-dock-tela-polish-v2'
NINE = ['firefox.desktop', 'kitty.desktop', 'yazi.desktop', 'nvim.desktop',
'org.darktable.darktable.desktop', 'orca-slicer.desktop', 'org.freecad.FreeCAD.desktop',
'org.kde.digikam.desktop', 'AppFlowy.desktop']
@ -23,8 +23,8 @@ class DockLab(Lab):
super().__init__()
self.config=self.directory/'dock-sway.conf'
base=(REPO/'tests/session_v2/fixtures/headless.conf').read_text()
effects=(REPO/'sway/.config/sway/swayfx').read_text().split('# CONTRACT: only the dock')[1]
effects=effects[effects.index('layer_effects'):]
effects=(REPO/'sway/.config/sway/swayfx').read_text().split('layer_effects "labfy-sway-dock"')[1]
effects='layer_effects "labfy-sway-dock"'+effects
self.config.write_text(base+'\noutput HEADLESS-1 bg #11111b solid_color\nset $bar_blur enable\n'+effects)
def start(self, argv):
if argv[0]==os.environ.get('LABFY_TEST_SWAY','sway'):
@ -49,6 +49,31 @@ ShellRoot {
D.Dock { id: dock; modelData: Quickshell.screens[0] }
PanelWindow { id: bar; implicitWidth: 100; implicitHeight: 30; anchors.top: true; color: "transparent" }
LazyLoader { id: menu; active: false; A.ApplicationsMenu { barWindow: bar } }
// TEST_ONLY: one pointer move per animation frame avoids measuring IPC
// process startup instead of continuous production hover. These are Qt
// animation intervals, not hardware presentation timestamps.
FrameAnimation {
id: sweepFrames
property var intervals: []
property real origin: 0
property bool geometryChanged: false
property bool imageChanged: false
property var baseline: []
running: false
onTriggered: {
if (currentFrame > 1) intervals.push(frameTime * 1000);
const cycle = (elapsedTime * 0.7) % 2;
input.mouseMove(dock.surfaceBody, 51 + 420 * (cycle <= 1 ? cycle : 2 - cycle), 70);
const tiles=Array.from(dock.contentItemRow.children).filter(t => t.modelData);
for (let i=0; i<tiles.length; ++i) {
const t=tiles[i], b=baseline[i];
if (t.x!==b.x || t.width!==b.width || t.children[1].width!==46 || dock.width!==origin)
geometryChanged=true;
if (t.children[1].children[0].status!==b.status) imageChanged=true;
}
if (elapsedTime >= 4.5) running=false;
}
}
TestCase { id: input; name: "DockInput"; when: false }
IpcHandler {
target: "dockTest"
@ -64,10 +89,30 @@ ShellRoot {
bodyY:dock.surfaceBody.y, width:dock.width, pointerX:dock.pointerX,
tileWidth:dock.contentItemRow.children[0] ? dock.contentItemRow.children[0].width : 0,
tiles:Array.from(dock.contentItemRow.children).filter(t => t.modelData).map(t => ({
x:t.x, width:t.width, growth:t.growth, scale:t.children[1].scale,
visualLeft:t.children[1].mapToItem(dock.contentItemRow.parent.parent,0,0).x,
visualRight:t.children[1].mapToItem(dock.contentItemRow.parent.parent,46,0).x,
iconWidth:t.children[1].width, imageStatus:t.children[1].children[0].status})),
context:!!dock.contextEntry});
}
function startFrames(): void {
sweepFrames.intervals=[]; sweepFrames.origin=dock.width;
sweepFrames.baseline=Array.from(dock.contentItemRow.children).filter(t => t.modelData)
.map(t => ({x:t.x,width:t.width,status:t.children[1].children[0].status}));
sweepFrames.start();
}
function frames(): string {
return JSON.stringify({running:sweepFrames.running, intervals:sweepFrames.intervals,
geometryChanged:sweepFrames.geometryChanged,imageChanged:sweepFrames.imageChanged});
}
function edge(): void { input.mouseMove(dock.edgeArea, dock.edgeArea.width/2, 2); }
function body(): void { input.mouseMove(dock.surfaceBody, 35, 70); }
function sweep(x:real): void { input.mouseMove(dock.surfaceBody, x, 70); }
function scrollEnd(): void {
const v=dock.contentItemRow.parent.parent; v.contentX=Math.max(0,v.contentWidth-v.width);
}
function scrollStart(): void { dock.contentItemRow.parent.parent.contentX=0; }
function leave(): void { input.mouseMove(dock.contentItem, 1, 1); }
function menuToggle(id:string): void { if (!menu.active) menu.active=true; menu.item.toggleFavorite(id); }
function dockToggle(id:string): void { A.FavoritesService.toggle(id); }
@ -120,8 +165,33 @@ ShellRoot {
capture('closed')
call('edge'); wait(lambda s:s['revealed']); time.sleep(.35)
capture('open')
resting=call('state')
call('body'); time.sleep(.3); zoom=call('state'); capture('zoom')
assert zoom['tileWidth']>54, zoom
lab.command('output HEADLESS-1 bg #aab4c8 solid_color')
time.sleep(.2); capture('zoom-light')
lab.command('output HEADLESS-1 bg #11111b solid_color')
assert zoom['tileWidth']==54 and zoom['tiles'][0]['scale']>1, zoom
assert zoom['tiles'][0]['visualLeft']>=0,zoom
# Exercise production hover across slot boundaries. Layout and image
# readiness must remain stable while visual transforms change.
samples=[]
for x in range(45, 480, 6):
call('sweep',x)
state=call('state')
assert state['phase']=='shown',state
assert state['width']==resting['width'],state
assert [(t['x'],t['width'],t['iconWidth']) for t in state['tiles']]==[(t['x'],t['width'],t['iconWidth']) for t in resting['tiles']],state
assert sum(t['growth']>0 for t in state['tiles'])<=3,state
assert [t['imageStatus'] for t in state['tiles']]==[t['imageStatus'] for t in resting['tiles']],state
samples.append({'pointerX':state['pointerX'],'scales':[t['scale'] for t in state['tiles']]})
call('startFrames')
time.sleep(4.8)
frames=call('frames')
assert not frames['running'] and len(frames['intervals'])>30,frames
assert not frames['geometryChanged'] and not frames['imageChanged'],frames
intervals=sorted(frames['intervals'])
timing={'frames':len(intervals),'meanMs':sum(intervals)/len(intervals),
'p95Ms':intervals[int((len(intervals)-1)*.95)],'maxMs':max(intervals)}
call('leave'); wait(lambda s:s['phase']=='hidden')
call('edge'); wait(lambda s:s['revealed']); time.sleep(.3)
call('rightClick'); wait(lambda s:s['phase']=='held'); time.sleep(.7)
@ -199,6 +269,16 @@ ShellRoot {
lab.command('output HEADLESS-1 mode 800x600')
resized=wait(lambda s:s['screenWidth']==800)
assert resized['width']<=776,resized
# Magnified end icons keep their visual margin at both scroll limits.
lab.command('output HEADLESS-1 mode 400x600')
wait(lambda s:s['screenWidth']==400)
call('edge'); wait(lambda s:s['revealed']); time.sleep(.3)
call('scrollEnd'); call('sweep',325); time.sleep(.15)
end=call('state')
assert end['tiles'][-1]['scale']>1 and end['tiles'][-1]['visualRight']<=end['width']-24,end
call('scrollStart'); call('sweep',51); time.sleep(.15)
start=call('state')
assert start['tiles'][0]['scale']>1 and start['tiles'][0]['visualLeft']>=0,start
lab.command('output HEADLESS-1 mode 1280x720')
wait(lambda s:s['screenWidth']==1280)
# Existing empty state must stay empty, including a bridge restart.
@ -208,9 +288,9 @@ ShellRoot {
process=lab.start(['quickshell','-p',str(qml)])
time.sleep(1)
assert wait(lambda s:s['ready'])['ids']==[]
(ARTIFACTS/'result.json').write_text(json.dumps(dict(status='PASS',initial=initial,
(ARTIFACTS/'result.json').write_text(json.dumps(dict(status='PASS',initial=initial,hoverSamples=samples,animationTiming=timing,frameSweep=frames,
reordered=reordered['ids'],checks=['shared favorites','menu and dock mutations','durable reorder',
'empty restart','autohide','context hold','kitty multi-window','floating scratchpad explicit focus',
'window close','no hover focus','real pointer drag','zoom growth','IPC reconnect','Firefox private profile','desktop install/remove monitoring','external writer','keyboard menu navigation','resolution change','SwayFX dock blur rule']),indent=2)+'\n')
'window close','no hover focus','real pointer drag','fixed layout transform hover', 'stable SVG readiness', 'lateral sweep 73 samples','IPC reconnect','Firefox private profile','desktop install/remove monitoring','external writer','keyboard menu navigation','resolution change','SwayFX dock blur disabled']),indent=2)+'\n')
proxy.close()
print('Dock headless: PASS',lab.directory)

View file

@ -24,8 +24,15 @@ input.held=false; assert.equal(event(),'waiting');
assert.equal(event('deadline'),'hidden');
input.held=true; event(); input.suppressed=true; assert.equal(event(),'hidden');
assert.equal(hide.influence(0),1);
assert.ok(hide.influence(55)>hide.influence(100));
assert.equal(hide.influence(115),0);
assert.ok(hide.influence(25)>hide.influence(55));
assert.equal(hide.influence(76),0);
assert.equal(hide.influence(-76),0);
assert.equal(hide.influence(-35),hide.influence(35));
for (let cursor=0; cursor<570; ++cursor) {
const affected=Array.from({length:10},(_,i)=>i).filter(i=>hide.influence(i*57-cursor)>0);
assert.ok(affected.length<=3);
assert.ok(affected.length===0 || affected.at(-1)-affected[0]<=2);
}
const model=load('applications/WindowModel.js');
const apps=[{id:'kitty.desktop',aliases:['kitty'],iconSource:'file:///tela/kitty.svg'},
{id:'org.app.desktop',aliases:['startup-class','executable']},