rework to having non-blocking screen cast prompts

This commit is contained in:
galister 2026-07-11 22:31:13 +09:00
parent 9f854d0649
commit f3dc87fa64
26 changed files with 1735 additions and 742 deletions

217
Cargo.lock generated
View File

@ -294,36 +294,6 @@ dependencies = [
"libloading 0.8.9",
]
[[package]]
name = "ashpd"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33a3c86f3fd70c0ffa500ed189abfa90b5a52398a45d5dc372fcc38ebeb7a645"
dependencies = [
"async-fs",
"async-net",
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.9.4",
"serde",
"serde_repr",
"url",
"zbus",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.5.0"
@ -431,17 +401,6 @@ dependencies = [
"rustix 1.1.4",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "async-signal"
version = "0.2.14"
@ -1501,6 +1460,8 @@ version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
dependencies = [
"futures-channel",
"futures-util",
"libc",
"libdbus-sys",
"windows-sys 0.61.2",
@ -1685,12 +1646,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enum-primitive-derive"
version = "0.2.2"
@ -1702,27 +1657,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "enumset"
version = "1.1.13"
@ -3196,15 +3130,6 @@ dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "miette"
version = "5.10.0"
@ -3916,16 +3841,6 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "ouroboros"
version = "0.18.5"
@ -5132,17 +5047,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
@ -6091,17 +5995,6 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "uds_windows"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
]
[[package]]
name = "uidev"
version = "0.1.0"
@ -6177,7 +6070,6 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
"serde_derive",
]
[[package]]
@ -7213,13 +7105,14 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
name = "wlx-capture"
version = "0.6.0"
dependencies = [
"ashpd",
"dbus",
"drm-fourcc",
"idmap",
"libc",
"log",
"pipewire",
"rxscreen",
"slotmap",
"smithay-client-toolkit 0.20.0",
"wayland-client",
"wayland-protocols",
@ -7422,67 +7315,6 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zbus"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"libc",
"ordered-stream",
"rustix 1.1.4",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.118",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
dependencies = [
"serde",
"winnow",
"zvariant",
]
[[package]]
name = "zeno"
version = "0.3.3"
@ -7592,44 +7424,3 @@ checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]
[[package]]
name = "zvariant"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
dependencies = [
"endi",
"enumflags2",
"serde",
"url",
"winnow",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.118",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.118",
"winnow",
]

View File

@ -16,6 +16,7 @@ members = [
anyhow = "1.0.100"
bytes = { version = "1.11.1" }
clap = { version = "4.5.53", features = ["derive"] }
dbus = { version = "0.9.12" }
glam = { version = "0.30.9", features = ["mint", "serde"] }
idmap = "0.2.2"
idmap-derive = "0.2.22"

View File

@ -28,6 +28,7 @@ wlx-common = { path = "../wlx-common" }
anyhow.workspace = true
clap.workspace = true
dbus.workspace = true
glam = { workspace = true, features = ["mint", "serde"] }
idmap = { workspace = true, features = ["serde"] }
idmap-derive.workspace = true
@ -44,41 +45,58 @@ vulkano.workspace = true
vulkano-shaders.workspace = true
xdg.workspace = true
ash = "^0.38.0" # must match vulkano
ash = "^0.38.0" # must match vulkano
bytes = { workspace = true }
chrono = { version = "0.4.42", features = ["unstable-locales"] }
chrono-tz = "0.10.4"
config = "0.15.19"
dbus = { version = "0.9.9" }
futures = "0.3.31"
image_dds = { version = "0.7.2", default-features = false, features = [
"ddsfile",
] }
image_dds = {
version = "0.7.2",
default-features = false,
features = [
"ddsfile",
]
}
input-linux = "0.7.1"
interprocess = { version = "2.2.3" }
json = { version = "0.12.4", optional = true }
json5 = "1.3.0"
libc = "0.2.178"
libmonado = { git = "https://github.com/wayvr-org/libmonado-rs.git", rev = "6f66b26930c24a8a2fc57ddcd85704784894c750", optional = true }
libmonado = {
git = "https://github.com/wayvr-org/libmonado-rs.git",
rev = "6f66b26930c24a8a2fc57ddcd85704784894c750",
optional = true
}
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
mint = "0.5.9"
openxr = { version = "0.21.0", features = ["linked", "mint"], optional = true }
ovr_overlay = { git = "https://github.com/galister/ovr_overlay_oyasumi", rev = "e477bd2a9e04293ea68c1e7529ef2cb131f32acc", features = [
"ovr_input",
"ovr_system",
], optional = true }
ovr_overlay = {
git = "https://github.com/galister/ovr_overlay_oyasumi",
rev = "e477bd2a9e04293ea68c1e7529ef2cb131f32acc",
features = [
"ovr_input",
"ovr_system",
],
optional = true
}
prost = { version = "0.14.3", optional = true }
pure-rust-locales = "0.8.2"
rosc = { version = "0.11.4", optional = true }
serde_yaml = "0.9.34"
signal-hook = "0.3.18"
smallvec = "1.15.1"
smithay = { version = "0.7.0", default-features = false, features = [
"backend_vulkan",
"desktop",
"wayland_frontend",
"xwayland",
] }
smithay = {
version = "0.7.0",
default-features = false,
features = [
"backend_vulkan",
"desktop",
"wayland_frontend",
"xwayland",
]
}
smithay-clipboard = "0.7.3"
smol = { workspace = true }
sysinfo = { version = "0.37" }
thiserror = "2.0"
@ -88,19 +106,23 @@ uuid = { workspace = true }
wayland-client = { workspace = true }
wayland-protocols = { workspace = true }
wayland-protocols-misc = { workspace = true }
winit = { version = "0.30.12", optional = true }
xcb = { version = "1.6.0", features = [
"as-raw-xcb-connection",
], optional = true }
xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros
whisper-rs = { version = "0.16.0", features = ["vulkan"], optional = true }
smithay-clipboard = "0.7.3"
winit = { version = "0.30.12", optional = true }
xcb = {
version = "1.6.0",
features = [
"as-raw-xcb-connection",
],
optional = true
}
xkbcommon = {
version = "0.8.0"
} # 0.9.0 breaks keymap import on some distros
calloop = { version = "0.14", optional = true }
calloop-wayland-source = { version = "0.4", optional = true }
[build-dependencies]
regex.workspace = true
[features]
@ -113,6 +135,11 @@ osc = ["dep:rosc"]
pipewire = ["wlx-capture/pipewire"]
uidev = ["dep:winit"]
wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"]
whisper = ["dep:pipewire", "dep:whisper-rs", "dep:calloop", "dep:calloop-wayland-source"]
whisper = [
"dep:calloop",
"dep:calloop-wayland-source",
"dep:pipewire",
"dep:whisper-rs"
]
x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"]
xcb = ["dep:xcb"]

View File

@ -0,0 +1,18 @@
<layout>
<elements>
<rectangle width="1280" height="720" padding="10" align_items="center" justify_content="space_evenly" flex_direction="column" color="#000000" color2="#200000" border="2" border_color="#ff0000" gradient="radial">
<div width="100%" align_items="center">
<label width="100%" id="status" text="" size="36" align="center" />
</div>
<div width="100%" align_items="center" flex_direction="column" gap="16">
<div width="100%" align_items="center">
<label width="100%" text="~name" size="48" align="center" />
</div>
<div width="100%" align_items="center">
<label width="100%" text="~description" size="36" align="center" wrap="1" />
</div>
</div>
</rectangle>
</elements>
</layout>

View File

@ -113,7 +113,8 @@
"ONE_CONTROLLER_ON_FLOOR": "Lege einen Controller auf den Boden!",
"CANNOT_ADD_SET": "Satz kann nicht hinzugefügt werden!",
"MAXIMUM_SETS_REACHED": "Maximale Anzahl an Sets erreicht.",
"FIXING_FLOOR_IN_X_SECS": "Boden wird in {SECONDS} Sekunde(n) repariert..."
"FIXING_FLOOR_IN_X_SECS": "Boden wird in {SECONDS} Sekunde(n) repariert...",
"TITLE_SCREENCAST_FAIL": "Bildschirmübertragungsquelle konnte nicht gestartet werden!"
},
"DONE": "Erledigt",
"WHISPER": {
@ -127,5 +128,12 @@
"INITIAL_TEXT": "Halten Sie die Mikrofontaste gedrückt, um mit der Transkription zu beginnen...",
"INIT_ERROR": "Fehler beim Starten der Whisper-Engine",
"ENTER_KEY": "Enter-Taste"
},
"SCREENCAST": {
"PENDING": "Bitte warten...",
"QUEUED": "Warten auf die Bestätigung des Screen-Cast-Pop-ups eines anderen Overlays.",
"WAITING_FOR_USER_DESC": "Überprüfen Sie Ihren Desktop auf ein Pop-up zur Bildschirmübertragung.\nWählen Sie Folgendes aus:",
"WAITING_FOR_USER": "Überprüfen Sie Ihren Desktop auf ein Pop-up zur Bildschirmübertragung.",
"FAIL": "Bildschirmübertragungsquelle konnte nicht initialisiert werden."
}
}

View File

@ -89,7 +89,15 @@
"GRABBING_WATCH": "To swap hands, move the watch in front and grab it with the other hand.",
"UNRESTRICTED_MOVEMENT": "Unrestricted movement"
},
"SCREENCAST": {
"PENDING": "Please wait...",
"QUEUED": "Waiting for a different overlay's screen cast pop-up to be approved.",
"WAITING_FOR_USER_DESC": "Check your dekstop for a screen cast pop-up.\nSelect the following:",
"WAITING_FOR_USER": "Check your desktop for a screen cast pop-up.",
"FAIL": "Could not initialize screen cast source."
},
"TOAST": {
"TITLE_SCREENCAST_FAIL": "Could not start screen cast source!",
"CANNOT_ADD_SET": "Cannot add set!",
"CANNOT_REMOVE_SET": "Cannot remove set!",
"DEFAULT_TITLE": "Notification",

View File

@ -113,7 +113,8 @@
"ONE_CONTROLLER_ON_FLOOR": "¡Coloca un mando en el suelo!",
"CANNOT_ADD_SET": "¡No se puede agregar el conjunto!",
"MAXIMUM_SETS_REACHED": "Se ha alcanzado el número máximo de sets.",
"FIXING_FLOOR_IN_X_SECS": "Corrigiendo el suelo en {SECONDS} segundo(s)..."
"FIXING_FLOOR_IN_X_SECS": "Corrigiendo el suelo en {SECONDS} segundo(s)...",
"TITLE_SCREENCAST_FAIL": "¡No se pudo iniciar la fuente de transmisión de pantalla!"
},
"DONE": "Hecho",
"WHISPER": {
@ -127,5 +128,12 @@
"INITIAL_TEXT": "Mantén presionado el botón del micrófono para comenzar a transcribir...",
"INIT_ERROR": "Error al iniciar el motor de Whisper",
"ENTER_KEY": "Tecla Enter"
},
"SCREENCAST": {
"PENDING": "Por favor, espere...",
"QUEUED": "Esperando a que se apruebe la ventana emergente de la transmisión de pantalla de otra superposición.",
"WAITING_FOR_USER_DESC": "Busca la ventana emergente de transmisión de pantalla en tu escritorio.\nSelecciona lo siguiente:",
"WAITING_FOR_USER": "Revisa tu escritorio para ver la ventana emergente de transmisión de pantalla.",
"FAIL": "No se pudo inicializar la fuente de transmisión de pantalla."
}
}

