// Stream settings — resolution / refresh / bitrate / gamepad / compositor / mic, written to // the flatpak client's JSON (main.py set_settings), which the client reads on launch. The // accepted gamepad/compositor names mirror punktfunk-core's `*Pref::from_name`. import { Dropdown, Field, SliderField, Spinner, ToggleField } from "@decky/ui"; import { CSSProperties, FC, useEffect, useState } from "react"; import { getSettings, setSettings, StreamSettings } from "./backend"; import { RowActions } from "./ui"; // Decky's Dropdown has no width prop — it fills whatever container it's in, and a // `childrenContainerWidth="max"` Field is the whole row. Wrapping it in this fit-content shell // (inside the right-aligned RowActions) shrinks the control to its selected label, with a floor // so short values like "60 Hz" don't collapse to a nub and a ceiling so nothing runs edge to // edge. Matches the right-aligned, content-sized buttons everywhere else. const selectShell: CSSProperties = { width: "fit-content", minWidth: "10em", maxWidth: "24em", }; const RESOLUTIONS: [number, number, string][] = [ [0, 0, "Native display"], [1280, 720, "1280 × 720"], [1280, 800, "1280 × 800 (Deck)"], [1920, 1080, "1920 × 1080"], [2560, 1440, "2560 × 1440"], ]; const REFRESH = [0, 30, 60, 90, 120]; // Render-resolution multipliers (mirrors punktfunk_core::render_scale::PRESETS). 1.0 = native. const RENDER_SCALES = [0.5, 0.67, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0]; const renderScaleLabel = (x: number): string => x === 1 ? "Native (1×)" : x > 1 ? `${x}× · supersample` : `${x}×`; const GAMEPADS = ["auto", "xbox360", "xboxone", "dualsense", "dualshock4", "steamdeck"]; const GAMEPAD_LABELS: Record = { auto: "Automatic", xbox360: "Xbox 360", xboxone: "Xbox One", dualsense: "DualSense", dualshock4: "DualShock 4", steamdeck: "Steam Deck", }; // Mirrors the desktop client's picker (ui_settings.rs CODECS) — a soft preference the host // falls back from when its GPU can't encode it. const CODECS = ["auto", "hevc", "h264", "av1"]; const CODEC_LABELS: Record = { auto: "Automatic", hevc: "HEVC (H.265)", h264: "H.264 (AVC)", av1: "AV1", }; const COMPOSITORS = ["auto", "kwin", "wlroots", "mutter", "gamescope"]; const COMPOSITOR_LABELS: Record = { auto: "Automatic", kwin: "KDE Plasma (KWin)", wlroots: "Sway (wlroots)", mutter: "GNOME (Mutter)", gamescope: "gamescope", }; export const SettingsSection: FC = () => { const [s, setS] = useState(null); useEffect(() => { void getSettings().then(setS); }, []); const patch = (p: Partial) => { setS((cur) => { if (!cur) return cur; const next = { ...cur, ...p }; void setSettings(next); return next; }); }; if (!s) return ; const resIdx = Math.max( 0, RESOLUTIONS.findIndex(([w, h]) => w === s.width && h === s.height), ); return ( <>
({ data: i, label }))} selectedOption={resIdx} onChange={(o) => { const [w, h] = RESOLUTIONS[o.data as number]; patch({ width: w, height: h }); }} />
({ data: r, label: r === 0 ? "Native" : `${r} Hz` }))} selectedOption={s.refresh_hz} onChange={(o) => patch({ refresh_hz: o.data as number })} />
({ data: x, label: renderScaleLabel(x) }))} // Snap the stored value to the nearest preset so the dropdown always shows a match. selectedOption={RENDER_SCALES.reduce((best, x) => Math.abs(x - (s.render_scale ?? 1)) < Math.abs(best - (s.render_scale ?? 1)) ? x : best, )} onChange={(o) => patch({ render_scale: o.data as number })} />
patch({ bitrate_kbps: v * 1000 })} />
({ data: c, label: CODEC_LABELS[c] ?? c }))} selectedOption={s.codec ?? "auto"} onChange={(o) => patch({ codec: o.data as string })} />
({ data: g, label: GAMEPAD_LABELS[g] ?? g }))} selectedOption={s.gamepad} onChange={(o) => patch({ gamepad: o.data as string })} />
{(s.gamepad === "steamdeck" || s.gamepad === "auto") && ( )}
({ data: c, label: COMPOSITOR_LABELS[c] ?? c }))} selectedOption={s.compositor} onChange={(o) => patch({ compositor: o.data as string })} />
patch({ mic_enabled: v })} /> ); };