View File

@ -99,7 +99,8 @@
"EMPTY_SET": "Set vuoto!",
"LETS_ADD_OVERLAYS": "Aggiungiamo degli overlay dal Watch!",
"ONE_CONTROLLER_ON_FLOOR": "Posiziona un controller a terra!",
"FIXING_FLOOR_IN_X_SECS": "Correzione del pavimento in {SECONDS} secondo/i..."
"FIXING_FLOOR_IN_X_SECS": "Correzione del pavimento in {SECONDS} secondo/i...",
"TITLE_SCREENCAST_FAIL": "Impossibile avviare la sorgente dello screen cast!"
},
"WATCH": {
"ADD_NEW_SET": "Aggiungi un nuovo set",
@ -125,5 +126,12 @@
"INITIAL_TEXT": "Tieni premuto il tasto del microfono per iniziare la trascrizione...",
"INIT_ERROR": "Errore durante l'avvio del motore Whisper",
"ENTER_KEY": "Tasto Invio"
},
"SCREENCAST": {
"PENDING": "Attendi prego...",
"QUEUED": "In attesa dell'approvazione del pop-up di screen cast di un altro overlay.",
"WAITING_FOR_USER_DESC": "Controlla il desktop per un pop-up di condivisione schermo.\nSeleziona quanto segue:",
"WAITING_FOR_USER": "Controlla il desktop per un pop-up di condivisione schermo.",
"FAIL": "Impossibile inizializzare la sorgente di trasmissione dello schermo."
}
}

View File

@ -111,7 +111,8 @@
"ONE_CONTROLLER_ON_FLOOR": "コントローラーを床に置いてください!",
"CANNOT_ADD_SET": "セットを追加できません!",
"MAXIMUM_SETS_REACHED": "最大セット数に達しました。",
"FIXING_FLOOR_IN_X_SECS": "{SECONDS}秒で床を固定中..."
"FIXING_FLOOR_IN_X_SECS": "{SECONDS}秒で床を固定中...",
"TITLE_SCREENCAST_FAIL": "スクリーンキャストのソースを開始できませんでした!"
},
"DONE": "完了",
"WHISPER": {
@ -125,5 +126,12 @@
"INITIAL_TEXT": "マイクボタンを押し続けて文字起こしを開始してください...",
"INIT_ERROR": "Whisperエンジンの起動中にエラーが発生しました",
"ENTER_KEY": "Enterキー"
},
"SCREENCAST": {
"PENDING": "少々お待ちください...",
"QUEUED": "別のオーバーレイのスクリーンキャスト許可ポップアップの応答を待っています。",
"WAITING_FOR_USER_DESC": "デスクトップのスクリーンキャストのポップアップを確認してください。\n以下を選択してください",
"WAITING_FOR_USER": "デスクトップにスクリーンキャストのポップアップが表示されていないか確認してください。",
"FAIL": "スクリーンキャストのソースを初期化できませんでした。"
}
}
}

View File

@ -111,7 +111,8 @@
"ONE_CONTROLLER_ON_FLOOR": "Umieść jeden kontroler na podłodze!",
"CANNOT_ADD_SET": "Nie można dodać zestawu!",
"MAXIMUM_SETS_REACHED": "Osiągnięto maksymalną liczbę zestawów.",
"FIXING_FLOOR_IN_X_SECS": "Naprawianie podłogi za {SECONDS} sekund(y)..."
"FIXING_FLOOR_IN_X_SECS": "Naprawianie podłogi za {SECONDS} sekund(y)...",
"TITLE_SCREENCAST_FAIL": "Nie udało się uruchomić źródła przesyłania obrazu z ekranu!"
},
"DONE": "Gotowe",
"WHISPER": {
@ -125,5 +126,12 @@
"INITIAL_TEXT": "Naciśnij i przytrzymaj przycisk mikrofonu, aby rozpocząć transkrypcję...",
"INIT_ERROR": "Błąd podczas uruchamiania silnika Whisper",
"ENTER_KEY": "Klawisz Enter"
},
"SCREENCAST": {
"PENDING": "Proszę czekać...",
"QUEUED": "Oczekiwanie na zatwierdzenie okna wysyłania obrazu z innego nakładki.",
"WAITING_FOR_USER_DESC": "Sprawdź na pulpicie okno wyboru ekranu do transmitowania.\nWybierz następujące:",
"WAITING_FOR_USER": "Sprawdź na pulpicie powiadomienie o przesyłaniu ekranu.",
"FAIL": "Nie udało się zainicjować źródła przesyłania obrazu z ekranu."
}
}

View File

@ -99,7 +99,8 @@
"EMPTY_SET": "空集合!",
"LETS_ADD_OVERLAYS": "让我们从手表添加一些覆盖层吧!",
"ONE_CONTROLLER_ON_FLOOR": "请将一个控制器放在地面上!",
"FIXING_FLOOR_IN_X_SECS": "{SECONDS}秒内固定地面..."
"FIXING_FLOOR_IN_X_SECS": "{SECONDS}秒内固定地面...",
"TITLE_SCREENCAST_FAIL": "无法启动屏幕投影源!"
},
"WATCH": {
"ADD_NEW_SET": "添加新集合",
@ -125,5 +126,12 @@
"INITIAL_TEXT": "按住麦克风按钮开始转录...",
"INIT_ERROR": "启动 Whisper 引擎时出错",
"ENTER_KEY": "回车键"
},
"SCREENCAST": {
"PENDING": "请稍候...",
"QUEUED": "等待另一个覆盖层的屏幕投射确认弹窗。",
"WAITING_FOR_USER_DESC": "请检查桌面上的屏幕投射弹出窗口。\n选择以下选项",
"WAITING_FOR_USER": "请检查桌面上的屏幕投射弹出窗口。",
"FAIL": "无法初始化屏幕镜像源。"
}
}

View File

@ -87,7 +87,13 @@ impl OverlayWindowData<OpenXrOverlayData> {
smallvec![(swapchain.get_subimage(0), EyeVisibility::BOTH),]
};
let transform = state.transform * self.config.backend.frame_meta().unwrap().transform; // contract
let transform = state.transform
* self
.config
.backend
.frame_meta()
.map(|x| x.transform)
.unwrap_or_default(); // contract
let aspect_ratio = swapchain.extent[0] as f32 / swapchain.extent[1] as f32;
let (scale_x, scale_y) = if aspect_ratio < 1.0 {

View File

@ -539,10 +539,9 @@ pub(super) fn setup_custom_button<S: 'static>(
OverlaySelector::Name(name.clone()),
SpawnPos::Spread,
Box::new(move |app| {
Some(crate::overlays::screen::mirror::new_mirror(
name,
&app.session,
))
crate::overlays::screen::mirror::new_mirror(name, app)
.log_err("Could not create mirror")
.ok()
}),
)));
Ok(EventResult::Consumed)

View File

@ -14,7 +14,7 @@ use button::setup_custom_button;
use glam::{Affine2, Vec2, vec2};
use idmap::IdMap;
use label::setup_custom_label;
use std::{cell::RefCell, rc::Rc};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use wgui::{
assets::AssetPath,
components::{
@ -95,6 +95,7 @@ pub struct NewGuiPanelParams<S> {
pub resize_to_parent: bool,
pub external_xml: bool,
pub gui_scale: f32,
pub extra_vars: HashMap<Rc<str>, Rc<str>>,
}
impl<S> Default for NewGuiPanelParams<S> {
@ -105,6 +106,7 @@ impl<S> Default for NewGuiPanelParams<S> {
resize_to_parent: false,
external_xml: false,
gui_scale: 1.0,
extra_vars: HashMap::default(),
}
}
}
@ -134,6 +136,7 @@ impl<S: 'static> GuiPanel<S> {
},
extra: wgui::parser::ParseDocumentExtra {
on_custom_attribs: Some(on_custom_attrib_inner.clone()),
extra_vars: params.extra_vars,
..Default::default()
},
};

View File

@ -59,7 +59,6 @@ pub struct ScreenBackend {
stereo_adjust_mouse: bool,
pub(super) logical_pos: Vec2,
pub(super) logical_size: Vec2,
pub(super) mouse_transform_original: Transform,
mouse_transform_override: MouseTransform,
just_resumed: bool,
}
@ -89,13 +88,12 @@ impl ScreenBackend {
stereo_adjust_mouse: false,
logical_pos: Vec2::ZERO,
logical_size: Vec2::ZERO,
mouse_transform_original: Transform::Undefined,
mouse_transform_override: MouseTransform::Default,
just_resumed: false,
}
}
pub(super) fn apply_mouse_transform_with_override(&mut self, override_transform: Transform) {
pub(super) fn apply_mouse_transform_with_override(&mut self, transform: Transform) {
let mut size = self.logical_size;
let pos = self.logical_pos;
@ -109,11 +107,6 @@ impl ScreenBackend {
}
}
let transform = match override_transform {
Transform::Undefined => self.mouse_transform_original,
other => other,
};
self.mouse_transform = match transform {
Transform::Normal | Transform::Undefined => {
Affine2::from_cols(vec2(size.x, 0.), vec2(0., size.y), pos)

View File

@ -1,164 +1,59 @@
use std::{
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
task::{Context, Poll},
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use futures::{Future, FutureExt};
use glam::{Affine2, Affine3A, Quat, Vec3, vec3};
use wlx_capture::{
WlxCapture,
pipewire::{PipewireCapture, PipewireSelectScreenResult, pipewire_select_screen},
};
use glam::{Affine2, Affine3A, Quat, Vec2, Vec3, vec3};
use wlx_capture::{WlxCapture, pipewire::ScreenCastParams};
use wlx_common::{
overlays::{BackendAttrib, BackendAttribValue},
windowing::OverlayWindowState,
};
use crate::{
backend::{
input::{HoverResult, PointerHit},
task::{OverlayTask, TaskType, ToggleMode},
},
backend::input::{HoverResult, PointerHit},
overlays::screen::{
backend::CaptureType,
capture::{MainThreadWlxCapture, new_wlx_capture},
capture::{WlxCaptureIn, WlxCaptureOut},
pw::ScreenCastBackend,
},
state::{AppSession, AppState},
state::AppState,
subsystem::hid::WheelDelta,
windowing::{
OverlaySelector,
backend::{
FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender,
ui_transform,
},
backend::{FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender},
window::{OverlayCategory, OverlayWindowConfig},
},
};
use super::backend::ScreenBackend;
type PinnedSelectorFuture = core::pin::Pin<
Box<dyn Future<Output = Result<PipewireSelectScreenResult, wlx_capture::pipewire::AshpdError>>>,
>;
static MIRROR_COUNTER: AtomicUsize = AtomicUsize::new(1);
pub struct MirrorBackend {
name: Arc<str>,
renderer: Option<ScreenBackend>,
selector: Option<PinnedSelectorFuture>,
last_extent: [u32; 2],
interaction_transform: Option<Affine2>,
}
impl MirrorBackend {
pub fn new(name: Arc<str>) -> Self {
let selector = Box::pin(pipewire_select_screen(None, false, false, false, false));
Self {
name,
renderer: None,
selector: Some(selector),
last_extent: [0; 2],
interaction_transform: None,
}
}
}
pub struct MirrorBackend(ScreenBackend);
impl OverlayBackend for MirrorBackend {
fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
if let Some(mut selector) = self.selector.take() {
let maybe_pw_result = match selector
.poll_unpin(&mut Context::from_waker(futures::task::noop_waker_ref()))
{
Poll::Ready(result) => result,
Poll::Pending => {
self.selector = Some(selector);
return Ok(ShouldRender::Unable);
}
};
match maybe_pw_result {
Ok(pw_result) => {
log::debug!(
"{}: PipeWire result streams: {:?}",
self.name,
&pw_result.streams
);
let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
log::info!("{}: PipeWire node selected: {}", self.name, node_id);
let capture = new_wlx_capture!(
app.gfx_extras.queue_capture,
PipewireCapture::new(self.name.clone(), node_id)
);
self.renderer = Some(ScreenBackend::new_raw(
self.name.clone(),
app.xr_backend,
CaptureType::PipeWire,
capture,
));
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::ToggleOverlay(
OverlaySelector::Name(self.name.clone()),
ToggleMode::EnsureOn,
)));
}
Err(e) => {
log::warn!("Failed to create mirror due to PipeWire error: {e:?}");
self.renderer = None;
// drop self
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::Drop(OverlaySelector::Name(
self.name.clone(),
))));
}
}
}
self.renderer
.as_mut()
.map_or(Ok(ShouldRender::Unable), |r| r.should_render(app))
self.0.should_render(app)
}
fn render(&mut self, app: &mut AppState, rdr: &mut RenderResources) -> anyhow::Result<()> {
let Some(renderer) = self.renderer.as_mut() else {
anyhow::bail!("render failed after should_render passed");
};
renderer.render(app, rdr)?;
if let Some(meta) = renderer.frame_meta() {
let extent = meta.extent;
if self.last_extent != extent {
self.last_extent = extent;
self.interaction_transform = Some(ui_transform(extent));
}
}
self.0.render(app, rdr)?;
Ok(())
}
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> {
if let Some(renderer) = self.renderer.as_mut() {
renderer.pause(app)?;
}
Ok(())
self.0.pause(app)
}
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> {
if let Some(renderer) = self.renderer.as_mut() {
renderer.resume(app)?;
}
Ok(())
self.0.resume(app)
}
fn frame_meta(&mut self) -> Option<FrameMeta> {
self.renderer.as_mut().and_then(ScreenBackend::frame_meta)
self.0.frame_meta()
}
fn notify(&mut self, app: &mut AppState, event_data: OverlayEventData) -> anyhow::Result<()> {
let Some(renderer) = self.renderer.as_mut() else {
return Ok(());
};
renderer.notify(app, event_data)
self.0.notify(app, event_data)
}
fn on_hover(&mut self, _: &mut AppState, _: &PointerHit) -> HoverResult {
@ -168,21 +63,13 @@ impl OverlayBackend for MirrorBackend {
fn on_pointer(&mut self, _: &mut AppState, _: &PointerHit, _: bool) {}
fn on_scroll(&mut self, _: &mut AppState, _: &PointerHit, _delta: WheelDelta) {}
fn get_interaction_transform(&mut self) -> Option<Affine2> {
self.interaction_transform
self.0.get_interaction_transform()
}
fn get_attrib(&self, attrib: BackendAttrib) -> Option<BackendAttribValue> {
if let Some(renderer) = self.renderer.as_ref() {
renderer.get_attrib(attrib)
} else {
None
}
self.0.get_attrib(attrib)
}
fn set_attrib(&mut self, app: &mut AppState, value: BackendAttribValue) -> bool {
if let Some(renderer) = self.renderer.as_mut() {
renderer.set_attrib(app, value)
} else {
false
}
self.0.set_attrib(app, value)
}
}
@ -190,8 +77,41 @@ pub fn new_mirror_name() -> Arc<str> {
format!("M-{}", MIRROR_COUNTER.fetch_add(1, Ordering::Relaxed)).into()
}
pub fn new_mirror(name: Arc<str>, session: &AppSession) -> OverlayWindowConfig {
OverlayWindowConfig {
pub fn new_mirror(name: Arc<str>, app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
fn finalize_fn(
name: Arc<str>,
_: Vec2,
_: Vec2,
capture: Box<dyn WlxCapture<WlxCaptureIn, WlxCaptureOut>>,
app: &mut AppState,
) -> Box<dyn OverlayBackend> {
let renderer =
ScreenBackend::new_raw(name.clone(), app.xr_backend, CaptureType::PipeWire, capture);
let backend = MirrorBackend(renderer);
Box::new(backend)
}
let params = ScreenCastParams {
token: None,
embed_mouse: true,
screens_only: false,
persist: false,
allow_multiple: false,
};
let backend = ScreenCastBackend::new_raw(
name.clone(),
"".into(),
Vec2::ZERO,
Vec2::ZERO,
params,
app,
finalize_fn,
)?;
Ok(OverlayWindowConfig {
name: name.clone(),
category: OverlayCategory::Mirror,
show_on_spawn: true,
@ -200,12 +120,12 @@ pub fn new_mirror(name: Arc<str>, session: &AppSession) -> OverlayWindowConfig {
grabbable: true,
curvature: Some(0.15),
transform: Affine3A::from_scale_rotation_translation(
Vec3::ONE * session.config.default_overlay_scale,
Vec3::ONE * app.session.config.default_overlay_scale,
Quat::IDENTITY,
vec3(0.0, 0.2, -0.35),
),
..OverlayWindowState::default()
},
..OverlayWindowConfig::from_backend(Box::new(MirrorBackend::new(name)))
}
..OverlayWindowConfig::from_backend(Box::new(backend))
})
}

View File

@ -72,7 +72,7 @@ pub fn create_screens(app: &mut AppState) -> anyhow::Result<(ScreenCreateData, b
{
if let Some(mut wl) = wlx_capture::wayland::WlxClient::new() {
log::info!("Wayland detected.");
return Ok((wl::create_screens_wayland(&mut wl, app), true));
return Ok((wl::create_screens_wayland(&mut wl, app)?, true));
}
log::info!("Wayland not detected, assuming X11.");
}

View File

@ -1,147 +1,426 @@
use std::{error::Error, path::PathBuf, task, time::Instant};
use std::{
any::Any,
collections::HashMap,
rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
use serde::{Deserialize, Serialize};
use glam::Vec2;
use wgui::{
event::CallbackDataCommon, i18n::Translation, log::LogErr, parser::Fetchable,
widget::label::WidgetLabel,
};
use wlx_capture::{
WlxCapture,
pipewire::{PipewireCapture, PipewireSelectScreenResult},
pipewire::{ScreenCastParams, ScreenCastRequestId, ScreenCastResult, capture::PipewireCapture},
wayland::WlxOutput,
};
use wlx_common::{
config::{PwTokenMap, def_pw_tokens},
config_io,
};
use wlx_common::{astr_containers::AStrMapExt, overlays::ToastTopic};
use crate::{state::AppState, subsystem::dbus::DbusConnector};
use crate::{
backend::task::{OverlayTask, TaskType},
gui::panel::{GuiPanel, NewGuiPanelParams},
overlays::{
screen::{
backend::CaptureType,
capture::{WlxCaptureIn, WlxCaptureOut},
},
toast::Toast,
},
state::{AppState, save_pw_token_config},
subsystem::dbus::DbusConnector,
windowing::{
OverlaySelector,
backend::{DummyBackend, OverlayBackend},
},
};
use super::{
backend::ScreenBackend,
capture::{MainThreadWlxCapture, new_wlx_capture},
};
#[cfg(feature = "wayland")]
impl ScreenBackend {
pub fn new_pw(
pub(super) type ScreenCastFinalizeFn = fn(
Arc<str>,
Vec2,
Vec2,
Box<dyn WlxCapture<WlxCaptureIn, WlxCaptureOut>>,
&mut AppState,
) -> Box<dyn OverlayBackend>;
pub struct ScreenCastPanelState {
name: Arc<str>,
description: Arc<str>,
logical_pos: Vec2,
logical_size: Vec2,
params: Option<ScreenCastParams>,
finalize_fn: ScreenCastFinalizeFn,
}
pub struct ScreenCastBackend {
panel: GuiPanel<ScreenCastPanelState>,
}
impl ScreenCastBackend {
#[cfg(feature = "wayland")]
pub fn new_wl(
output: &WlxOutput,
token: Option<&str>,
token: Option<Rc<str>>,
app: &mut AppState,
) -> anyhow::Result<(Self, Option<String> /* pipewire restore token */)> {
use crate::overlays::screen::backend::CaptureType;
) -> anyhow::Result<Self> {
use glam::vec2;
let name = output.name.clone();
let embed_mouse = !app.session.config.double_cursor_fix;
fn finalize_fn(
name: Arc<str>,
logical_pos: Vec2,
logical_size: Vec2,
capture: Box<dyn WlxCapture<WlxCaptureIn, WlxCaptureOut>>,
app: &mut AppState,
) -> Box<dyn OverlayBackend> {
use wlx_capture::frame::Transform;
let select_screen_result = select_pw_screen(
&format!(
"Now select: {} {} {} @ {},{}",
&output.name,
&output.make,
&output.model,
&output.logical_pos.0,
&output.logical_pos.1
),
let mut backend =
ScreenBackend::new_raw(name, app.xr_backend, CaptureType::PipeWire, capture);
backend.logical_pos = logical_pos;
backend.logical_size = logical_size;
backend.apply_mouse_transform_with_override(Transform::Undefined);
Box::new(backend)
}
let params = ScreenCastParams {
token,
embed_mouse,
true,
true,
false,
)?;
embed_mouse: true,
screens_only: true,
persist: true,
allow_multiple: false,
};
log::debug!(
"{}: PipeWire result streams: {:?}",
output.name,
&select_screen_result.streams
);
Self::new_raw(
output.name.clone(),
format!(
"{} {} @ {},{}",
output.make, output.model, output.logical_pos.0, output.logical_pos.1
)
.into(),
vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32),
vec2(output.logical_size.0 as f32, output.logical_size.1 as f32),
params,
app,
finalize_fn,
)
}
let node_id = select_screen_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
pub(super) fn new_raw(
name: Arc<str>,
description: Arc<str>,
logical_pos: Vec2,
logical_size: Vec2,
params: ScreenCastParams,
app: &mut AppState,
finalize_fn: ScreenCastFinalizeFn,
) -> anyhow::Result<Self> {
if app.screencast_manager.is_none() {
anyhow::bail!("xdg-desktop-portal screencasts not supported");
};
let capture = new_wlx_capture!(
app.gfx_extras.queue_capture,
PipewireCapture::new(name, node_id)
);
Ok((
Self::new_raw(
output.name.clone(),
app.xr_backend,
CaptureType::PipeWire,
capture,
),
select_screen_result.restore_token,
))
let panel_params = NewGuiPanelParams {
extra_vars: HashMap::from([
("name".into(), name.as_ref().into()),
("description".into(), description.as_ref().into()),
]),
..Default::default()
};
let state = ScreenCastPanelState {
name: name.clone(),
description: description.clone(),
logical_pos,
logical_size,
params: Some(params),
finalize_fn,
};
let mut panel =
GuiPanel::new_from_template(app, "gui/screencast.xml", state, panel_params)?;
panel.update_layout(app)?;
Ok(Self { panel })
}
}
#[allow(clippy::fn_params_excessive_bools)]
pub(super) fn select_pw_screen(
instructions: &str,
token: Option<&str>,
embed_mouse: bool,
screens_only: bool,
persist: bool,
multiple: bool,
) -> Result<PipewireSelectScreenResult, wlx_capture::pipewire::AshpdError> {
use std::time::Duration;
use wlx_capture::pipewire::pipewire_select_screen;
impl OverlayBackend for ScreenCastBackend {
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
if let Some(params) = self.panel.state.params.take()
&& let Some(screencast_manager) = app.screencast_manager.as_mut()
{
let request_id = screencast_manager.request(params.clone())?;
check(
self.panel.state.name.clone(),
self.panel.state.description.clone(),
self.panel.state.logical_pos,
self.panel.state.logical_size,
0,
None,
None,
request_id.clone(),
self.panel.state.finalize_fn,
app,
);
}
let future = async move {
let print_at = Instant::now() + Duration::from_millis(250);
let mut notify = None;
self.panel.init(app)
}
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.panel.pause(app)
}
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.panel.resume(app)
}
fn should_render(
&mut self,
app: &mut AppState,
) -> anyhow::Result<crate::windowing::backend::ShouldRender> {
self.panel.should_render(app)
}
fn render(
&mut self,
app: &mut AppState,
rdr: &mut crate::windowing::backend::RenderResources,
) -> anyhow::Result<()> {
self.panel.render(app, rdr)
}
fn frame_meta(&mut self) -> Option<crate::windowing::backend::FrameMeta> {
self.panel.frame_meta()
}
fn notify(
&mut self,
app: &mut AppState,
event_data: crate::windowing::backend::OverlayEventData,
) -> anyhow::Result<()> {
self.panel.notify(app, event_data)
}
fn on_hover(
&mut self,
app: &mut AppState,
hit: &crate::backend::input::PointerHit,
) -> crate::backend::input::HoverResult {
self.panel.on_hover(app, hit)
}
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
self.panel.on_left(app, pointer);
}
fn on_pointer(
&mut self,
app: &mut AppState,
hit: &crate::backend::input::PointerHit,
pressed: bool,
) {
self.panel.on_pointer(app, hit, pressed);
}
fn on_scroll(
&mut self,
app: &mut AppState,
hit: &crate::backend::input::PointerHit,
delta: crate::subsystem::hid::WheelDelta,
) {
self.panel.on_scroll(app, hit, delta);
}
fn get_interaction_transform(&mut self) -> Option<glam::Affine2> {
self.panel.get_interaction_transform()
}
fn get_attrib(
&self,
_attrib: wlx_common::overlays::BackendAttrib,
) -> Option<wlx_common::overlays::BackendAttribValue> {
None
}
fn set_attrib(
&mut self,
_app: &mut AppState,
_value: wlx_common::overlays::BackendAttribValue,
) -> bool {
false
}
}
let f = pipewire_select_screen(token, embed_mouse, screens_only, persist, multiple);
futures::pin_mut!(f);
fn check(
name: Arc<str>,
description: Arc<str>,
logical_pos: Vec2,
logical_size: Vec2,
user_wait: u32,
mut notify_id: Option<u32>,
last_result: Option<ScreenCastResult>,
request_id: ScreenCastRequestId,
finalize_fn: ScreenCastFinalizeFn,
app: &mut AppState,
) {
const POLL_INTERVAL: Duration = Duration::from_millis(100);
loop {
match futures::poll!(&mut f) {
task::Poll::Ready(result) => return result,
task::Poll::Pending => {
if Instant::now() >= print_at {
log::info!("{instructions}");
if let Ok(id) =
DbusConnector::notify_send(instructions, "", 1, 30000, 0, true)
{
notify = Some(id);
}
break;
}
futures::future::lazy(|_| {
std::thread::sleep(Duration::from_millis(10));
})
.await;
if let Some(screencast_manager) = app.screencast_manager.as_mut() {
let new_result = screencast_manager.check(&request_id);
match new_result {
ScreenCastResult::Ok(ref pw_result) => {
log::debug!(
"{}: PipeWire result streams: {:?}",
name,
&pw_result.streams
);
let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
log::info!("{}: PipeWire node selected: {}", name, node_id);
if let Some(id) = notify_id.take() {
let _ = DbusConnector::notify_close(id);
}
let pw_tokens_copy = app.session.pw_tokens.clone();
if let Some(restore_token) = pw_result.restore_token.as_ref()
&& app
.session
.pw_tokens
.arc_set(name.clone(), restore_token.clone())
{
log::info!("Adding Pipewire token for {name}");
}
if pw_tokens_copy != app.session.pw_tokens {
// Token list changed, re-create token config file
if let Err(err) = save_pw_token_config(app.session.pw_tokens.clone()) {
log::error!("Failed to save Pipewire token config: {err}");
}
}
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Name(name.clone()),
Box::new(move |app, owc| {
let capture = new_wlx_capture!(
app.gfx_extras.queue_capture,
PipewireCapture::new(name.clone(), node_id)
);
owc.backend = finalize_fn(name, logical_pos, logical_size, capture, app);
let _ = owc
.backend
.init(app)
.log_err("Failed to init ScreenBackend");
}),
)));
}
// TODO: display status to user
ScreenCastResult::Queued
| ScreenCastResult::Pending
| ScreenCastResult::WaitingForUser => {
let user_wait_add = if matches!(new_result, ScreenCastResult::WaitingForUser) {
if user_wait == 2 {
notify_id = DbusConnector::notify_send(
"Select screen cast for:",
format!("{name} {description}").as_str(),
1,
30000,
0,
true,
)
.ok();
}
1
} else {
0
};
if last_result.is_none_or(|last_result| last_result != new_result) {
update_status_hack(&name, &description, &new_result, app);
}
app.tasks.enqueue_at(
TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Name(name.clone()),
Box::new({
move |app, _owc| {
check(
name.clone(),
description.clone(),
logical_pos,
logical_size,
user_wait + user_wait_add,
notify_id,
Some(new_result),
request_id.clone(),
finalize_fn,
app,
);
}
}),
)),
Instant::now() + POLL_INTERVAL,
);
}
ScreenCastResult::Failed(ref e) => {
if let Some(id) = notify_id.take() {
let _ = DbusConnector::notify_close(id);
}
update_status_hack(&name, &description, &new_result, app);
log::warn!("Failed to create mirror due to PipeWire error: {e:?}");
Toast::new(
ToastTopic::Error,
"TOAST.TITLE_SCREENCAST_FAIL".into(),
format!("{e}"),
)
.submit(app);
}
}
}
}
let result = f.await;
if let Some(id) = notify {
//safe unwrap; checked above
let _ = DbusConnector::notify_close(id);
fn update_status_hack(
name: &Arc<str>,
description: &Arc<str>,
result: &ScreenCastResult,
app: &mut AppState,
) {
let message = match result {
ScreenCastResult::Queued => "SCREENCAST.QUEUED",
ScreenCastResult::WaitingForUser => {
if description.is_empty() {
"SCREENCAST.WAITING_FOR_USER"
} else {
"SCREENCAST.WAITING_FOR_USER_DESC"
}
}
result
ScreenCastResult::Failed(_) => "SCREENCAST.FAIL",
_ => "SCREENCAST.PENDING", // Ok is technically never visible
};
futures::executor::block_on(future)
}
#[derive(Deserialize, Serialize, Default)]
pub struct TokenConf {
#[serde(default = "def_pw_tokens")]
pub pw_tokens: PwTokenMap,
}
fn get_pw_token_path() -> PathBuf {
let mut path = config_io::ConfigRoot::Generic.get_conf_d_path();
path.push("pw_tokens.yaml");
path
}
pub fn save_pw_token_config(tokens: PwTokenMap) -> Result<(), Box<dyn Error>> {
let conf = TokenConf { pw_tokens: tokens };
let yaml = serde_yaml::to_string(&conf)?;
std::fs::write(get_pw_token_path(), yaml)?;
Ok(())
}
pub fn load_pw_token_config() -> Result<PwTokenMap, Box<dyn Error>> {
let yaml = std::fs::read_to_string(get_pw_token_path())?;
let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?;
Ok(conf.pw_tokens)
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Name(name.clone()),
Box::new({
move |_app, owc| {
let backend =
std::mem::replace(&mut owc.backend, Box::new(DummyBackend {})) as Box<dyn Any>;
let mut backend = backend
.downcast::<ScreenCastBackend>()
.expect("Wrong type to unwrap");
{
let Ok(mut label) = backend
.panel
.parser_state
.fetch_widget_as::<WidgetLabel>(&backend.panel.layout.state, "status")
.log_err("element with id=\"status\" not found in screencast.xml!")
else {
return;
};
let mut common = CallbackDataCommon {
state: &backend.panel.layout.state,
alterables: &mut backend.panel.layout.alterables,
};
label.set_text(&mut common, Translation::from_translation_key(message));
}
let _ = std::mem::replace(&mut owc.backend, backend);
}
}),
)));
}

View File

@ -1,25 +1,23 @@
use glam::vec2;
use wgui::log::LogErr;
use wlx_capture::{
WlxCapture,
frame::Transform,
wayland::{WlxClient, WlxOutput},
wlr_screencopy::WlrScreencopyCapture,
};
use wlx_common::{
astr_containers::AStrMapExt,
config::{CaptureMethod, PwTokenMap},
};
use wlx_common::{astr_containers::AStrMapExt, config::CaptureMethod};
use crate::{
overlays::screen::{backend::CaptureType, create_screen_from_backend},
overlays::screen::{backend::CaptureType, create_screen_from_backend, pw::ScreenCastBackend},
state::{AppState, ScreenMeta},
windowing::backend::OverlayBackend,
};
use super::{
ScreenCreateData,
backend::ScreenBackend,
capture::{MainThreadWlxCapture, new_wlx_capture},
pw::{load_pw_token_config, save_pw_token_config},
};
impl ScreenBackend {
@ -38,67 +36,50 @@ impl ScreenBackend {
}
}
#[allow(clippy::useless_let_if_seq)]
pub fn create_screen_renderer_wl(
output: &WlxOutput,
has_wlr_screencopy: bool,
pw_token_store: &mut PwTokenMap,
app: &mut AppState,
) -> Option<ScreenBackend> {
let mut capture: Option<ScreenBackend> = None;
) -> anyhow::Result<Box<dyn OverlayBackend>> {
if matches!(
app.session.config.capture_method,
CaptureMethod::ScreenCopyCpu | CaptureMethod::ScreenCopyGpu | CaptureMethod::Auto
) && has_wlr_screencopy
{
log::info!("{}: Using ScreenCopy capture", &output.name);
capture = ScreenBackend::new_wlr_screencopy(output, app);
}
if capture.is_none() {
log::info!("{}: Using Pipewire capture", &output.name);
let display_name = &*output.name;
// Find existing token by display
let token = pw_token_store
.arc_get(display_name)
.map(std::string::String::as_str);
if let Some(t) = token {
log::info!("Found existing Pipewire token for display {display_name}: {t}");
}
match ScreenBackend::new_pw(output, token, app) {
Ok((renderer, restore_token)) => {
capture = Some(renderer);
if let Some(token) = restore_token
&& pw_token_store.arc_set(display_name.into(), token.clone())
{
log::info!("Adding Pipewire token {token}");
}
}
Err(e) => {
log::warn!(
"{}: Failed to create Pipewire capture: {:?}",
&output.name,
e
);
}
if let Some(mut backend) = ScreenBackend::new_wlr_screencopy(output, app) {
log::info!("{}: Using ScreenCopy capture", &output.name);
backend.logical_pos = vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32);
backend.logical_size = vec2(output.logical_size.0 as f32, output.logical_size.1 as f32);
backend.apply_mouse_transform_with_override(Transform::Undefined);
return Ok(Box::new(backend));
}
}
capture
log::info!("{}: Using Pipewire capture", &output.name);
let display_name = &*output.name;
// Find existing token by display
let token = app
.session
.pw_tokens
.arc_get(display_name)
.map(|x| x.to_string().into());
if token.is_some() {
log::info!("Found existing Pipewire token for display {display_name}");
}
Ok(Box::new(
ScreenCastBackend::new_wl(output, token, app)
.log_err("Failed to create screen with screen cast backend")?,
))
}
pub fn create_screens_wayland(wl: &mut WlxClient, app: &mut AppState) -> ScreenCreateData {
pub fn create_screens_wayland(
wl: &mut WlxClient,
app: &mut AppState,
) -> anyhow::Result<ScreenCreateData> {
let mut screens = vec![];
// Load existing Pipewire tokens from file
let mut pw_tokens: PwTokenMap = load_pw_token_config().unwrap_or_default();
let pw_tokens_copy = pw_tokens.clone();
let has_wlr_screencopy = wl.maybe_wlr_screencopy_mgr.is_some();
for (id, output) in &wl.outputs {
@ -114,35 +95,20 @@ pub fn create_screens_wayland(wl: &mut WlxClient, app: &mut AppState) -> ScreenC
output.logical_pos,
);
if let Some(mut backend) =
create_screen_renderer_wl(output, has_wlr_screencopy, &mut pw_tokens, app)
{
backend.logical_pos = vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32);
backend.logical_size = vec2(output.logical_size.0 as f32, output.logical_size.1 as f32);
backend.mouse_transform_original = output.transform;
backend.apply_mouse_transform_with_override(Transform::Undefined);
let backend = create_screen_renderer_wl(output, has_wlr_screencopy, app)?;
let window_config = create_screen_from_backend(
output.name.clone(),
output.transform,
&app.session,
backend,
);
let window_config = create_screen_from_backend(
output.name.clone(),
output.transform,
&app.session,
Box::new(backend),
);
let meta = ScreenMeta {
name: wl.outputs[id].name.clone(),
native_handle: *id,
};
let meta = ScreenMeta {
name: wl.outputs[id].name.clone(),
native_handle: *id,
};
screens.push((meta, window_config));
}
}
if pw_tokens_copy != pw_tokens {
// Token list changed, re-create token config file
if let Err(err) = save_pw_token_config(pw_tokens) {
log::error!("Failed to save Pipewire token config: {err}");
}
screens.push((meta, window_config));
}
let extent = wl.get_desktop_extent();
@ -155,5 +121,5 @@ pub fn create_screens_wayland(wl: &mut WlxClient, app: &mut AppState) -> ScreenC
.inner
.set_desktop_origin(vec2(origin.0 as f32, origin.1 as f32));
ScreenCreateData { screens }
Ok(ScreenCreateData { screens })
}

View File

@ -7,6 +7,8 @@ use wlx_capture::{
xshm::{XshmCapture, XshmScreen},
};
#[cfg(feature = "pipewire")]
use crate::state::save_pw_token_config;
use crate::{
overlays::screen::{backend::CaptureType, create_screen_from_backend},
state::{AppState, ScreenMeta},
@ -19,7 +21,9 @@ use super::{
};
#[cfg(feature = "pipewire")]
use wlx_capture::pipewire::PipewireStream;
use wlx_capture::pipewire::{
PipewireStream, ScreenCastParams, capture::PipewireCapture, screen_cast_select_blocking,
};
impl ScreenBackend {
pub fn new_xshm(screen: Arc<XshmScreen>, app: &AppState) -> Self {
@ -39,42 +43,41 @@ impl ScreenBackend {
#[cfg(feature = "pipewire")]
pub fn create_screens_x11pw(app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
use glam::vec2;
use wlx_capture::{pipewire::PipewireCapture, xshm::xshm_get_monitors};
use wlx_common::{astr_containers::AStrMapExt, config::PwTokenMap};
use wlx_capture::xshm::xshm_get_monitors;
use wlx_common::astr_containers::AStrMapExt;
use crate::{
overlays::screen::{
create_screen_from_backend,
pw::{load_pw_token_config, save_pw_token_config, select_pw_screen},
},
state::ScreenMeta,
};
use crate::{overlays::screen::create_screen_from_backend, state::ScreenMeta};
use super::ScreenCreateData;
// Load existing Pipewire tokens from file
let mut pw_tokens: PwTokenMap = load_pw_token_config().unwrap_or_default();
let pw_tokens_copy = pw_tokens.clone();
let token = pw_tokens.arc_get("x11").map(std::string::String::as_str);
let embed_mouse = !app.session.config.double_cursor_fix;
let pw_tokens_copy = app.session.pw_tokens.clone();
let select_screen_result = select_pw_screen(
"Select ALL screens on the screencast pop-up!",
token,
embed_mouse,
true,
true,
true,
)?;
let params = ScreenCastParams {
token: app
.session
.pw_tokens
.arc_get("x11")
.map(|x| x.to_string().into()),
embed_mouse: !app.session.config.double_cursor_fix,
allow_multiple: true,
persist: true,
screens_only: true,
};
// this one still blocks
let select_screen_result = screen_cast_select_blocking(params)?;
if let Some(restore_token) = select_screen_result.restore_token
&& pw_tokens.arc_set("x11".into(), restore_token.clone())
&& app
.session
.pw_tokens
.arc_set("x11".into(), restore_token.clone())
{
log::info!("Adding Pipewire token {restore_token}");
}
if pw_tokens_copy != pw_tokens {
if pw_tokens_copy != app.session.pw_tokens {
// Token list changed, re-create token config file
if let Err(err) = save_pw_token_config(pw_tokens) {
if let Err(err) = save_pw_token_config(app.session.pw_tokens.clone()) {
log::error!("Failed to save Pipewire token config: {err}");
}
}

View File

@ -1,6 +1,9 @@
use glam::Affine3A;
use idmap::IdMap;
#[cfg(feature = "pipewire")]
use serde::{Deserialize, Serialize};
use smallvec::{SmallVec, smallvec};
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use wgui::log::LogErr;
@ -9,6 +12,9 @@ use wgui::{
drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex,
renderer_vk::context::SharedContext as WSharedContext,
};
#[cfg(feature = "pipewire")]
use wlx_capture::pipewire::ScreenCastManager;
use wlx_common::config::PwTokenMap;
use wlx_common::locale::WayVRLangProvider;
use wlx_common::{
audio,
@ -80,6 +86,9 @@ pub struct AppState {
#[cfg(feature = "openxr")]
pub monado_state: Option<backend::openxr::monado_state::MonadoState>,
#[cfg(feature = "pipewire")]
pub screencast_manager: Option<ScreenCastManager>,
pub delta_time: f32,
}
@ -165,6 +174,14 @@ impl AppState {
let lang_provider = WayVRLangProvider::from_config(&session.config);
#[cfg(feature = "pipewire")]
let screencast_manager = ScreenCastManager::new()
.log_err(
// would only fail if session D-bus is unreachable
"Could not initialize ScreenCastManager. PipeWire screen capture will not work. Check your D-bus setup.",
)
.ok();
let mut app_state = Self {
session,
tasks,
@ -202,6 +219,9 @@ impl AppState {
#[cfg(feature = "openxr")]
monado_state: None,
#[cfg(feature = "pipewire")]
screencast_manager,
delta_time: 1.0 / 120.0,
};
@ -254,6 +274,9 @@ pub struct AppSession {
pub config: GeneralConfig,
pub config_dirty: bool,
#[cfg(feature = "pipewire")]
pub pw_tokens: PwTokenMap,
pub no_autostart: bool,
pub toast_topics: IdMap<ToastTopic, ToastDisplayMethod>,
@ -275,11 +298,18 @@ impl AppSession {
toast_topics.insert(*k, *v);
});
#[cfg(feature = "pipewire")]
let pw_tokens = load_pw_token_config()
.log_err("Could not load PipeWire tokens")
.unwrap_or_default();
Self {
config,
toast_topics,
no_autostart: false,
config_dirty: false,
#[cfg(feature = "pipewire")]
pw_tokens,
}
}
}
@ -289,3 +319,31 @@ pub struct ScreenMeta {
#[allow(dead_code)]
pub native_handle: u32,
}
#[cfg(feature = "pipewire")]
#[derive(Deserialize, Serialize, Default)]
struct TokenConf {
pub pw_tokens: PwTokenMap,
}
#[cfg(feature = "pipewire")]
fn get_pw_token_path() -> PathBuf {
let mut path = config_io::ConfigRoot::Generic.get_conf_d_path();
path.push("pw_tokens.yaml");
path
}
#[cfg(feature = "pipewire")]
pub fn save_pw_token_config(tokens: PwTokenMap) -> anyhow::Result<()> {
let conf = TokenConf { pw_tokens: tokens };
let yaml = serde_yaml::to_string(&conf)?;
std::fs::write(get_pw_token_path(), yaml)?;
Ok(())
}
#[cfg(feature = "pipewire")]
pub fn load_pw_token_config() -> anyhow::Result<PwTokenMap> {
let yaml = std::fs::read_to_string(get_pw_token_path())?;
let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?;
Ok(conf.pw_tokens)
}

View File

@ -523,6 +523,12 @@ impl ParserContext<'_> {
}
}
fn populate_extra_variables(&mut self, other: &HashMap<Rc<str>, Rc<str>>) {
for (k,v) in other.iter() {
self.data_local.var_map.insert(k.clone(), v.clone());
}
}
fn populate_theme_variables(&mut self) {
let theme = self.layout.state.theme.clone();
@ -1275,6 +1281,7 @@ pub type OnCustomAttribsFunc = Rc<dyn Fn(CustomAttribsInfo)>;
pub struct ParseDocumentExtra {
pub on_custom_attribs: Option<OnCustomAttribsFunc>, // all attributes with '_' character prepended
pub dev_mode: bool,
pub extra_vars: HashMap<Rc<str>, Rc<str>>,
}
// filled-in by you in `new_layout_from_assets` function
@ -1292,6 +1299,7 @@ pub fn parse_from_assets(
let parser_data = ParserData::default();
let mut ctx = create_default_context(doc_params, layout, &parser_data);
ctx.populate_theme_variables();
ctx.populate_extra_variables(&doc_params.extra.extra_vars);
let (file, node_layout) = get_doc_from_asset_path(&ctx, doc_params.path)?;
parse_document_root(&file, &mut ctx, parent_id, node_layout)?;

View File

@ -5,44 +5,50 @@ edition = "2024"
authors = ["galister"]
description = "Screen capture library for Wayland and X11"
repository = "https://github.com/galister/wlx-capture"
keywords = ["desktop", "capture", "pipewire", "wayland", "x11"]
keywords = ["capture", "desktop", "pipewire", "wayland", "x11"]
categories = ["computer-vision", "multimedia::images"]
documentation = "https://docs.rs/wlx-capture"
license = "GPL-3.0-only"
[dependencies]
dbus = { workspace = true, features = ["futures"], optional = true }
idmap = { workspace = true }
log = { workspace = true }
pipewire = { workspace = true, optional = true }
slotmap.workspace = true
wayland-client = { workspace = true, optional = true }
wayland-protocols = {
workspace = true,
features = [
"client",
"staging",
"unstable",
"wayland-client",
],
optional = true
}
drm-fourcc = "2.2.0"
libc = "0.2.178"
rxscreen = {
version = "0.1.7",
features = [
"mouse",
"shm",
"xrandr",
],
optional = true
}
smithay-client-toolkit = { version = "0.20.0", optional = true }
[features]
default = ["wayland", "pipewire"]
default = ["pipewire", "wayland"]
egl = []
wlr = ["wayland"]
pipewire = ["wayland", "dep:ashpd", "dep:pipewire"]
pipewire = ["dep:dbus", "dep:pipewire", "wayland"]
wayland = [
"dep:smithay-client-toolkit",
"dep:wayland-client",
"dep:wayland-protocols",
]
wlr = ["wayland"]
xshm = ["dep:rxscreen"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ashpd = { version = "0.12.0", default-features = false, features = [
"async-std",
], optional = true }
drm-fourcc = "2.2.0"
idmap = { workspace = true }
libc = "0.2.178"
log = { workspace = true }
pipewire = { workspace = true, optional = true }
rxscreen = { version = "0.1.7", features = [
"shm",
"xrandr",
"mouse",
], optional = true }
smithay-client-toolkit = { version = "0.20.0", optional = true }
wayland-client = { workspace = true, optional = true }
wayland-protocols = { version = "0.32.9", features = [
"wayland-client",
"client",
"staging",
"unstable",
], optional = true }

View File

@ -1,18 +1,9 @@
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::thread::JoinHandle;
use ashpd::desktop::{
PersistMode,
screencast::{CursorMode, Screencast, SourceType},
};
pub use ashpd::Error as AshpdError;
use drm_fourcc::DrmFormat;
use drm_fourcc::DrmFourcc;
use drm_fourcc::DrmModifier;
@ -46,93 +37,6 @@ use crate::frame::Transform;
use crate::frame::WlxFrame;
use crate::frame::{DmabufFrame, FramePlane, MemFdFrame, MemPtrFrame};
#[derive(Debug, Clone)]
pub struct PipewireStream {
pub node_id: u32,
pub position: Option<(i32, i32)>,
pub size: Option<(i32, i32)>,
}
#[derive(Debug, Clone)]
pub struct PipewireSelectScreenResult {
pub streams: Vec<PipewireStream>,
pub restore_token: Option<String>,
}
pub async fn pipewire_select_screen(
token: Option<&str>,
embed_mouse: bool,
screens_only: bool,
persist: bool,
multiple: bool,
) -> Result<PipewireSelectScreenResult, AshpdError> {
static CURSOR_MODES: AtomicU32 = AtomicU32::new(0);
let proxy = Screencast::new().await?;
let session = proxy.create_session().await?;
let mut cursor_modes = CURSOR_MODES.load(Ordering::Relaxed);
if cursor_modes == 0 {
cursor_modes = proxy.get_property::<u32>("AvailableCursorModes").await?;
log::debug!("Available cursor modes: {cursor_modes:#x}");
// properly will be same system-wide, so race condition not a concern
CURSOR_MODES.store(cursor_modes, Ordering::Relaxed);
}
let cursor_mode = match embed_mouse {
true if cursor_modes & (CursorMode::Embedded as u32) != 0 => CursorMode::Embedded,
_ if cursor_modes & (CursorMode::Metadata as u32) != 0 => CursorMode::Metadata,
_ => CursorMode::Hidden,
};
log::debug!("Selected cursor mode: {cursor_mode:?}");
let source_type = if screens_only {
SourceType::Monitor.into()
} else {
SourceType::Monitor | SourceType::Window | SourceType::Virtual
};
let persist_mode = if persist {
PersistMode::ExplicitlyRevoked
} else {
PersistMode::DoNot
};
proxy
.select_sources(
&session,
cursor_mode,
source_type,
multiple,
token,
persist_mode,
)
.await?;
let response = proxy.start(&session, None).await?.response()?;
let streams: Vec<_> = response
.streams()
.iter()
.map(|stream| PipewireStream {
node_id: stream.pipe_wire_node_id(),
position: stream.position(),
size: stream.size(),
})
.collect();
if !streams.is_empty() {
return Ok(PipewireSelectScreenResult {
streams,
restore_token: response.restore_token().map(String::from),
});
}
Err(ashpd::Error::NoResponse)
}
#[derive(Default)]
struct StreamData {
format: Option<FrameFormat>,

View File

@ -0,0 +1,50 @@
// This code was autogenerated with `dbus-codegen-rust -g -m None --client nonblock`, see https://github.com/diwic/dbus-rs
use dbus as dbus;
#[allow(unused_imports)]
use dbus::arg;
use dbus::nonblock;
pub trait OrgFreedesktopPortalScreenCast {
fn create_session(&self, options: arg::PropMap) -> nonblock::MethodReply<dbus::Path<'static>>;
fn select_sources(&self, session_handle: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply<dbus::Path<'static>>;
fn start(&self, session_handle: dbus::Path, parent_window: &str, options: arg::PropMap) -> nonblock::MethodReply<dbus::Path<'static>>;
fn open_pipe_wire_remote(&self, session_handle: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply<arg::OwnedFd>;
fn available_source_types(&self) -> nonblock::MethodReply<u32>;
fn available_cursor_modes(&self) -> nonblock::MethodReply<u32>;
fn version(&self) -> nonblock::MethodReply<u32>;
}
impl<'a, T: nonblock::NonblockReply, C: ::std::ops::Deref<Target=T>> OrgFreedesktopPortalScreenCast for nonblock::Proxy<'a, C> {
fn create_session(&self, options: arg::PropMap) -> nonblock::MethodReply<dbus::Path<'static>> {
self.method_call("org.freedesktop.portal.ScreenCast", "CreateSession", (options, ))
.and_then(|r: (dbus::Path<'static>, )| Ok(r.0, ))
}
fn select_sources(&self, session_handle: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply<dbus::Path<'static>> {
self.method_call("org.freedesktop.portal.ScreenCast", "SelectSources", (session_handle, options, ))
.and_then(|r: (dbus::Path<'static>, )| Ok(r.0, ))
}
fn start(&self, session_handle: dbus::Path, parent_window: &str, options: arg::PropMap) -> nonblock::MethodReply<dbus::Path<'static>> {
self.method_call("org.freedesktop.portal.ScreenCast", "Start", (session_handle, parent_window, options, ))
.and_then(|r: (dbus::Path<'static>, )| Ok(r.0, ))
}
fn open_pipe_wire_remote(&self, session_handle: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply<arg::OwnedFd> {
self.method_call("org.freedesktop.portal.ScreenCast", "OpenPipeWireRemote", (session_handle, options, ))
.and_then(|r: (arg::OwnedFd, )| Ok(r.0, ))
}
fn available_source_types(&self) -> nonblock::MethodReply<u32> {
<Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(self, "org.freedesktop.portal.ScreenCast", "AvailableSourceTypes")
}
fn available_cursor_modes(&self) -> nonblock::MethodReply<u32> {
<Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(self, "org.freedesktop.portal.ScreenCast", "AvailableCursorModes")
}
fn version(&self) -> nonblock::MethodReply<u32> {
<Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(self, "org.freedesktop.portal.ScreenCast", "version")
}
}

View File

@ -0,0 +1,905 @@
use std::{
collections::{HashMap, VecDeque},
error::Error as StdError,
fmt,
future::Future,
pin::Pin,
rc::Rc,
sync::{Arc, Mutex, MutexGuard},
task::{Context, Poll, Wake, Waker},
time::Duration,
};
use dbus::{
arg::{self, RefArg},
channel::{BusType, Channel},
message::{MatchRule, Message},
nonblock::{self, MsgMatch, Process, Proxy, SyncConnection},
};
use slotmap::SlotMap;
use dbus_screencast::OrgFreedesktopPortalScreenCast;
slotmap::new_key_type! {
pub struct ScreenCastRequestId;
}
pub mod capture;
mod dbus_screencast;
#[derive(Debug, Clone)]
pub struct PipewireStream {
pub node_id: u32,
pub position: Option<(i32, i32)>,
pub size: Option<(i32, i32)>,
}
#[derive(Debug, Clone)]
pub struct ScreenCastResponse {
pub streams: Vec<PipewireStream>,
pub restore_token: Option<String>,
}
#[derive(Default, Debug, Clone)]
pub struct ScreenCastParams {
/// Optional restore token.
pub token: Option<Rc<str>>,
/// If true, use the first available: EMBEDDED, METADATA/FALLBACK, HIDDEN.
pub embed_mouse: bool,
/// false: only request MONITOR
/// true: request MONITOR, WINDOW, VIRTUAL
pub screens_only: bool,
/// true: EXPLICITLY_REVOKED
/// false: DO_NOT
pub persist: bool,
/// Allow the user to select multiple sources.
pub allow_multiple: bool,
}
#[derive(Debug, Clone)]
pub enum ScreenCastResult {
Ok(ScreenCastResponse),
Queued,
Pending,
WaitingForUser,
Failed(ScreenCastError),
}
impl PartialEq for ScreenCastResult {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Ok(_), Self::Ok(_)) => true,
(Self::Queued, Self::Queued) => true,
(Self::Pending, Self::Pending) => true,
(Self::WaitingForUser, Self::WaitingForUser) => true,
(Self::Failed(_), Self::Failed(_)) => true,
_ => false,
}
}
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
}
#[derive(Debug, Clone)]
pub enum ScreenCastError {
Dbus(String),
DbusDisconnected,
InvalidObjectPath(String),
UnknownRequest,
UnsupportedSourceTypes { requested: u32, available: u32 },
UnsupportedCursorMode { available: u32 },
PortalResponse(u32),
MissingField(&'static str),
InvalidResponse(&'static str),
}
impl fmt::Display for ScreenCastError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Dbus(e) => write!(f, "D-Bus error: {e}"),
Self::DbusDisconnected => write!(f, "D-Bus connection is disconnected"),
Self::InvalidObjectPath(e) => write!(f, "invalid D-Bus object path: {e}"),
Self::UnknownRequest => write!(f, "unknown screen select request"),
Self::UnsupportedSourceTypes {
requested,
available,
} => {
write!(
f,
"unsupported source types: requested={requested:#x}, available={available:#x}"
)
}
Self::UnsupportedCursorMode { available } => {
write!(f, "unsupported cursor mode: available={available:#x}")
}
Self::PortalResponse(code) => write!(f, "portal returned response code {code}"),
Self::MissingField(name) => write!(f, "portal response missing field {name}"),
Self::InvalidResponse(what) => write!(f, "invalid portal response: {what}"),
}
}
}
impl StdError for ScreenCastError {}
impl From<dbus::Error> for ScreenCastError {
fn from(value: dbus::Error) -> Self {
Self::Dbus(value.to_string())
}
}
/// manages pipewire screen requests
pub struct ScreenCastManager {
conn: Arc<SyncConnection>,
timeout: Duration,
sender_component: String,
source_types: OneShot<u32>,
cursor_modes: OneShot<u32>,
requests: SlotMap<ScreenCastRequestId, RequestEntry>,
queue: VecDeque<ScreenCastRequestId>,
active: Option<ScreenCastRequestId>,
responses: Arc<Mutex<HashMap<String, Result<PortalResponse, ScreenCastError>>>>,
cleanup: Vec<DbusFuture<()>>,
next_token: u64,
}
impl ScreenCastManager {
pub fn new() -> Result<Self, ScreenCastError> {
let channel = Channel::get_private(BusType::Session)?;
let conn = Arc::new(SyncConnection::from(channel));
let timeout = Duration::from_secs(120);
let proxy = portal_proxy(conn.clone(), timeout);
let sender_component = sender_path_component(&format!("{}", conn.unique_name()));
Ok(Self {
conn,
timeout,
sender_component,
source_types: OneShot::Pending(proxy.available_source_types()),
cursor_modes: OneShot::Pending(proxy.available_cursor_modes()),
requests: SlotMap::with_key(),
queue: VecDeque::new(),
active: None,
responses: Arc::new(Mutex::new(HashMap::new())),
cleanup: Vec::new(),
next_token: 0,
})
}
pub fn request(
&mut self,
params: ScreenCastParams,
) -> Result<ScreenCastRequestId, ScreenCastError> {
let id = self.requests.insert(RequestEntry {
params,
state: RequestState::Queued,
});
self.queue.push_back(id);
Ok(id)
}
pub fn check(&mut self, request_id: &ScreenCastRequestId) -> ScreenCastResult {
if !self.requests.contains_key(*request_id) {
return ScreenCastResult::Failed(ScreenCastError::UnknownRequest);
}
if let Err(e) = self.pump_dbus() {
return ScreenCastResult::Failed(e);
}
self.poll_cleanup();
self.poll_globals();
self.ensure_active();
// drive several immediate transitions, but never spin forever
for _ in 0..12 {
if !self.drive_active_once() {
break;
}
self.ensure_active();
}
let result = self.result_for(*request_id);
if is_terminal(&result) {
let _ = self.requests.remove(*request_id);
if self.active == Some(*request_id) {
self.active = None;
}
self.queue.retain(|id| id != request_id);
}
result
}
fn pump_dbus(&self) -> Result<(), ScreenCastError> {
let channel: &Channel = self.conn.as_ref().as_ref();
channel
.read_write(Some(Duration::ZERO))
.map_err(|_| ScreenCastError::DbusDisconnected)?;
self.conn.process_all();
Ok(())
}
fn poll_cleanup(&mut self) {
let mut i = 0;
while i < self.cleanup.len() {
match poll_boxed(&mut self.cleanup[i]) {
Poll::Ready(_) => {
let _ = self.cleanup.swap_remove(i);
}
Poll::Pending => i += 1,
}
}
}
fn poll_globals(&mut self) {
poll_one_shot(&mut self.source_types);
poll_one_shot(&mut self.cursor_modes);
}
fn globals(&self) -> Result<Option<Globals>, ScreenCastError> {
match (&self.source_types, &self.cursor_modes) {
(OneShot::Ready(source_types), OneShot::Ready(cursor_modes)) => Ok(Some(Globals {
source_types: *source_types,
cursor_modes: *cursor_modes,
})),
(OneShot::Failed(e), _) | (_, OneShot::Failed(e)) => Err(e.clone()),
_ => Ok(None),
}
}
fn ensure_active(&mut self) {
if self.active.is_some() {
return;
}
while let Some(id) = self.queue.pop_front() {
let Some(entry) = self.requests.get_mut(id) else {
continue;
};
if matches!(entry.state, RequestState::Queued) {
entry.state = RequestState::Init;
self.active = Some(id);
return;
}
}
}
fn drive_active_once(&mut self) -> bool {
let Some(id) = self.active else {
return false;
};
let Some(params) = self.requests.get(id).map(|r| r.params.clone()) else {
self.active = None;
return true;
};
let state = match self.requests.get_mut(id) {
Some(entry) => std::mem::replace(&mut entry.state, RequestState::Queued),
None => {
self.active = None;
return true;
}
};
let next_state = match self.advance_state(params, state) {
Ok(state) => state,
Err(error) => RequestState::Terminal(ScreenCastResult::Failed(error)),
};
let progressed = !matches!(next_state, RequestState::Queued);
let terminal = matches!(next_state, RequestState::Terminal(_));
if let Some(entry) = self.requests.get_mut(id) {
entry.state = next_state;
} else {
self.active = None;
return true;
}
if terminal {
self.active = None;
}
progressed
}
fn advance_state(
&mut self,
params: ScreenCastParams,
state: RequestState,
) -> Result<RequestState, ScreenCastError> {
match state {
RequestState::Queued => Ok(RequestState::Queued),
RequestState::Init => {
if self.globals()?.is_none() {
return Ok(RequestState::Init);
}
let session_token = self.next_token("pw_session");
self.begin_step(PortalStep::CreateSession { session_token })
}
RequestState::AddMatch {
step,
handle_token,
request_path,
mut future,
} => match poll_boxed(&mut future) {
Poll::Pending => Ok(RequestState::AddMatch {
step,
handle_token,
request_path,
future,
}),
Poll::Ready(Err(e)) => Err(e.into()),
Poll::Ready(Ok(match_handle)) => {
let reply = self.call_step(&params, &step, &handle_token)?;
Ok(RequestState::MethodCall {
step,
request_path,
match_handle,
reply,
})
}
},
RequestState::MethodCall {
step,
request_path,
match_handle,
mut reply,
} => match poll_unpin(&mut reply) {
Poll::Pending => Ok(RequestState::MethodCall {
step,
request_path,
match_handle,
reply,
}),
Poll::Ready(Err(e)) => {
self.schedule_remove_match(match_handle);
Err(e.into())
}
Poll::Ready(Ok(_handle)) => Ok(RequestState::WaitResponse {
step,
request_path,
match_handle,
}),
},
RequestState::WaitResponse {
step,
request_path,
match_handle,
} => {
let Some(response) = self.take_response(&request_path) else {
return Ok(RequestState::WaitResponse {
step,
request_path,
match_handle,
});
};
self.schedule_remove_match(match_handle);
let response = response?;
if response.response != 0 {
return Ok(RequestState::Terminal(ScreenCastResult::Failed(
ScreenCastError::PortalResponse(response.response),
)));
}
match step {
PortalStep::CreateSession { .. } => {
let session_handle = required_string(&response.results, "session_handle")?;
let session_handle = dbus::Path::new(session_handle)
.map_err(ScreenCastError::InvalidObjectPath)?;
self.begin_step(PortalStep::SelectSources { session_handle })
}
PortalStep::SelectSources { session_handle } => {
self.begin_step(PortalStep::Start { session_handle })
}
PortalStep::Start { .. } => {
let response = parse_start_response(&response.results)?;
Ok(RequestState::Terminal(ScreenCastResult::Ok(response)))
}
}
}
RequestState::Terminal(result) => Ok(RequestState::Terminal(result)),
}
}
fn begin_step(&mut self, step: PortalStep) -> Result<RequestState, ScreenCastError> {
let handle_token = self.next_token("pw_request");
let request_path = self.request_path(&handle_token)?;
let future = make_response_match(
self.conn.clone(),
self.responses.clone(),
request_path.clone(),
)?;
Ok(RequestState::AddMatch {
step,
handle_token,
request_path,
future,
})
}
fn call_step(
&self,
params: &ScreenCastParams,
step: &PortalStep,
handle_token: &str,
) -> Result<nonblock::MethodReply<dbus::Path<'static>>, ScreenCastError> {
let proxy = portal_proxy(self.conn.clone(), self.timeout);
match step {
PortalStep::CreateSession { session_token } => {
let mut options = arg::PropMap::new();
prop_insert(&mut options, "handle_token", handle_token.to_owned());
prop_insert(
&mut options,
"session_handle_token",
session_token.to_owned(),
);
Ok(proxy.create_session(options))
}
PortalStep::SelectSources { session_handle } => {
let globals = self.globals()?.ok_or(ScreenCastError::InvalidResponse(
"global portal properties not ready",
))?;
let requested_types = if params.screens_only {
SOURCE_MONITOR | SOURCE_WINDOW | SOURCE_VIRTUAL
} else {
SOURCE_MONITOR
};
let source_types = requested_types & globals.source_types;
if source_types == 0 {
return Err(ScreenCastError::UnsupportedSourceTypes {
requested: requested_types,
available: globals.source_types,
});
}
let cursor_mode = choose_cursor_mode(globals.cursor_modes, params.embed_mouse)
.ok_or(ScreenCastError::UnsupportedCursorMode {
available: globals.cursor_modes,
})?;
let mut options = arg::PropMap::new();
prop_insert(&mut options, "handle_token", handle_token.to_owned());
prop_insert(&mut options, "types", source_types);
prop_insert(&mut options, "multiple", params.allow_multiple);
prop_insert(&mut options, "cursor_mode", cursor_mode);
prop_insert(
&mut options,
"persist_mode",
if params.persist { 2u32 } else { 0u32 },
);
if let Some(token) = &params.token {
prop_insert(&mut options, "restore_token", token.to_string());
}
Ok(proxy.select_sources(session_handle.clone(), options))
}
PortalStep::Start { session_handle } => {
let mut options = arg::PropMap::new();
prop_insert(&mut options, "handle_token", handle_token.to_owned());
Ok(proxy.start(session_handle.clone(), "", options))
}
}
}
fn request_path(&self, handle_token: &str) -> Result<String, ScreenCastError> {
let path = format!(
"/org/freedesktop/portal/desktop/request/{}/{}",
self.sender_component, handle_token
);
let _ = dbus::Path::new(path.clone()).map_err(ScreenCastError::InvalidObjectPath)?;
Ok(path)
}
fn next_token(&mut self, prefix: &str) -> String {
self.next_token = self.next_token.saturating_add(1);
format!("{prefix}_{}", self.next_token)
}
fn take_response(&self, request_path: &str) -> Option<Result<PortalResponse, ScreenCastError>> {
lock_response_map(&self.responses).remove(request_path)
}
fn schedule_remove_match(&mut self, match_handle: MsgMatch) {
let token = match_handle.token();
let conn = self.conn.clone();
self.cleanup
.push(Box::pin(async move { conn.remove_match(token).await }));
}
fn result_for(&self, id: ScreenCastRequestId) -> ScreenCastResult {
let Some(entry) = self.requests.get(id) else {
return ScreenCastResult::Failed(ScreenCastError::UnknownRequest);
};
match &entry.state {
RequestState::Queued => ScreenCastResult::Queued,
RequestState::Init
| RequestState::AddMatch { .. }
| RequestState::MethodCall { .. } => ScreenCastResult::Pending,
RequestState::WaitResponse { step, .. } => match step {
PortalStep::Start { .. } => ScreenCastResult::WaitingForUser,
_ => ScreenCastResult::Pending,
},
RequestState::Terminal(result) => result.clone(),
}
}
}
struct RequestEntry {
params: ScreenCastParams,
state: RequestState,
}
enum RequestState {
Queued,
Init,
AddMatch {
step: PortalStep,
handle_token: String,
request_path: String,
future: DbusFuture<MsgMatch>,
},
MethodCall {
step: PortalStep,
request_path: String,
match_handle: MsgMatch,
reply: nonblock::MethodReply<dbus::Path<'static>>,
},
WaitResponse {
step: PortalStep,
request_path: String,
match_handle: MsgMatch,
},
Terminal(ScreenCastResult),
}
#[derive(Clone)]
enum PortalStep {
CreateSession { session_token: String },
SelectSources { session_handle: dbus::Path<'static> },
Start { session_handle: dbus::Path<'static> },
}
#[derive(Debug, Clone, Copy)]
struct Globals {
source_types: u32,
cursor_modes: u32,
}
enum OneShot<T> {
Pending(nonblock::MethodReply<T>),
Ready(T),
Failed(ScreenCastError),
}
#[derive(Debug)]
struct PortalResponse {
response: u32,
results: arg::PropMap,
}
type DbusFuture<T> = Pin<Box<dyn Future<Output = Result<T, dbus::Error>>>>;
const PORTAL_DEST: &str = "org.freedesktop.portal.Desktop";
const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
const REQUEST_IFACE: &str = "org.freedesktop.portal.Request";
const SOURCE_MONITOR: u32 = 1;
const SOURCE_WINDOW: u32 = 2;
const SOURCE_VIRTUAL: u32 = 4;
const CURSOR_HIDDEN: u32 = 1;
const CURSOR_EMBEDDED: u32 = 2;
const CURSOR_METADATA: u32 = 4;
fn portal_proxy(
conn: Arc<SyncConnection>,
timeout: Duration,
) -> Proxy<'static, Arc<SyncConnection>> {
Proxy::new(PORTAL_DEST, PORTAL_PATH, timeout, conn)
}
fn make_response_match(
conn: Arc<SyncConnection>,
responses: Arc<Mutex<HashMap<String, Result<PortalResponse, ScreenCastError>>>>,
request_path: String,
) -> Result<DbusFuture<MsgMatch>, ScreenCastError> {
let path = dbus::Path::new(request_path.clone()).map_err(ScreenCastError::InvalidObjectPath)?;
let rule = MatchRule::new_signal(REQUEST_IFACE, "Response")
.with_sender(PORTAL_DEST)
.with_path(path);
Ok(Box::pin(async move {
let match_handle = conn.add_match(rule).await?;
let key = request_path.clone();
let responses = responses.clone();
Ok(match_handle.msg_cb(move |msg: Message| {
let parsed = parse_portal_response_message(&msg);
lock_response_map(&responses).insert(key.clone(), parsed);
false
}))
}))
}
fn parse_portal_response_message(msg: &Message) -> Result<PortalResponse, ScreenCastError> {
let (response, results): (u32, arg::PropMap) = msg
.read2()
.map_err(|_| ScreenCastError::InvalidResponse("Request::Response signal"))?;
Ok(PortalResponse { response, results })
}
fn parse_start_response(results: &arg::PropMap) -> Result<ScreenCastResponse, ScreenCastError> {
Ok(ScreenCastResponse {
streams: parse_streams(results)?,
restore_token: optional_string(results, "restore_token"),
})
}
fn parse_streams(results: &arg::PropMap) -> Result<Vec<PipewireStream>, ScreenCastError> {
let streams = required_arg(results, "streams")?;
let streams = unvariant(streams);
let iter = streams
.as_iter()
.ok_or(ScreenCastError::InvalidResponse("streams array"))?;
let mut out = Vec::new();
for stream in iter {
let mut fields = stream
.as_iter()
.ok_or(ScreenCastError::InvalidResponse("stream tuple"))?;
let node_id = fields
.next()
.and_then(|v| v.as_u64())
.and_then(|v| u32::try_from(v).ok())
.ok_or(ScreenCastError::InvalidResponse("stream node id"))?;
let props = fields
.next()
.ok_or(ScreenCastError::InvalidResponse("stream properties"))?;
let position = dict_get(props, "position").and_then(tuple_i32);
let size = dict_get(props, "size").and_then(tuple_i32);
out.push(PipewireStream {
node_id,
position,
size,
});
}
if out.is_empty() {
return Err(ScreenCastError::InvalidResponse("empty streams array"));
}
Ok(out)
}
fn required_string(map: &arg::PropMap, key: &'static str) -> Result<String, ScreenCastError> {
optional_string(map, key).ok_or(ScreenCastError::MissingField(key))
}
fn optional_string(map: &arg::PropMap, key: &str) -> Option<String> {
map.get(key)
.and_then(|v| unvariant(&*v.0).as_str())
.map(str::to_owned)
}
fn required_arg<'a>(
map: &'a arg::PropMap,
key: &'static str,
) -> Result<&'a dyn RefArg, ScreenCastError> {
map.get(key)
.map(|v| &*v.0 as &dyn RefArg)
.ok_or(ScreenCastError::MissingField(key))
}
fn dict_get<'a>(dict: &'a dyn RefArg, key: &str) -> Option<&'a dyn RefArg> {
let dict = unvariant(dict);
let mut iter = dict.as_iter()?;
loop {
let k = iter.next()?;
let v = iter.next()?;
if k.as_str() == Some(key) {
return Some(unvariant(v));
}
}
}
fn tuple_i32(value: &dyn RefArg) -> Option<(i32, i32)> {
let value = unvariant(value);
let mut iter = value.as_iter()?;
let x = i32::try_from(iter.next()?.as_i64()?).ok()?;
let y = i32::try_from(iter.next()?.as_i64()?).ok()?;
Some((x, y))
}
fn unvariant(value: &dyn RefArg) -> &dyn RefArg {
if value.arg_type() != arg::ArgType::Variant {
return value;
}
value
.as_iter()
.and_then(|mut iter| iter.next())
.unwrap_or(value)
}
fn prop_insert<T>(map: &mut arg::PropMap, key: &str, value: T)
where
T: RefArg + 'static,
{
map.insert(key.to_owned(), arg::Variant(Box::new(value)));
}
fn choose_cursor_mode(available: u32, embed_mouse: bool) -> Option<u32> {
let choices: &[u32] = if embed_mouse {
// The portal names this METADATA; this is the closest "fallback" mode.
&[CURSOR_EMBEDDED, CURSOR_METADATA, CURSOR_HIDDEN]
} else {
&[CURSOR_HIDDEN]
};
choices.iter().copied().find(|mode| available & *mode != 0)
}
fn poll_one_shot<T>(value: &mut OneShot<T>) {
let old = std::mem::replace(
value,
OneShot::Failed(ScreenCastError::InvalidResponse(
"internal one-shot placeholder",
)),
);
*value = match old {
OneShot::Pending(mut future) => match poll_unpin(&mut future) {
Poll::Ready(Ok(value)) => OneShot::Ready(value),
Poll::Ready(Err(e)) => OneShot::Failed(e.into()),
Poll::Pending => OneShot::Pending(future),
},
other => other,
};
}
fn poll_unpin<F>(future: &mut F) -> Poll<F::Output>
where
F: Future + Unpin,
{
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
Pin::new(future).poll(&mut cx)
}
fn poll_boxed<T>(future: &mut DbusFuture<T>) -> Poll<Result<T, dbus::Error>> {
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
future.as_mut().poll(&mut cx)
}
fn noop_waker() -> Waker {
struct NoopWake;
impl Wake for NoopWake {
fn wake(self: Arc<Self>) {}
fn wake_by_ref(self: &Arc<Self>) {}
}
Waker::from(Arc::new(NoopWake))
}
fn sender_path_component(unique_name: &str) -> String {
let name = unique_name.strip_prefix(':').unwrap_or(unique_name);
let mut out = String::with_capacity(name.len());
for ch in name.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
out.push(ch);
} else {
out.push('_');
}
}
if out.is_empty() {
"unknown".to_owned()
} else {
out
}
}
fn lock_response_map(
responses: &Arc<Mutex<HashMap<String, Result<PortalResponse, ScreenCastError>>>>,
) -> MutexGuard<'_, HashMap<String, Result<PortalResponse, ScreenCastError>>> {
match responses.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
fn is_terminal(result: &ScreenCastResult) -> bool {
matches!(
result,
ScreenCastResult::Ok(_) | ScreenCastResult::Failed(_)
)
}
/// Helper function to make a single screen cast selection
pub fn screen_cast_select_blocking(
params: ScreenCastParams,
) -> Result<ScreenCastResponse, ScreenCastError> {
let mut manager = ScreenCastManager::new()?;
let request_id = manager.request(params)?;
loop {
match manager.check(&request_id) {
ScreenCastResult::Ok(response) => return Ok(response),
ScreenCastResult::Failed(error) => return Err(error),
ScreenCastResult::Queued
| ScreenCastResult::Pending
| ScreenCastResult::WaitingForUser => {
std::thread::sleep(Duration::from_millis(10));
}
}
}
}