diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a61b95..7cfac82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,3 +104,97 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + publish-linux-appimage: + name: Linux AppImage x86_64 + needs: publish-tauri + permissions: + contents: write + runs-on: ubuntu-22.04 + steps: + - name: Проверка исходного кода + uses: actions/checkout@v4 + + - name: Установка системных зависимостей + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev \ + patchelf libmpv-dev mpv ffmpeg mediainfo libfuse2 curl git cmake ninja-build \ + meson python3-dev cython3 nasm libvulkan-dev glslang-tools libavcodec-dev \ + libavformat-dev libavutil-dev libswresample-dev libswscale-dev libass-dev \ + libplacebo-dev libpipewire-0.3-dev libpulse-dev libx11-dev libwayland-dev \ + wayland-protocols libegl1-mesa-dev libzimg-dev + + - name: Настройка Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Настройка Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Кэширование сборок Rust + uses: swatinem/rust-cache@v2 + with: + workspaces: './src-tauri -> target' + + - name: Синхронизация версии + shell: bash + run: | + tag='${{ github.ref_name }}' + version="${tag#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + version="$(node -p "require('./package.json').version")" + fi + npm version "$version" --no-git-tag-version + sed -i -E "0,/^version = \"[^\"]+\"/s//version = \"$version\"/" src-tauri/Cargo.toml + echo "VERSION=$version" >> "$GITHUB_ENV" + + - name: Сборка самодостаточного AppImage + run: bash scripts/build-linux-appimage.sh + + - name: Публикация AppImage + run: | + tag='${{ github.ref_name }}' + [[ "$tag" =~ ^v[0-9] ]] || tag="v$VERSION" + gh release upload "$tag" dist-linux/*.AppImage --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-arch-linux: + name: Arch Linux package x86_64 + needs: publish-tauri + permissions: + contents: write + runs-on: ubuntu-latest + container: + image: archlinux:base-devel + steps: + - name: Установка инструментов CI + run: pacman -Syu --noconfirm git github-cli nodejs npm rust webkit2gtk-4.1 gtk3 ffmpeg mediainfo cmake ninja meson python cython vulkan-headers glslang libplacebo libass zimg patchelf + + - name: Проверка исходного кода + uses: actions/checkout@v4 + + - name: Сборка pacman-пакета + shell: bash + run: | + tag='${{ github.ref_name }}' + version="${tag#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + version="$(node -p "require('./package.json').version")" + fi + sed -i "s/^pkgver=.*/pkgver=$version/" packaging/arch/PKGBUILD + useradd -m builder + chown -R builder:builder "$GITHUB_WORKSPACE" + su builder -c "cd '$GITHUB_WORKSPACE/packaging/arch' && makepkg --printsrcinfo > .SRCINFO && makepkg --noconfirm --cleanbuild --nodeps" + echo "VERSION=$version" >> "$GITHUB_ENV" + + - name: Публикация Arch Linux пакета и PKGBUILD + shell: bash + run: | + tag='${{ github.ref_name }}' + [[ "$tag" =~ ^v[0-9] ]] || tag="v$VERSION" + gh release upload "$tag" packaging/arch/*.pkg.tar.zst packaging/arch/PKGBUILD packaging/arch/.SRCINFO packaging/arch/l-mpv.desktop --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index dcc86e4..ec807c3 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ Portable-L-MPV* *.tar.gz *.rar scratch/ +/dist-linux/ +/packaging/arch/*.pkg.tar.zst +/.runtime/ @@ -98,4 +101,3 @@ scratch/ .agents/rules/ agents/rules/ .codebase-memory/* - diff --git a/README.md b/README.md index 26247bf..f0600fc 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@

🎬 L-MPV — Modern & Portable Media Player

- Высокопроизводительный, эстетичный и портативный медиаплеер нового поколения.
- Построен на базе Tauri v2, React 19, Direct3D 11 и нативного движка libmpv (C-FFI) с поддержкой Real-Time 4K AI Upscaling. + Высокопроизводительный медиаплеер для Windows и Linux с нативным выводом видео.
+ Построен на базе Tauri v2, React 19 и libmpv с кроссплатформенным Real-Time 4K AI Upscaling.

@@ -16,8 +16,8 @@ TypeScript Rust libmpv - AI Upscale - Windows + AI Upscale + Windows and Linux Telegram Channel MIT License

@@ -26,9 +26,39 @@ ## 🌟 О проекте -**L-MPV** — это современный настольный медиаплеер для Windows, объединяющий эталонное качество воспроизведения нативного видеодвижка **MPV** (`vo=gpu-next`, Direct3D 11, HDR, WASAPI, студийный 32-tap sinc-ресемплинг), аппаратный **Real-Time 4K AI Апскейлинг** ( ONNX инференс через DirectML и TensorRT) с утонченным, отзывчивым интерфейсом в стиле **Glassmorphism**, созданным на **React 19** и **TypeScript**. - -Плеер спроектирован по строгой концепции **True Portable Architecture (Zero-Install)**: он полностью автономен, не привязан к системному реестру Windows и не создает мусор в системных каталогах пользователя. Все конфигурации, кэш, скриншоты, универсальная папка ONNX-моделей (`models/onnx/`) и нативные бинарные библиотеки (`libmpv-2.dll`, `ffmpeg.exe`, `mediainfo.dll`) расположены непосредственно в каталоге приложения. +**L-MPV** — современный настольный медиаплеер для Windows 10/11 и Linux x86_64. Windows использует прямое встраивание libmpv в HWND, Direct3D 11 и ONNX-инференс через DirectML/TensorRT. Linux выводит видео через libmpv render API в GTK `GLArea` на нативных Wayland и X11, а AI-апскейлинг выполняет через NCNN/Vulkan на NVIDIA, AMD и Intel. + +Windows-редакция сохраняет концепцию **True Portable Architecture (Zero-Install)**: настройки, скриншоты, ONNX-модели и нативные библиотеки расположены рядом с приложением. Linux следует XDG Base Directory, хранит пользовательские данные в `$XDG_CONFIG_HOME/l-mpv` (по умолчанию `~/.config/l-mpv`) и поставляется как AppImage либо пакет Arch Linux. + +## 🖥️ Совместимость платформ + +| Возможность | Windows 10/11 x64 | Linux x86_64 | +| :--- | :---: | :---: | +| Основное воспроизведение через libmpv | ✅ `libmpv-2.dll` | ✅ `libmpv.so` | +| Графическая сессия | ✅ Win32 / DWM | ✅ нативные Wayland и X11 | +| Нативный Wayland | — | ✅ GTK `GLArea` + libmpv render API | +| Аппаратное декодирование | ✅ D3D11VA / DXVA2 | ✅ VA-API / NVDEC — через mpv | +| GPU-рендеринг | ✅ D3D11 | ✅ OpenGL/EGL; Vulkan compute для NCNN | +| HDR и tone mapping | ✅ | ✅ Зависит от mpv, драйвера и окружения рабочего стола | +| AI-апскейлинг ONNX | ✅ | ❌ | +| DirectML | ✅ AMD / Intel / NVIDIA | ❌ Windows-only | +| TensorRT | ✅ NVIDIA | ❌ Текущая реализация Windows-only | +| NCNN / Vulkan AI | ❌ | ✅ NVIDIA / AMD / Intel, модели `.param` + `.bin` | +| Авто-fallback AI | ❌ | ✅ FSRCNNX / Anime4K при пропусках кадров | +| Шейдерный Ambient Light | ✅ | ✅ | +| WASAPI-аудиовизуализатор | ✅ | ❌ Windows-only | +| Скриншоты и копирование кадра | ✅ | ✅ | +| Извлечение дорожек через FFmpeg | ✅ Встроенный `ffmpeg.exe` | ✅ Встроенный в AppImage или системный `ffmpeg` | +| Подробный отчёт MediaInfo | ✅ `mediainfo.dll` | ✅ MediaInfo CLI | +| Плейлисты, главы и внешние дорожки | ✅ | ✅ | +| Несколько экземпляров приложения | ✅ | ✅ | +| Ассоциации файлов из настроек приложения | ✅ Реестр Windows | ❌ | +| Desktop-файл и MIME-типы | ❌ | ✅ При установке Arch-пакета | +| Встроенное portable-автообновление | ✅ | ❌ Обновление через релиз или пакетный менеджер | +| Расположение настроек | Каталог приложения | `$XDG_CONFIG_HOME/l-mpv` или `~/.config/l-mpv` | +| Форматы сборок | NSIS, portable ZIP, `.exe` | AppImage, Arch `pkg.tar.zst`, нативный бинарник | + +> **Примечание:** Linux использует libmpv render API, а не X11 Window ID, поэтому работает без XWayland. AI-фильтр принимает заранее конвертированные пары NCNN `.param`/`.bin`; ONNX, DirectML и TensorRT остаются Windows-функциями.

L-MPV Player Interface @@ -97,25 +127,28 @@

🚀 Real-Time 4K AI Upscaling (Нейросетевой Апскейлинг Аниме и Видео) -- **Нативный инференс в видеопотоке:** интеграция фильтра `vf_animejanai` на базе `the-database/mpv-AnimeJaNai` и моста `aji.dll` с аппаратным выводом кадров в 4K Ultra HD в реальном времени. +- **Нативный инференс в видеопотоке:** Windows использует `vf_animejanai`/`aji.dll`; Linux — приватный mpv/VapourSynth runtime и NCNN через Vulkan. - **Два режима работы:** переключатель **«Выкл»** и **«AI Upscaling»** в Настройках (`F2`) — пользователь сам решает, когда задействовать искусственный интеллект. -- **Два движка инференса (Backend):** +- **Windows-движки инференса (Backend):** - **DirectML:** универсальный инференс через DirectX 12 для любых видеокарт (AMD Radeon, Intel Arc/Iris, NVIDIA GeForce) с максимальной стабильностью. - **TensorRT:** максимальная скорость и частота кадров для видеокарт NVIDIA GeForce RTX (поддержка архитектур от `sm75` до новейшей Blackwell `sm120`). -- **Умный автозагрузчик компонентов («Скачать движок»):** +- **Linux backend — NCNN Vulkan:** заранее конвертированные пары `.param` + `.bin`, работающие на NVIDIA, AMD и Intel; при недостаточной скорости автоматически включаются FSRCNNX/Anime4K. +- **Умный автозагрузчик компонентов Windows («Скачать движок»):** - Автоматическое определение установленного GPU и скачивание точного набора библиотек. - Потоковая индикация процентов, объема (МБ), аккуратный статус распаковки и фиксация 100% готовности в течение 3 секунд. - Неблокирующая фоновая распаковка через `spawn_blocking` без зависаний UI и без всплывающих консольных окон. -- **Предварительная компиляция TensorRT 1080p (.engine):** +- **Предварительная компиляция TensorRT 1080p (.engine, Windows):** - Фоновая оптимизация ONNX моделей под разрешение 1080p -> 4K с многоступенчатым отслеживанием фаз (разбор ONNX, тактики CUDA, оптимизация графа, сериализация). - Компиляция через нативный `aji_harness.exe` с точным соответствием динамических осей `dyn-HW` и оптимизационных параметров. - Устранение задержек при первом включении видео и поддержка перекомпиляции в один клик. -- **Автоматическая FP16-нормализация и поддержка моделей всех поколений (V1, V2, V3):** +- **Автоматическая FP16-нормализация ONNX (Windows):** - Встроенный алгоритм аппаратной валидации и автоконвертации типов ONNX: модели в формате FP32 автоматически приводятся к стандарту IEEE Float16 перед компиляцией. - Полное устранение артефактов «радужного шума» и расхождения памяти шейдера: идеальная четкость и чистота картинки как на компактных сетях V2, так и на тяжелых моделях V1 и HD V3 / V3Sharp1. -- **Универсальная библиотека ONNX (`models/onnx/`):** - - Возможность использования любых сторонних моделей формата `.onnx`. - - Кнопка **«Папка моделей»** для быстрого перехода в Проводник Windows. +- **Библиотека моделей:** + - Windows: ONNX в `models/onnx/`. + - Linux: NCNN `.param` + `.bin` в `models/ncnn/`, включая загрузку проверенной AnimeJaNai V2 из интерфейса. + - Windows принимает сторонние модели `.onnx`; Linux требует заранее конвертированную совместимую пару `.param` + `.bin` с масштабом 2×. + - Кнопка **«Папка моделей»** открывает каталог в Проводнике Windows или через `xdg-open` в Linux. - Переключатель скрытия/отображения названий моделей (маскировка точками). - **Переключение нейросетей на лету по горячим клавишам:** - `Ctrl+J` — Информационный OSD-оверлей статуса и статистики апскейлинга (активный движок, выбранная модель, слот, разрешение видео -> 4K); @@ -129,13 +162,13 @@
-⚡ Рендеринг Нового Поколения (vo=gpu-next) +⚡ Нативный GPU-рендеринг через libmpv -- **GPU-HQ пайплайн:** профиль `profile=gpu-hq`, нативный Direct3D 11 (`gpu-api=d3d11`) и аппаратное декодирование `hwdec=auto-safe`. +- **GPU-HQ пайплайн:** Direct3D 11 в Windows; OpenGL/EGL через libmpv render API в Linux; аппаратное декодирование `hwdec=auto-safe` вне AI-фильтра. - **Прецизионное масштабирование:** алгоритмы интерполяции `scale=spline36` и `cscale=spline36` для идеальной четкости деталей и цветовых переходов. - **Интеллектуальный HDR:** автоматическая передача метаданных в дисплей (`target-colorspace-hint=yes`), динамический расчет пиков яркости (`hdr-compute-peak=yes`) и адаптивный tone mapping. - **Zero-Flicker жизненный цикл:** окно создается в скрытом режиме, рассчитывает истинный Display Aspect Ratio видео, центрируется и отображается (`window.show()`) строго тогда, когда интерфейс и первый видеокадр готовы к показу, исключая любые рывки геометрии и мерцания. -- **Прямой вывод в окно:** видеопоток отрисовывается в нативный Win32 HWND через C-FFI с нулевой задержкой под полностью прозрачным DOM-слоем WebView2 (`transparent: true`). +- **Прямой вывод в окно:** Win32 HWND/WebView2 в Windows; GTK `GLArea` внутри `GtkOverlay` в нативных Wayland и X11 в Linux.
@@ -168,7 +201,7 @@
🎧 Студийный Аудиофильский Звук и Усиление до 150% (Audiophile Profile) -- **Низколатентный вывод:** нативный драйвер Windows WASAPI (`ao=wasapi`) с оптимизированным буфером 0.2 с. +- **Низколатентный вывод:** Windows использует WASAPI (`ao=wasapi`), Linux выбирает PipeWire, PulseAudio или ALSA; аудиобуфер настроен на 0.2 с. - **Софтверный буст громкости (до 150%):** возможность усиления тихих аудиодорожек до 150% без искажений и клиппинга (`volume-max=150.0`). - **Студийный 32-точечный sinc-ресемплинг:** фильтр `audio-resample-filter-size=32`, 16 384 фазы сдвига (`audio-resample-phase-shift=14`) и линейная интерполяция между отсчетами (`audio-resample-linear=yes`). - **Нормализованный даунмикс:** автоматическое безопасное сведение многоканального аудио 5.1/7.1 в стерео (`audio-normalize-downmix=yes`) с аппаратной защитой от перегрузок и клиппинга. @@ -307,37 +340,37 @@ Backend & Shell Rust (2021 edition), Tauri v2, Tokio - Низкоуровневая интеграция с Win32 API, многопоточный IPC-мост, управление окнами и DWM + IPC-мост, управление окнами, Win32/DWM в Windows и GTK/WebKitGTK в Linux Media Engine - libmpv-2.dll (сборка the-database/mpv-winbuild) via dynamic FFI (libloading) - Аппаратный рендеринг vo=gpu-next, Direct3D 11, HDR tone-mapping, demuxing + libmpv-2.dll в Windows; приватный libmpv.so.2 и render API в Linux + Direct3D 11/HWND либо OpenGL/EGL/GTK GLArea, HDR tone mapping и demuxing AI Upscaling - vf_animejanai + aji.dll (DirectML / TensorRT) + ONNX - Аппаратный апскейлинг видео в реальном времени до 4K на любых GPU + DirectML/TensorRT + ONNX в Windows; VapourSynth + NCNN/Vulkan в Linux + Аппаратный апскейлинг 2× и автоматический fallback на FSRCNNX/Anime4K в Linux MediaInfo Engine - mediainfo.dll via dynamic C-FFI + mediainfo.dll через C-FFI либо MediaInfo CLI Извлечение исчерпывающего технического отчёта о видеоконтейнере и потоках данных Audio Engine - Windows WASAPI, 32-tap Sinc Resampler, Scaletempo2 + WASAPI либо PipeWire/PulseAudio/ALSA, 32-tap Sinc Resampler, Scaletempo2 Студийный 32-точечный ресемплинг, безопасный даунмикс 5.1/7.1 в стерео, pitch correction Track Extraction - Встроенный FFmpeg (ffmpeg.exe) + Встроенный FFmpeg (ffmpeg.exe или AppImage ffmpeg) Прямой экспорт потоков аудио/субтитров (-c copy) и интеллектуальный fallback-транскодинг Платформа - Windows 10 / 11 x64 - Аппаратное ускорение DXVA2/D3D11VA, Windows Explorer Context Menu API, Taskbar API + Windows 10/11 x64, Linux x86_64 (Wayland/X11) + D3D11VA/DXVA2 либо VA-API/NVDEC; платформенные интеграции включаются условно @@ -355,11 +388,11 @@ L-MPV/ │ │ ├── PlayerControls.tsx # Плавающая панель управления (кнопки, громкость, треки, скорость) │ │ ├── ContextMenu.tsx # Кастомное ПКМ-меню (масштаб, пропорции, поворот, дорожки, подсветка) │ │ ├── SettingsModal.tsx # Настройки (скриншоты, цвета, подсветка полос, апскейлинг, хоткеи) -│ │ ├── UpscalingSettingsSection.tsx # Управление 4K AI апскейлингом и библиотекой ONNX-моделей +│ │ ├── UpscalingSettingsSection.tsx # Управление AI-апскейлингом и библиотекой ONNX/NCNN-моделей │ │ ├── upscale/ # Модульные подкомпоненты апскейлинга: │ │ │ ├── types.ts # Модели данных и событий прогресса │ │ │ ├── GpuHardwareCard.tsx # Информационная карточка обнаруженного GPU (VRAM, архитектура, рекомендации) -│ │ │ └── BackendSelector.tsx # Селектор DirectML / TensorRT, скачивание движков, удаление, индикация прогресса +│ │ │ └── BackendSelector.tsx # DirectML / TensorRT / NCNN Vulkan и состояние runtime │ │ ├── MediaInfoModal.tsx # Компактное окно технической информации о медиафайле │ │ ├── StandaloneMediaInfoWindow.tsx # Автономное окно просмотра MediaInfo из проводника │ │ ├── ChaptersModal.tsx # Модальное окно навигации по главам видео @@ -397,24 +430,31 @@ L-MPV/ ├── src-tauri/ # Бэкенд (Rust + Tauri v2) │ ├── capabilities/default.json # Манифест разрешений Tauri v2 │ ├── src/ -│ │ ├── main.rs # Точка входа приложения -│ │ ├── lib.rs # Инициализация Tauri, HWND-привязка, фокус и реестр 103 IPC-команд +│ │ ├── main.rs # Точка входа и настройка приватного Linux runtime +│ │ ├── lib.rs # Инициализация Tauri, HWND либо GTK GLArea и IPC-команды │ │ ├── upscale/ # Модульная подсистема 4K AI апскейлинга: │ │ │ ├── mod.rs # Фасад подсистемы, IPC-команды, unit-тесты │ │ │ ├── types.rs # Модели данных: ModelFileItem, UpscaleSettings, GpuHardwareInfo, Progress -│ │ │ ├── hardware.rs # Диагностика GPU через Win32 DXGI, определение SM-архитектуры NVIDIA -│ │ │ ├── config.rs # Разрешение путей, окружение DLL PATH, сканирование models/onnx/, upscale.conf +│ │ │ ├── hardware.rs # Диагностика GPU через DXGI либо vulkaninfo/lspci +│ │ │ ├── config.rs # Пути и сканирование models/onnx либо models/ncnn │ │ │ ├── downloader.rs # Асинхронная потоковая загрузка DirectML/TensorRT, распаковка архивов -│ │ │ └── controller.rs # Управление libmpv фильтром, фоновая компиляция TensorRT .engine, хоткеи +│ │ │ ├── controller.rs # Платформенная маршрутизация AI-фильтров и хоткеи +│ │ │ └── linux.rs # VapourSynth/NCNN, загрузка модели и watchdog fallback │ │ ├── ambient.rs # Контроллер подсветки черных полос (GPU Blur / Color / Off) │ │ ├── audio_capture.rs # Нативный захват звука WASAPI Loopback, быстрый БПФ (FFT Radix-2), 32 полосы -│ │ ├── mediainfo.rs # FFI-интеграция с mediainfo.dll и управление автономным окном -│ │ ├── mpv_manager.rs # FFI-обертка libmpv (vo=gpu-next, WASAPI, D3D11, vf_animejanai, sinc-фильтр) +│ │ ├── audio_capture_stub.rs # Linux-заглушка Windows-only WASAPI-визуализатора +│ │ ├── mediainfo.rs # mediainfo.dll в Windows и MediaInfo CLI в Linux +│ │ ├── mpv_manager.rs # Динамический libmpv API, HWND и OpenGL render context │ │ ├── system_integration.rs # Интеграция с Проводником Windows (контекстное меню, ассоциации файлов) │ │ ├── updater.rs # Модуль фонового и ручного обновления │ │ └── commands.rs # IPC #[tauri::command] обработчики, экспорт дорожек через FFmpeg │ ├── Cargo.toml # Зависимости бэкенда Rust -│ └── tauri.conf.json # Конфигурация Tauri v2 +│ ├── tauri.conf.json # Основная конфигурация Tauri v2 +│ └── tauri.linux.conf.json # Linux-конфигурация окна и AppImage +├── native/linux-ncnn/ # VapourSynth API 4 плагин NCNN/Vulkan +├── scripts/ # Сборка приватного runtime и AppImage +├── packaging/arch/ # PKGBUILD, .SRCINFO и desktop-файл +├── vendor/tauri-runtime-wry/ # GTK-патч иерархии GtkOverlay/GLArea ├── models/ # Каталог нейросетей │ └── onnx/ # Универсальная папка для размещения ONNX-моделей ├── inference/ # Папка библиотек инференса (aji.dll, DirectML, TensorRT) @@ -478,9 +518,10 @@ L-MPV/ ## ⚡ IPC-Архитектура (Rust ↔ React) -Связь интерфейса React с движком MPV, подсистемой апскейлинга и системными модулями осуществляется через **91 нативную IPC-команду**, гарантирующую мгновенный отклик и отсутствие задержек: +Связь интерфейса React с движком MPV, подсистемой апскейлинга и системными модулями осуществляется через нативные IPC-команды Tauri: -- **Апскейлинг и AI Модели (6 команд):** `get_upscale_status`, `scan_onnx_models`, `open_models_folder`, `apply_upscale_settings`, `download_recommended_models`, `switch_upscale_network_hotkey`. +- **Апскейлинг и AI-модели:** `get_upscale_status`, `scan_onnx_models`, `open_models_folder`, `apply_upscale_settings`, `download_recommended_models`, `download_curated_ncnn_model`, `switch_upscale_network_hotkey`. +- **Определение платформы:** `get_runtime_platform` адаптирует интерфейс без изменения поведения Windows-сборки. - **Воспроизведение и Плейлист (16 команд):** `open_file`, `toggle_pause`, `set_pause`, `seek`, `seek_absolute`, `frame_step`, `frame_back_step`, `playlist_prev`, `playlist_next`, `get_playlist`, `play_playlist_item`, `set_loop_file`, `set_loop_playlist`, `toggle_shuffle`, `get_play_next_on_end`, `set_play_next_on_end`. - **Громкость и Скорость (2 команды):** `set_volume`, `set_speed`. - **Дорожки и FFmpeg Извлечение (11 команд):** `get_tracks`, `set_audio_track`, `set_subtitle_track`, `disable_subtitles`, `load_subtitle_file`, `load_audio_file`, `set_video_track`, `extract_track`, `get_auto_load_tracks`, `set_auto_load_tracks`, `load_external_tracks_for_file`. @@ -500,9 +541,74 @@ L-MPV/ ### Требования к окружению - **Node.js** v20+ и менеджер пакетов **npm** -- **Rust** (toolchain `stable-x86_64-pc-windows-msvc`) -- Нативная библиотека `libmpv-2.dll` из релиза `the-database/mpv-winbuild` (с фильтром `vf_animejanai`) -- Утилита `ffmpeg.exe` для прямого экспорта дорожек и `mediainfo.dll` для детального анализа +- **Rust stable** (`x86_64-pc-windows-msvc` или `x86_64-unknown-linux-gnu`) +- Windows: `libmpv-2.dll`, `ffmpeg.exe` и `mediainfo.dll` +- Linux: GTK 3, WebKitGTK 4.1, FFmpeg, MediaInfo и Vulkan-драйвер GPU + +### Linux + +В релизах публикуются два варианта для `x86_64`: + +- самодостаточный `AppImage` с приватными libmpv 0.41, VapourSynth R79, NCNN/Vulkan, Python runtime, FFmpeg, MediaInfo и fallback-шейдерами; +- пакет Arch Linux `pkg.tar.zst` с тем же приватным runtime в `/usr/lib/l-mpv` и desktop/MIME-интеграцией. + +AppImage включает пользовательские библиотеки, но не поставляет драйвер GPU: в системе должен быть установлен рабочий Vulkan ICD для NVIDIA, AMD или Intel. Воспроизведение работает нативно в Wayland и X11 через GTK `GLArea` и libmpv render API, без обязательного XWayland. + +#### Установка и запуск + +```bash +# AppImage +chmod +x L-MPV-*-linux-x86_64.AppImage +./L-MPV-*-linux-x86_64.AppImage + +# Arch Linux +sudo pacman -U l-mpv-*-x86_64.pkg.tar.zst +``` + +Пользовательские настройки, история, скриншоты и модели находятся в +`$XDG_CONFIG_HOME/l-mpv` или `~/.config/l-mpv`. Linux-версия не изменяет системный +реестр и не использует portable-настройки из каталога исполняемого файла. + +#### NCNN/Vulkan AI + +1. Откройте **Настройки → Апскейлинг**. +2. Нажмите **AnimeJaNai V2**, чтобы скачать проверенную модель с валидацией SHA-256, либо поместите собственную совместимую пару `имя.param` + `имя.bin` в `~/.config/l-mpv/models/ncnn/`. +3. Выберите модель и включите **AI Upscaling**. Модель должна принимать RGB float и выдавать изображение с масштабом 2×. + +Во время работы VapourSynth передаёт кадры через системную память, поэтому аппаратное +декодирование временно отключается. Watchdog отслеживает счётчики пропущенных кадров; +если NCNN не справляется с воспроизведением, фильтр автоматически снимается и включаются +пакетные FSRCNNX/Anime4K-шейдеры с уведомлением в OSD. DirectML, TensorRT и +WASAPI-визуализатор остаются Windows-only. + +#### Сборка из исходников + +Версии ключевых компонентов закреплены в `scripts/build-linux-runtime.sh`: mpv 0.41, +VapourSynth R79, NCNN и GLSL-шейдеры собираются из фиксированных ревизий. + +Сборка приватного runtime: + +```bash +bash scripts/build-linux-runtime.sh +``` + +Сборка готового AppImage в `dist-linux/`: + +```bash +bash scripts/build-linux-appimage.sh +``` + +Сборка пакета Arch Linux: + +```bash +cd packaging/arch +makepkg -s +``` + +Для разработки необходимы GTK 3, WebKitGTK 4.1, OpenGL/EGL и Vulkan headers/loader. +Полная сборка runtime дополнительно использует CMake, Ninja, Meson, Python/Cython, +FFmpeg development libraries, libplacebo, libass и zimg. Актуальный список пакетов +для Ubuntu 22.04 и Arch Linux приведён в `.github/workflows/release.yml`. ### Режим разработки (Development) ```bash @@ -557,4 +663,3 @@ Copy-Item -Path "src-tauri/target/release/l-mpv.exe" -Destination "Portable-L-MP ## 📄 Лицензия Проект распространяется под свободной лицензией **MIT**. Подробная информация доступна в файле [LICENSE](./LICENSE). - diff --git a/native/linux-ncnn/CMakeLists.txt b/native/linux-ncnn/CMakeLists.txt new file mode 100644 index 0000000..20233f5 --- /dev/null +++ b/native/linux-ncnn/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.20) +project(lmpv_ncnn LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +find_package(ncnn CONFIG REQUIRED) +find_path(VAPOURSYNTH_INCLUDE_DIR VapourSynth4.h PATH_SUFFIXES vapoursynth REQUIRED) + +add_library(lmpv_ncnn SHARED lmpv_ncnn.cpp) +target_include_directories(lmpv_ncnn PRIVATE "${VAPOURSYNTH_INCLUDE_DIR}") +target_link_libraries(lmpv_ncnn PRIVATE ncnn) +set_target_properties(lmpv_ncnn PROPERTIES + PREFIX "lib" + OUTPUT_NAME "lmpv_ncnn" + INSTALL_RPATH "$ORIGIN/../lib" +) + +install(TARGETS lmpv_ncnn LIBRARY DESTINATION vapoursynth) diff --git a/native/linux-ncnn/lmpv_ncnn.cpp b/native/linux-ncnn/lmpv_ncnn.cpp new file mode 100644 index 0000000..c5ef5e4 --- /dev/null +++ b/native/linux-ncnn/lmpv_ncnn.cpp @@ -0,0 +1,179 @@ +// L-MPV NCNN/Vulkan VapourSynth filter. +// Accepts planar RGB float frames and preconverted NCNN .param/.bin models. + +#include +#include +#include +#include + +#include +#include +#include +#include + +struct FilterData { + VSNode *node{}; + VSVideoInfo vi{}; + ncnn::Net net; + int input_index{}; + int output_index{}; + int scale{2}; + int tile_size{256}; + int overlap{16}; +}; + +static void set_filter_error(VSFrameContext *ctx, const VSAPI *vsapi, const std::string &message) { + vsapi->setFilterError(("L-MPV NCNN: " + message).c_str(), ctx); +} + +static const VSFrame *VS_CC get_frame(int n, int activation_reason, void *instance_data, + void **, VSFrameContext *frame_ctx, VSCore *core, + const VSAPI *vsapi) { + auto *d = static_cast(instance_data); + if (activation_reason == arInitial) { + vsapi->requestFrameFilter(n, d->node, frame_ctx); + return nullptr; + } + if (activation_reason != arAllFramesReady) + return nullptr; + + const VSFrame *src = vsapi->getFrameFilter(n, d->node, frame_ctx); + const int src_w = vsapi->getFrameWidth(src, 0); + const int src_h = vsapi->getFrameHeight(src, 0); + const int dst_w = src_w * d->scale; + const int dst_h = src_h * d->scale; + VSFrame *dst = vsapi->newVideoFrame(&d->vi.format, dst_w, dst_h, src, core); + + const int tile = std::max(32, d->tile_size); + const int overlap = std::clamp(d->overlap, 0, tile / 3); + const int step = std::max(1, tile - overlap * 2); + + for (int top = 0;; top = std::min(top + step, std::max(0, src_h - tile))) { + const int tile_h = std::min(tile, src_h - top); + for (int left = 0;; left = std::min(left + step, std::max(0, src_w - tile))) { + const int tile_w = std::min(tile, src_w - left); + ncnn::Mat input(tile_w, tile_h, 3, sizeof(float), 1); + for (int plane = 0; plane < 3; ++plane) { + const auto *source = reinterpret_cast(vsapi->getReadPtr(src, plane)); + const ptrdiff_t stride = vsapi->getStride(src, plane) / sizeof(float); + float *target = input.channel(plane); + for (int y = 0; y < tile_h; ++y) + std::copy_n(source + (top + y) * stride + left, tile_w, target + y * tile_w); + } + + ncnn::Extractor extractor = d->net.create_extractor(); + if (extractor.input(d->input_index, input) != 0) { + vsapi->freeFrame(src); + vsapi->freeFrame(dst); + set_filter_error(frame_ctx, vsapi, "failed to upload an input tile"); + return nullptr; + } + ncnn::Mat output; + if (extractor.extract(d->output_index, output) != 0 || output.empty()) { + vsapi->freeFrame(src); + vsapi->freeFrame(dst); + set_filter_error(frame_ctx, vsapi, "Vulkan inference failed"); + return nullptr; + } + if (output.w != tile_w * d->scale || output.h != tile_h * d->scale || output.c < 3) { + vsapi->freeFrame(src); + vsapi->freeFrame(dst); + set_filter_error(frame_ctx, vsapi, "model output shape does not match the selected 2x scale"); + return nullptr; + } + + const int crop_l = left == 0 ? 0 : overlap * d->scale; + const int crop_t = top == 0 ? 0 : overlap * d->scale; + const int crop_r = left + tile_w == src_w ? 0 : overlap * d->scale; + const int crop_b = top + tile_h == src_h ? 0 : overlap * d->scale; + for (int plane = 0; plane < 3; ++plane) { + auto *target = reinterpret_cast(vsapi->getWritePtr(dst, plane)); + const ptrdiff_t stride = vsapi->getStride(dst, plane) / sizeof(float); + const float *source = output.channel(plane); + for (int y = crop_t; y < output.h - crop_b; ++y) { + std::copy_n(source + y * output.w + crop_l, output.w - crop_l - crop_r, + target + (top * d->scale + y) * stride + left * d->scale + crop_l); + } + } + + if (left + tile_w == src_w) + break; + } + if (top + tile_h == src_h) + break; + } + + vsapi->freeFrame(src); + return dst; +} + +static void VS_CC free_filter(void *instance_data, VSCore *, const VSAPI *vsapi) { + auto *d = static_cast(instance_data); + vsapi->freeNode(d->node); + delete d; +} + +static void VS_CC create_filter(const VSMap *in, VSMap *out, void *, VSCore *core, + const VSAPI *vsapi) { + auto d = std::make_unique(); + d->node = vsapi->mapGetNode(in, "clip", 0, nullptr); + const VSVideoInfo *input_vi = vsapi->getVideoInfo(d->node); + if (!vsh::isConstantVideoFormat(input_vi) || input_vi->format.colorFamily != cfRGB || + input_vi->format.sampleType != stFloat || input_vi->format.bitsPerSample != 32) { + vsapi->mapSetError(out, "L-MPV NCNN: input must be constant RGBS"); + vsapi->freeNode(d->node); + return; + } + + int error = 0; + const char *param_path = vsapi->mapGetData(in, "param_path", 0, &error); + const char *model_path = vsapi->mapGetData(in, "model_path", 0, &error); + d->scale = static_cast(vsapi->mapGetInt(in, "scale", 0, &error)); + if (error) d->scale = 2; + d->tile_size = static_cast(vsapi->mapGetInt(in, "tile_size", 0, &error)); + if (error) d->tile_size = 256; + d->overlap = static_cast(vsapi->mapGetInt(in, "overlap", 0, &error)); + if (error) d->overlap = 16; + const bool fp16 = !!vsapi->mapGetInt(in, "fp16", 0, &error); + int device_error = 0; + const int requested_device = static_cast(vsapi->mapGetInt(in, "device_id", 0, &device_error)); + + ncnn::create_gpu_instance(); + const int device_id = device_error ? ncnn::get_default_gpu_index() : requested_device; + ncnn::VulkanDevice *device = ncnn::get_gpu_device(device_id); + if (!device) { + vsapi->mapSetError(out, "L-MPV NCNN: no Vulkan compute device found"); + vsapi->freeNode(d->node); + return; + } + d->net.opt.use_vulkan_compute = true; + d->net.opt.use_fp16_packed = fp16; + d->net.opt.use_fp16_storage = fp16; + d->net.set_vulkan_device(device); + if (d->net.load_param(param_path) != 0 || d->net.load_model(model_path) != 0 || + d->net.input_indexes().empty() || d->net.output_indexes().empty()) { + vsapi->mapSetError(out, "L-MPV NCNN: failed to load .param/.bin model pair"); + vsapi->freeNode(d->node); + return; + } + d->input_index = d->net.input_indexes().front(); + d->output_index = d->net.output_indexes().front(); + d->vi = *input_vi; + d->vi.width *= d->scale; + d->vi.height *= d->scale; + + FilterData *data = d.release(); + VSFilterDependency dependency[] = {{data->node, rpStrictSpatial}}; + vsapi->createVideoFilter(out, "LmpvNcnn", &data->vi, get_frame, free_filter, + fmParallelRequests, dependency, 1, data, core); +} + +VS_EXTERNAL_API(void) VapourSynthPluginInit2(VSPlugin *plugin, const VSPLUGINAPI *vspapi) { + vspapi->configPlugin("io.github.menely.lmpv.ncnn", "lmpvncnn", + "L-MPV NCNN Vulkan Upscaler", VS_MAKE_VERSION(1, 0), + VAPOURSYNTH_API_VERSION, 0, plugin); + vspapi->registerFunction( + "Model", + "clip:vnode;param_path:data;model_path:data;scale:int:opt;tile_size:int:opt;overlap:int:opt;fp16:int:opt;device_id:int:opt;", + "clip:vnode;", create_filter, nullptr, plugin); +} diff --git a/packaging/arch/.SRCINFO b/packaging/arch/.SRCINFO new file mode 100644 index 0000000..0472581 --- /dev/null +++ b/packaging/arch/.SRCINFO @@ -0,0 +1,33 @@ +pkgbase = l-mpv + pkgdesc = Modern desktop media player powered by Tauri and libmpv + pkgver = 2.0.2 + pkgrel = 1 + url = https://github.com/Menely/L-MPV + arch = x86_64 + license = MIT + makedepends = cargo + makedepends = npm + makedepends = rust + makedepends = git + makedepends = cmake + makedepends = ninja + makedepends = meson + makedepends = cython + makedepends = vulkan-headers + makedepends = glslang + makedepends = patchelf + depends = ffmpeg + depends = mediainfo + depends = webkit2gtk-4.1 + depends = gtk3 + depends = python + depends = vulkan-icd-loader + depends = libplacebo + depends = libass + depends = zimg + source = l-mpv-2.0.2.tar.gz::https://github.com/Menely/L-MPV/archive/refs/tags/v2.0.2.tar.gz + source = l-mpv.desktop + sha256sums = SKIP + sha256sums = SKIP + +pkgname = l-mpv diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..3a2d1b0 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,39 @@ +# Maintainer: L-MPV contributors +pkgname=l-mpv +pkgver=2.0.2 +pkgrel=1 +pkgdesc='Modern desktop media player powered by Tauri and libmpv' +arch=('x86_64') +url='https://github.com/Menely/L-MPV' +license=('MIT') +depends=('ffmpeg' 'mediainfo' 'webkit2gtk-4.1' 'gtk3' 'python' 'vulkan-icd-loader' 'libplacebo' 'libass' 'zimg') +makedepends=('cargo' 'npm' 'rust' 'git' 'cmake' 'ninja' 'meson' 'cython' 'vulkan-headers' 'glslang' 'patchelf') +source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz" + 'l-mpv.desktop') +sha256sums=('SKIP' 'SKIP') + +prepare() { + cd "L-MPV-$pkgver" + npm version "$pkgver" --no-git-tag-version + sed -i -E "0,/^version = \"[^\"]+\"/s//version = \"$pkgver\"/" src-tauri/Cargo.toml + npm ci +} + +build() { + cd "L-MPV-$pkgver" + L_MPV_RUNTIME_BUILD_DIR="$srcdir/runtime-build" \ + L_MPV_RUNTIME_PREFIX="$srcdir/runtime" \ + bash scripts/build-linux-runtime.sh + npm run build + cargo build --manifest-path src-tauri/Cargo.toml --release +} + +package() { + cd "L-MPV-$pkgver" + install -Dm755 src-tauri/target/release/l-mpv "$pkgdir/usr/bin/l-mpv" + install -d "$pkgdir/usr/lib/l-mpv" + cp -a "$srcdir/runtime/." "$pkgdir/usr/lib/l-mpv/" + install -Dm644 src-tauri/icons/128x128.png "$pkgdir/usr/share/icons/hicolor/128x128/apps/l-mpv.png" + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + install -Dm644 "$srcdir/l-mpv.desktop" "$pkgdir/usr/share/applications/l-mpv.desktop" +} diff --git a/packaging/arch/l-mpv.desktop b/packaging/arch/l-mpv.desktop new file mode 100644 index 0000000..ebf2efa --- /dev/null +++ b/packaging/arch/l-mpv.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=L-MPV +Comment=Modern media player powered by libmpv +Exec=l-mpv %U +Icon=l-mpv +Terminal=false +Categories=AudioVideo;Player; +MimeType=video/mp4;video/x-matroska;video/x-msvideo;video/quicktime;video/webm;video/mp2t;video/mpeg;audio/mpeg;audio/flac;audio/x-wav;audio/aac;audio/ogg;audio/mp4;audio/opus; +StartupNotify=true diff --git a/scripts/build-linux-appimage.sh b/scripts/build-linux-appimage.sh new file mode 100755 index 0000000..10b434b --- /dev/null +++ b/scripts/build-linux-appimage.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VERSION="${VERSION:-$(node -p "require('${ROOT_DIR}/package.json').version")}" +ARCH="${ARCH:-x86_64}" +TOOLS_DIR="${RUNNER_TEMP:-/tmp}/l-mpv-appimage-tools" +OUTPUT_DIR="${ROOT_DIR}/dist-linux" + +mkdir -p "${TOOLS_DIR}" "${OUTPUT_DIR}" +cd "${ROOT_DIR}" + +RUNTIME_DIR="${L_MPV_RUNTIME_PREFIX:-${ROOT_DIR}/.runtime/linux}" +if [[ ! -f "${RUNTIME_DIR}/lib/libmpv.so.2" && ! -f "${RUNTIME_DIR}/lib64/libmpv.so.2" ]]; then + L_MPV_RUNTIME_PREFIX="${RUNTIME_DIR}" bash scripts/build-linux-runtime.sh +fi + +npm ci +export NO_STRIP=1 +TAURI_APPDIR="${ROOT_DIR}/src-tauri/target/release/bundle/appimage/L-MPV.AppDir" +if ! npm run tauri -- build --bundles appimage; then + # Newer appimagetool validates icon filename casing after linuxdeploy has + # already produced a complete AppDir. Reuse that staging tree and let the + # final packaging pass below normalize the icon and create the image. + if [[ ! -x "${TAURI_APPDIR}/usr/bin/l-mpv" ]]; then + echo 'Tauri failed before producing a usable AppDir' >&2 + exit 1 + fi + echo 'Tauri AppImage wrapper failed; continuing from its completed AppDir' >&2 +fi + +BASE_IMAGE="$(find src-tauri/target/release/bundle/appimage -maxdepth 1 -name '*.AppImage' -print -quit)" + +rm -rf "${TOOLS_DIR}/squashfs-root" +cd "${TOOLS_DIR}" +if [[ -n "${BASE_IMAGE}" ]]; then + BASE_IMAGE="$(realpath "${BASE_IMAGE}")" + "${BASE_IMAGE}" --appimage-extract >/dev/null +else + cp -a "${TAURI_APPDIR}" squashfs-root +fi + +# appimagetool requires the root icon filename to match Desktop Entry Icon=. +DESKTOP_FILE="$(find squashfs-root -maxdepth 1 -name '*.desktop' -print -quit)" +if [[ -n "${DESKTOP_FILE}" ]]; then + ICON_NAME="$(sed -n 's/^Icon=//p' "${DESKTOP_FILE}" | head -1)" + if [[ -n "${ICON_NAME}" && ! -e "squashfs-root/${ICON_NAME}.png" ]]; then + ROOT_ICON="$(find squashfs-root -maxdepth 1 -iname '*.png' -print -quit)" + [[ -z "${ROOT_ICON}" ]] || cp "${ROOT_ICON}" "squashfs-root/${ICON_NAME}.png" + fi +fi + +install -Dm755 "$(command -v ffmpeg)" squashfs-root/usr/bin/ffmpeg +install -Dm755 "$(command -v mediainfo)" squashfs-root/usr/bin/mediainfo + +LIBMPV="$(find "${RUNTIME_DIR}" -name 'libmpv.so.2' -print -quit)" +if [[ -z "${LIBMPV}" ]]; then + echo 'libmpv was not found by ldconfig' >&2 + exit 1 +fi + +LINUXDEPLOY="${TOOLS_DIR}/linuxdeploy-${ARCH}.AppImage" +if [[ ! -x "${LINUXDEPLOY}" ]]; then + curl --fail --location --retry 3 \ + "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage" \ + --output "${LINUXDEPLOY}" + chmod +x "${LINUXDEPLOY}" +fi + +export OUTPUT="${OUTPUT_DIR}/L-MPV-v${VERSION}-linux-${ARCH}.AppImage" + +# Private runtime: custom libmpv with VapourSynth, NCNN/Vulkan plugin and +# packaged shader fallback. Keep it isolated from host mpv. +mkdir -p squashfs-root/usr/lib/l-mpv +cp -a "${RUNTIME_DIR}/." squashfs-root/usr/lib/l-mpv/ +find squashfs-root/usr/lib/l-mpv -type f -name 'lib*.so*' -exec patchelf --set-rpath '$ORIGIN' {} \; 2>/dev/null || true +if [[ -f squashfs-root/usr/lib/l-mpv/vapoursynth/liblmpv_ncnn.so ]]; then + patchelf --set-rpath '$ORIGIN/../lib' squashfs-root/usr/lib/l-mpv/vapoursynth/liblmpv_ncnn.so +fi + +"${LINUXDEPLOY}" --appimage-extract-and-run \ + --appdir squashfs-root \ + --executable squashfs-root/usr/bin/ffmpeg \ + --executable squashfs-root/usr/bin/mediainfo \ + --library "${LIBMPV}" \ + --output appimage + +chmod +x "${OUTPUT}" +echo "Created ${OUTPUT}" diff --git a/scripts/build-linux-runtime.sh b/scripts/build-linux-runtime.sh new file mode 100755 index 0000000..13ae8de --- /dev/null +++ b/scripts/build-linux-runtime.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BUILD_DIR="${L_MPV_RUNTIME_BUILD_DIR:-${RUNNER_TEMP:-/tmp}/l-mpv-runtime-build}" +PREFIX="${L_MPV_RUNTIME_PREFIX:-${ROOT_DIR}/.runtime/linux}" +JOBS="${JOBS:-$(nproc)}" + +MPV_REF="2c219aa822df18a1b7fd9abe3e151cd93ad67307" # v0.41.0 +NCNN_REF="c4193aadbbb56582aa87b1850dd3d98fb8fd936d" # 20250916 +VS_REF="acabf605b2205b32d65859bb2736405719d2fafd" # R79 +SHADER_REF="1e4ded59a4c5080c832badedb5a4a80d9064be6e" + +mkdir -p "${BUILD_DIR}" "${PREFIX}" + +clone_at() { + local url="$1" ref="$2" destination="$3" + if [[ ! -d "${destination}/.git" ]]; then + git clone --filter=blob:none "${url}" "${destination}" + fi + git -C "${destination}" fetch --depth 1 origin "${ref}" + git -C "${destination}" checkout --detach FETCH_HEAD +} + +clone_at https://github.com/Tencent/ncnn.git "${NCNN_REF}" "${BUILD_DIR}/ncnn" +git -C "${BUILD_DIR}/ncnn" submodule update --init --depth 1 +cmake -S "${BUILD_DIR}/ncnn" -B "${BUILD_DIR}/ncnn-build" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${PREFIX}" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DNCNN_VULKAN=ON -DNCNN_SHARED_LIB=ON -DNCNN_BUILD_TOOLS=OFF \ + -DNCNN_BUILD_EXAMPLES=OFF -DNCNN_BUILD_BENCHMARK=OFF -DNCNN_BUILD_TESTS=OFF +cmake --build "${BUILD_DIR}/ncnn-build" --parallel "${JOBS}" +cmake --install "${BUILD_DIR}/ncnn-build" + +clone_at https://github.com/vapoursynth/vapoursynth.git "${VS_REF}" "${BUILD_DIR}/vapoursynth" +rm -rf "${BUILD_DIR}/vapoursynth-build" +meson setup "${BUILD_DIR}/vapoursynth-build" "${BUILD_DIR}/vapoursynth" \ + --prefix="${PREFIX}" --libdir=lib --buildtype=release +meson compile -C "${BUILD_DIR}/vapoursynth-build" -j "${JOBS}" +meson install -C "${BUILD_DIR}/vapoursynth-build" + +VS_SITE_DIR="$(find "${PREFIX}/lib" -type d -path '*/site-packages/vapoursynth' -print -quit)" +if [[ -z "${VS_SITE_DIR}" ]]; then + echo 'VapourSynth Python package was not installed' >&2 + exit 1 +fi +mkdir -p "${PREFIX}/include/vapoursynth" +cp -a "${VS_SITE_DIR}/include/." "${PREFIX}/include/vapoursynth/" +find "${VS_SITE_DIR}" -maxdepth 1 -type f \( -name 'libvapoursynth*.so*' -o -name 'libvsscript.so*' \) \ + -exec cp -aL {} "${PREFIX}/lib/" \; +cat > "${PREFIX}/lib/pkgconfig/vapoursynth-script.pc" < Self { + Self + } + + pub fn set_active(&self, _active: bool) {} + + pub fn get_spectrum(&self) -> [f32; 32] { + [0.0; 32] + } +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1b884c3..fb8c459 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -11,6 +11,30 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::collections::{HashMap, HashSet}; use tauri::State; +/// Корневая директория пользовательских данных. +/// Windows сохраняет portable-поведение, Linux следует XDG Base Directory. +pub fn app_data_root() -> std::path::PathBuf { + #[cfg(target_os = "linux")] + { + let base = std::env::var_os("XDG_CONFIG_HOME") + .map(std::path::PathBuf::from) + .or_else(|| std::env::var_os("HOME").map(|home| std::path::PathBuf::from(home).join(".config"))) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + return base.join("l-mpv"); + } + + #[cfg(not(target_os = "linux"))] + std::env::current_exe() + .ok() + .and_then(|path| path.parent().map(std::path::Path::to_path_buf)) + .unwrap_or_else(|| std::path::PathBuf::from(".")) +} + +#[tauri::command] +pub fn get_runtime_platform() -> &'static str { + std::env::consts::OS +} + fn default_true() -> bool { true } @@ -510,16 +534,9 @@ pub fn load_external_tracks_internal( state: &PlayerState, video_path: &std::path::Path, ) -> Result<(), String> { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - let (auto_load, auto_select_audio) = if let Some(ref p_dir) = exe_dir { - let settings = AppSettings::load(p_dir); - (settings.auto_load_tracks, settings.auto_select_external_audio) - } else { - (false, false) - }; + let settings = AppSettings::load(&app_data_root()); + let (auto_load, auto_select_audio) = + (settings.auto_load_tracks, settings.auto_select_external_audio); if !auto_load { return Ok(()); @@ -1155,19 +1172,13 @@ pub fn set_screenshot_dir( state: State<'_, PlayerState>, path: String, ) -> Result<(), String> { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); + let exe_dir = app_data_root(); let safe_path = path.replace("\\", "/"); let is_reset = safe_path == "screenshots" || safe_path.is_empty(); let target_path = if is_reset { - if let Some(ref p_dir) = exe_dir { - p_dir.join("screenshots").to_string_lossy().replace("\\", "/") - } else { - "screenshots".to_string() - } + exe_dir.join("screenshots").to_string_lossy().replace("\\", "/") } else { safe_path.clone() }; @@ -1176,15 +1187,13 @@ pub fn set_screenshot_dir( .mpv .set_property_string("screenshot-directory", &target_path)?; - if let Some(p_dir) = exe_dir { - let mut settings = AppSettings::load(&p_dir); - if is_reset { - settings.screenshot_directory = None; - } else { - settings.screenshot_directory = Some(target_path); - } - settings.save(&p_dir).ok(); + let mut settings = AppSettings::load(&exe_dir); + if is_reset { + settings.screenshot_directory = None; + } else { + settings.screenshot_directory = Some(target_path); } + settings.save(&exe_dir).ok(); Ok(()) } @@ -1192,105 +1201,52 @@ pub fn set_screenshot_dir( /// Получить настройку multi-instance. #[tauri::command] pub fn get_multi_instance() -> Result { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - if let Some(p_dir) = exe_dir { - let settings = AppSettings::load(&p_dir); - return Ok(settings.allow_multi_instance); - } - Ok(false) + Ok(AppSettings::load(&app_data_root()).allow_multi_instance) } /// Установить настройку multi-instance. #[tauri::command] pub fn set_multi_instance(allow: bool) -> Result<(), String> { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - if let Some(p_dir) = exe_dir { - let mut settings = AppSettings::load(&p_dir); - settings.allow_multi_instance = allow; - settings.save(&p_dir).ok(); - } - - Ok(()) + let root = app_data_root(); + let mut settings = AppSettings::load(&root); + settings.allow_multi_instance = allow; + settings.save(&root) } /// Получить текущий статус настройки автоматического подхвата внешних дорожек. #[tauri::command] pub fn get_auto_load_tracks() -> Result { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - if let Some(p_dir) = exe_dir { - let settings = AppSettings::load(&p_dir); - return Ok(settings.auto_load_tracks); - } - Ok(false) + Ok(AppSettings::load(&app_data_root()).auto_load_tracks) } /// Установить статус настройки автоматического подхвата внешних дорожек с сохранением в settings.json. #[tauri::command] pub fn set_auto_load_tracks(enabled: bool) -> Result<(), String> { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - if let Some(p_dir) = exe_dir { - let mut settings = AppSettings::load(&p_dir); - settings.auto_load_tracks = enabled; - settings.save(&p_dir).ok(); - return Ok(()); - } - Err("Не удалось определить директорию приложения для сохранения настроек".to_string()) + let root = app_data_root(); + let mut settings = AppSettings::load(&root); + settings.auto_load_tracks = enabled; + settings.save(&root) } /// Получить текущий статус настройки автоматического переключения звука на внешнюю аудиодорожку. #[tauri::command] pub fn get_auto_select_external_audio() -> Result { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - if let Some(p_dir) = exe_dir { - let settings = AppSettings::load(&p_dir); - return Ok(settings.auto_select_external_audio); - } - Ok(false) + Ok(AppSettings::load(&app_data_root()).auto_select_external_audio) } /// Установить статус настройки автоматического переключения звука на внешнюю аудиодорожку. #[tauri::command] pub fn set_auto_select_external_audio(enabled: bool) -> Result<(), String> { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - if let Some(p_dir) = exe_dir { - let mut settings = AppSettings::load(&p_dir); - settings.auto_select_external_audio = enabled; - settings.save(&p_dir).ok(); - return Ok(()); - } - Err("Не удалось определить директорию приложения для сохранения настроек".to_string()) + let root = app_data_root(); + let mut settings = AppSettings::load(&root); + settings.auto_select_external_audio = enabled; + settings.save(&root) } /// Получить текущий статус настройки автоматического переключения на следующее видео по окончании. #[tauri::command] pub fn get_play_next_on_end() -> Result { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - if let Some(p_dir) = exe_dir { - let settings = AppSettings::load(&p_dir); - return Ok(settings.play_next_on_end); - } - Ok(true) + Ok(AppSettings::load(&app_data_root()).play_next_on_end) } /// Установить статус настройки автоматического переключения на следующее видео по окончании. @@ -1299,20 +1255,12 @@ pub fn set_play_next_on_end( state: State<'_, PlayerState>, enabled: bool, ) -> Result<(), String> { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - if let Some(p_dir) = exe_dir { - let mut settings = AppSettings::load(&p_dir); - settings.play_next_on_end = enabled; - settings.save(&p_dir).ok(); - - let keep_open_val = if enabled { "yes" } else { "always" }; - let _ = state.mpv.set_property_string("keep-open", keep_open_val); - return Ok(()); - } - Err("Не удалось определить директорию приложения для сохранения настроек".to_string()) + let root = app_data_root(); + let mut settings = AppSettings::load(&root); + settings.play_next_on_end = enabled; + settings.save(&root)?; + let keep_open_val = if enabled { "yes" } else { "always" }; + state.mpv.set_property_string("keep-open", keep_open_val) } /// Сканирование и загрузка внешних дорожек и субтитров для указанного медиафайла. @@ -1358,10 +1306,7 @@ pub fn set_ambient_settings( ) -> Result<(), String> { state.ambient_controller.apply(&settings)?; - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())) - .ok_or_else(|| "Не удалось определить путь к директории приложения".to_string())?; + let exe_dir = app_data_root(); let mut current_settings = AppSettings::load(&exe_dir); current_settings.ambient = settings; @@ -1381,10 +1326,7 @@ pub fn toggle_ambient_mode( current.mode = AmbientController::cycle_mode(¤t.mode); state.ambient_controller.apply(¤t)?; - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())) - .ok_or_else(|| "Не удалось определить путь к директории приложения".to_string())?; + let exe_dir = app_data_root(); let mut current_settings = AppSettings::load(&exe_dir); current_settings.ambient = current.clone(); @@ -1724,7 +1666,7 @@ pub fn toggle_shuffle(state: State<'_, PlayerState>) -> Result<(), String> { #[tauri::command] pub async fn copy_frame_to_clipboard(state: tauri::State<'_, PlayerState>) -> Result<(), String> { use std::time::Duration; - let exe_dir = std::env::current_exe().map_err(|e| e.to_string())?.parent().unwrap().to_path_buf(); + let exe_dir = app_data_root(); let data_dir = exe_dir.join("data"); let _ = std::fs::create_dir_all(&data_dir); @@ -1800,15 +1742,11 @@ pub fn normalize_history_path(path: &str) -> String { fn get_history_map() -> &'static Mutex> { WATCH_HISTORY.get_or_init(|| { let mut map = HashMap::new(); - if let Ok(exe_dir) = std::env::current_exe() { - if let Some(parent) = exe_dir.parent() { - let history_path = parent.join("config").join("history.json"); - if let Ok(content) = std::fs::read_to_string(&history_path) { - if let Ok(parsed) = serde_json::from_str::>(&content) { - for (k, v) in parsed { - map.insert(normalize_history_path(&k), v); - } - } + let history_path = app_data_root().join("config").join("history.json"); + if let Ok(content) = std::fs::read_to_string(&history_path) { + if let Ok(parsed) = serde_json::from_str::>(&content) { + for (k, v) in parsed { + map.insert(normalize_history_path(&k), v); } } } @@ -1818,15 +1756,11 @@ fn get_history_map() -> &'static Mutex> { pub fn save_history_to_disk() { if let Ok(map) = get_history_map().lock() { - if let Ok(exe_dir) = std::env::current_exe() { - if let Some(parent) = exe_dir.parent() { - let config_dir = parent.join("config"); - let _ = std::fs::create_dir_all(&config_dir); - let history_path = config_dir.join("history.json"); - if let Ok(json) = serde_json::to_string_pretty(&*map) { - let _ = std::fs::write(&history_path, json); - } - } + let config_dir = app_data_root().join("config"); + let _ = std::fs::create_dir_all(&config_dir); + let history_path = config_dir.join("history.json"); + if let Ok(json) = serde_json::to_string_pretty(&*map) { + let _ = std::fs::write(&history_path, json); } } } @@ -2189,13 +2123,22 @@ pub async fn extract_track( }; // Определение пути к встроенному исполняемому файлу ffmpeg + #[cfg(target_os = "windows")] let exe_dir = std::env::current_exe() .map_err(|e| e.to_string())? .parent() .ok_or_else(|| "Не удалось определить каталог приложения L-MPV".to_string())? .to_path_buf(); + #[cfg(target_os = "windows")] let mut ffmpeg_path = exe_dir.join("ffmpeg.exe"); + #[cfg(target_os = "linux")] + let ffmpeg_path = std::env::var_os("APPDIR") + .map(std::path::PathBuf::from) + .map(|dir| dir.join("usr/bin/ffmpeg")) + .unwrap_or_else(|| std::path::PathBuf::from("ffmpeg")); + + #[cfg(target_os = "windows")] if !ffmpeg_path.exists() { if std::path::Path::new("ffmpeg.exe").exists() { ffmpeg_path = std::path::PathBuf::from("ffmpeg.exe"); @@ -2361,11 +2304,7 @@ fn sanitize_filename(name: &str) -> String { /// Получение абсолютного пути к портативной директории config/presets/. fn get_presets_dir() -> Result { - let exe_dir = std::env::current_exe() - .map_err(|e| format!("Не удалось определить путь к исполняемому файлу: {e}"))? - .parent() - .ok_or_else(|| "Не удалось определить директорию исполняемого файла".to_string())? - .to_path_buf(); + let exe_dir = app_data_root(); let dir = exe_dir.join("config").join("presets"); if !dir.exists() { let _ = std::fs::create_dir_all(&dir); @@ -2540,5 +2479,3 @@ pub fn write_text_file(path: String, content: String) -> Result<(), String> { pub fn read_text_file(path: String) -> Result { std::fs::read_to_string(&path).map_err(|e| format!("Ошибка чтения файла по пути {path}: {e}")) } - - diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index baca0b5..8a4e343 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,6 +5,10 @@ mod ambient; pub mod upscale; +#[cfg(target_os = "windows")] +mod audio_capture; +#[cfg(not(target_os = "windows"))] +#[path = "audio_capture_stub.rs"] mod audio_capture; mod commands; mod mediainfo; @@ -35,7 +39,7 @@ pub fn parse_cli_args>(args: I) -> ParsedCliArgs let lower = arg.to_lowercase(); if lower == "--mediainfo" || lower == "-mediainfo" || lower == "/mediainfo" { open_mediainfo = true; - } else if !arg.starts_with('-') && !arg.starts_with('/') && file_path.is_none() { + } else if !arg.starts_with('-') && file_path.is_none() { file_path = Some(arg); } } @@ -46,6 +50,28 @@ pub fn parse_cli_args>(args: I) -> ParsedCliArgs } } +#[cfg(test)] +mod cli_tests { + use super::parse_cli_args; + + #[test] + fn accepts_linux_absolute_media_path() { + let parsed = parse_cli_args(["l-mpv".to_string(), "/tmp/video.mp4".to_string()]); + assert_eq!(parsed.file_path.as_deref(), Some("/tmp/video.mp4")); + } + + #[test] + fn keeps_windows_mediainfo_switch() { + let parsed = parse_cli_args([ + "l-mpv".to_string(), + "/mediainfo".to_string(), + "C:\\video.mp4".to_string(), + ]); + assert!(parsed.open_mediainfo); + assert_eq!(parsed.file_path.as_deref(), Some("C:\\video.mp4")); + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let raw_args: Vec = std::env::args().collect(); @@ -74,11 +100,7 @@ pub fn run() { println!("[L-MPV] Запуск функции run()..."); // Определяем портативную директорию приложения - let exe_dir = std::env::current_exe() - .expect("Не удалось определить путь к исполняемому файлу") - .parent() - .expect("Не удалось определить директорию исполняемого файла") - .to_path_buf(); + let exe_dir = commands::app_data_root(); println!("[L-MPV] Директория exe: {:?}", exe_dir); @@ -111,10 +133,14 @@ pub fn run() { .map(|d| d.as_secs()) .unwrap_or(0); - let payload = panic_info.payload().downcast_ref::<&str>() - .unwrap_or(&"Box"); + let payload = panic_info + .payload() + .downcast_ref::<&str>() + .map(|value| (*value).to_string()) + .or_else(|| panic_info.payload().downcast_ref::().cloned()) + .unwrap_or_else(|| "Box".to_string()); let location = panic_info.location().map(|l| format!("{}:{}", l.file(), l.line())).unwrap_or_default(); - + eprintln!("[L-MPV] CRASH at {location}: {payload}"); let _ = writeln!(file, "[{}] CRASH (Panic) at {}: {}", timestamp, location, payload); })); @@ -264,6 +290,7 @@ pub fn run() { commands::set_play_next_on_end, commands::load_external_tracks_for_file, commands::get_app_version, + commands::get_runtime_platform, // Автообновление updater::check_launch_and_update, updater::check_for_updates, @@ -293,6 +320,7 @@ pub fn run() { upscale::switch_upscale_network_hotkey, upscale::precompile_model_engine_1080p, upscale::save_models_order, + upscale::download_curated_ncnn_model, ]) .on_window_event(|window, event| match event { tauri::WindowEvent::CloseRequested { api, .. } => { @@ -323,6 +351,7 @@ pub fn run() { }) .setup(|app| { let window = app.get_webview_window("main").unwrap(); + let cli = parse_cli_args(std::env::args()); #[cfg(target_os = "windows")] { @@ -361,7 +390,159 @@ pub fn run() { } } - let cli = parse_cli_args(std::env::args()); + #[cfg(target_os = "linux")] + { + use gtk::glib::{self, Cast, ControlFlow, ObjectExt, Propagation}; + use gtk::prelude::{ContainerExt, GLAreaExt, OverlayExt, WidgetExt}; + + let state = app.state::(); + let mpv = state.mpv.clone(); + let root = window.default_vbox() + .map_err(|e| format!("Не удалось получить GTK-контейнер: {e}"))?; + let Some(overlay) = root.children().into_iter().last() + .and_then(|widget| widget.downcast::().ok()) else { + println!("[L-MPV] Нативная видеоповерхность отключена"); + window.show().ok(); + return Ok(()); + }; + let children = overlay.children(); + let video = children.iter() + .find_map(|widget| widget.clone().downcast::().ok()) + .ok_or_else(|| "GtkGLArea отсутствует в GtkOverlay".to_string())?; + let webview_host = children.into_iter() + .find(|widget| !widget.is::()) + .ok_or_else(|| "WebView отсутствует в GtkOverlay".to_string())?; + let webview = webview_host.clone().downcast::().ok() + .and_then(|container| container.children().into_iter().next()) + .unwrap_or_else(|| webview_host.clone()); + + video.set_auto_render(false); + video.set_has_alpha(false); + video.set_required_version(3, 2); + video.set_hexpand(true); + video.set_vexpand(true); + let display_name = gtk::gdk::Display::default() + .map(|display| display.type_().name().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + if display_name == "GdkX11Display" { + // X11 native child windows cannot alpha-compose WebKitGTK + // above GLArea. Keep the GL surface between the titlebar + // and controls so both remain visible and interactive. + video.set_vexpand(false); + video.set_valign(gtk::Align::Start); + video.set_margin_top(44); + video.set_margin_bottom(0); + let weak_x11_video = video.downgrade(); + overlay.connect_size_allocate(move |_overlay, allocation| { + if let Some(video) = weak_x11_video.upgrade() { + video.set_size_request(-1, (allocation.height() - 140).max(1)); + } + }); + overlay.set_overlay_pass_through(&video, true); + } else { + overlay.set_overlay_pass_through(&webview_host, false); + } + // The Linux config creates this window decorated so Tauri does + // not install its hierarchy-dependent resize handler. Once the + // WebView has been placed over GtkGLArea we restore the custom + // title bar used by the application. + window.set_decorations(false) + .map_err(|e| format!("Не удалось отключить системную рамку: {e}"))?; + + let mpv_realize = mpv.clone(); + let startup_path = if cli.open_mediainfo { None } else { cli.file_path.clone() }; + let startup_app = app.handle().clone(); + video.connect_realize(move |area| { + area.make_current(); + if let Some(error) = area.error() { + eprintln!("[L-MPV] Не удалось создать OpenGL-контекст: {error}"); + return; + } + if let Err(error) = mpv_realize.initialize_opengl_renderer() { + eprintln!("[L-MPV] Ошибка libmpv render API: {error}"); + return; + } + if let Some(path) = startup_path.as_deref() { + let state = startup_app.state::(); + if let Err(error) = commands::open_file_internal(&state, path) { + eprintln!("[L-MPV] Ошибка открытия файла при запуске: {error}"); + } else { + println!("[L-MPV] Открыт файл из CLI: {path}"); + } + if let Some(window) = startup_app.get_webview_window("main") { + window.show().ok(); + } + } + }); + + let mpv_render = mpv.clone(); + video.connect_render(move |area, _context| { + const GL_DRAW_FRAMEBUFFER_BINDING: u32 = 0x8CA6; + #[link(name = "GL")] + unsafe extern "C" { + fn glGetIntegerv(name: u32, value: *mut i32); + } + + let mut fbo = 0i32; + unsafe { glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &mut fbo) }; + let scale = area.scale_factor().max(1); + let allocation = area.allocation(); + let width = allocation.width().max(1) * scale; + let height = allocation.height().max(1) * scale; + if let Err(error) = mpv_render.render_opengl_frame(fbo, width, height) { + eprintln!("[L-MPV] Ошибка отрисовки кадра: {error}"); + } + Propagation::Stop + }); + + let mpv_unrealize = mpv.clone(); + video.connect_unrealize(move |area| { + area.make_current(); + mpv_unrealize.free_opengl_renderer(); + }); + + let weak_video = video.downgrade(); + let mpv_updates = mpv.clone(); + glib::timeout_add_local(std::time::Duration::from_millis(8), move || { + if let Some(video) = weak_video.upgrade() { + if mpv_updates.take_render_request() { + video.queue_render(); + } + ControlFlow::Continue + } else { + ControlFlow::Break + } + }); + + overlay.show_all(); + // GtkGLArea and WebKitGTK both own native GDK child windows. + // Keep the WebView's window above the OpenGL surface; GTK's + // widget order alone is not sufficient on every compositor. + if display_name == "GdkX11Display" { + if let Some(video_window) = video.window() { + video_window.raise(); + } + } else { + if let Some(video_window) = video.window() { + video_window.lower(); + } + if let Some(webview_window) = webview.window() { + webview_window.raise(); + } + webview.connect_realize(|widget| { + if let Some(window) = widget.window() { + window.raise(); + } + }); + } + println!("[L-MPV] libmpv render API подключён к GTK GLArea ({display_name})"); + + let saved_cfg = commands::AppSettings::load(&commands::app_data_root()); + if let Err(e) = state.ambient_controller.apply(&saved_cfg.ambient) { + println!("[L-MPV] Ошибка инициализации Ambient Light: {e}"); + } + } + if cli.open_mediainfo { let target_path = cli.file_path.as_deref().unwrap_or(""); let app_handle = app.handle().clone(); @@ -370,10 +551,20 @@ pub fn run() { } // Окно плеера main остается скрытым } else if let Some(ref path) = cli.file_path { + #[cfg(target_os = "linux")] + { + let _ = path; + window.show().ok(); + } + #[cfg(not(target_os = "linux"))] + { let state = app.state::(); if let Err(e) = commands::open_file_internal(&state, path) { println!("[L-MPV] Ошибка открытия файла при запуске: {}", e); window.show().ok(); + } else { + println!("[L-MPV] Открыт файл из CLI: {path}"); + } } } else { // Нет аргумента файла — показываем окно сразу со стартовой страницей diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ac272a3..8849a48 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,115 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { + #[cfg(target_os = "linux")] + configure_linux_runtime(); l_mpv_lib::run() } + +#[cfg(target_os = "linux")] +fn configure_linux_runtime() { + use std::path::PathBuf; + + // WebKitGTK's accelerated child surface cannot be alpha-composited over a + // GtkGLArea reliably on native Wayland (and some X11 compositors). Keep + // WebKit in GTK's compositing path; video itself remains GPU-rendered by + // libmpv in GtkGLArea. + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + let runtime = std::env::var_os("L_MPV_RUNTIME_DIR") + .map(PathBuf::from) + .or_else(|| std::env::var_os("APPDIR").map(|dir| PathBuf::from(dir).join("usr/lib/l-mpv"))) + .or_else(|| { + std::env::current_exe().ok().and_then(|exe| { + let candidate = exe.parent()?.join("../lib/l-mpv"); + candidate.exists().then_some(candidate) + }) + }); + let Some(runtime) = runtime else { return }; + std::env::set_var("L_MPV_RUNTIME_DIR", &runtime); + let libmpv = runtime.join("lib/libmpv.so.2"); + if libmpv.is_file() { + std::env::set_var("L_MPV_LIBMPV", libmpv); + } + + let bundled_python = runtime.join("python/bin/python3"); + let bundled_python_home = runtime.join("python"); + if bundled_python.is_file() { + std::env::set_var("PYTHONHOME", &bundled_python_home); + prepend_env_path("PATH", runtime.join("bin")); + } + + let python_root = runtime.join("lib"); + if let Ok(entries) = std::fs::read_dir(&python_root) { + if let Some(site_packages) = entries.flatten() + .map(|entry| entry.path().join("site-packages")) + .find(|path| path.is_dir()) + { + let mut paths = vec![site_packages]; + if let Some(existing) = std::env::var_os("PYTHONPATH") { + paths.extend(std::env::split_paths(&existing)); + } + if let Ok(value) = std::env::join_paths(paths) { + std::env::set_var("PYTHONPATH", value); + } + } + } + + configure_vapoursynth(&runtime, &bundled_python); +} + +#[cfg(target_os = "linux")] +fn prepend_env_path(name: &str, path: std::path::PathBuf) { + let mut paths = vec![path]; + if let Some(existing) = std::env::var_os(name) { + paths.extend(std::env::split_paths(&existing)); + } + if let Ok(value) = std::env::join_paths(paths) { + std::env::set_var(name, value); + } +} + +#[cfg(target_os = "linux")] +fn configure_vapoursynth(runtime: &std::path::Path, python: &std::path::Path) { + use std::os::unix::fs::MetadataExt; + + let vsscript = runtime.join("lib/libvsscript.so"); + let libpython = std::fs::read_dir(runtime.join("lib")).ok().and_then(|entries| { + entries.flatten().map(|entry| entry.path()).find(|path| { + path.file_name().and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("libpython") && name.contains(".so")) + }) + }); + let (Ok(vsscript), Ok(python), Some(libpython)) = ( + vsscript.canonicalize(), + python.canonicalize(), + libpython.and_then(|path| path.canonicalize().ok()), + ) else { return }; + let mtime = std::fs::metadata(&python).map(|metadata| metadata.mtime()).unwrap_or(0); + let config_root = std::env::var_os("XDG_CONFIG_HOME") + .map(std::path::PathBuf::from) + .or_else(|| std::env::var_os("HOME").map(|home| std::path::PathBuf::from(home).join(".config"))) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + let config_dir = config_root.join("vapoursynth"); + let config_path = config_dir.join("vapoursynth.toml"); + let key = toml_string(&vsscript.to_string_lossy()); + let line = format!( + "{key} = [{}, {}, \"{mtime}\"]", + toml_string(&python.to_string_lossy()), + toml_string(&libpython.to_string_lossy()), + ); + let existing = std::fs::read_to_string(&config_path).unwrap_or_default(); + let mut lines = existing.lines() + .filter(|existing_line| !existing_line.starts_with(&key)) + .map(str::to_owned) + .collect::>(); + lines.push(line); + if std::fs::create_dir_all(config_dir).is_ok() { + let _ = std::fs::write(config_path, format!("{}\n", lines.join("\n"))); + } +} + +#[cfg(target_os = "linux")] +fn toml_string(value: &str) -> String { + format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\"")) +} diff --git a/src-tauri/src/mediainfo.rs b/src-tauri/src/mediainfo.rs index c579b04..2398d31 100644 --- a/src-tauri/src/mediainfo.rs +++ b/src-tauri/src/mediainfo.rs @@ -4,9 +4,12 @@ //! извлечение исчерпывающего текстового и JSON-отчёта обо всех потоках и метаданных //! медиаконтейнера (видео, аудиодорожки, субтитры, кодеки, параметры кодирования). +#[cfg(target_os = "windows")] use std::os::raw::c_void; use std::path::{Path, PathBuf}; +#[cfg(target_os = "windows")] use std::sync::OnceLock; +#[cfg(target_os = "windows")] use libloading::{Library, Symbol}; use serde::{Deserialize, Serialize}; use tauri::State; @@ -21,9 +24,11 @@ pub struct DetailedMediaInfo { pub json: String, } +#[cfg(target_os = "windows")] static MEDIAINFO_API: OnceLock> = OnceLock::new(); /// Набор динамически загружаемых символов C-API MediaInfo. +#[cfg(target_os = "windows")] struct MediaInfoApi { _lib: &'static Library, new_fn: Symbol<'static, unsafe extern "C" fn() -> *mut c_void>, @@ -35,9 +40,12 @@ struct MediaInfoApi { option_fn: Symbol<'static, unsafe extern "C" fn(*mut c_void, *const u16, *const u16) -> *const u16>, } +#[cfg(target_os = "windows")] unsafe impl Send for MediaInfoApi {} +#[cfg(target_os = "windows")] unsafe impl Sync for MediaInfoApi {} +#[cfg(target_os = "windows")] impl MediaInfoApi { /// Получение синглтона API библиотеки (ленивая инициализация один раз). pub fn get() -> Result<&'static Self, String> { @@ -96,6 +104,7 @@ impl MediaInfoApi { } /// Поиск пути к файлу библиотеки mediainfo.dll по списку стандартных кандидатов. +#[cfg(target_os = "windows")] fn find_mediainfo_dll() -> Result { let mut candidates = Vec::new(); @@ -125,11 +134,13 @@ fn find_mediainfo_dll() -> Result { } /// Конвертация строкового среза в нуль-терминированный UTF-16 буфер. +#[cfg(target_os = "windows")] fn to_wide(s: &str) -> Vec { s.encode_utf16().chain(std::iter::once(0)).collect() } /// Чтение нуль-терминированной UTF-16 строки из нативного указателя. +#[cfg(target_os = "windows")] unsafe fn from_wide_ptr(ptr: *const u16) -> String { if ptr.is_null() { return String::new(); @@ -161,6 +172,13 @@ pub fn analyze_media_file>(path: P) -> Result>(path: P) -> Result Result { + let binary = std::env::var_os("APPDIR") + .map(PathBuf::from) + .map(|dir| dir.join("usr/bin/mediainfo")) + .filter(|candidate| candidate.exists()) + .unwrap_or_else(|| PathBuf::from("mediainfo")); + + let text_output = std::process::Command::new(&binary) + .arg(path) + .output() + .map_err(|e| format!("Не удалось запустить MediaInfo: {e}"))?; + if !text_output.status.success() { + return Err(String::from_utf8_lossy(&text_output.stderr).trim().to_string()); + } + + let json_output = std::process::Command::new(&binary) + .arg("--Output=JSON") + .arg(path) + .output() + .map_err(|e| format!("Не удалось получить JSON MediaInfo: {e}"))?; + + Ok(DetailedMediaInfo { + text: String::from_utf8_lossy(&text_output.stdout).into_owned(), + json: if json_output.status.success() { + String::from_utf8_lossy(&json_output.stdout).into_owned() + } else { + String::new() + }, + }) } /// Получение полного детального отчёта MediaInfo через библиотеку MediaInfo.dll. diff --git a/src-tauri/src/mpv_manager.rs b/src-tauri/src/mpv_manager.rs index 937cea7..0f611dc 100644 --- a/src-tauri/src/mpv_manager.rs +++ b/src-tauri/src/mpv_manager.rs @@ -8,6 +8,8 @@ use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_double, c_int, c_void}; use std::path::Path; use std::sync::Mutex; +#[cfg(target_os = "linux")] +use std::sync::atomic::{AtomicBool, Ordering}; use libloading::{Library, Symbol}; // ─── Определения FFI для libmpv C API ─────────────────── @@ -32,6 +34,37 @@ struct MpvHandle { _private: [u8; 0], } +#[repr(C)] +struct MpvRenderContext { + _private: [u8; 0], +} + +#[repr(C)] +struct MpvRenderParam { + param_type: c_int, + data: *mut c_void, +} + +#[repr(C)] +struct MpvOpenGlInitParams { + get_proc_address: Option *mut c_void>, + get_proc_address_ctx: *mut c_void, +} + +#[repr(C)] +struct MpvOpenGlFbo { + fbo: c_int, + width: c_int, + height: c_int, + internal_format: c_int, +} + +const MPV_RENDER_PARAM_INVALID: c_int = 0; +const MPV_RENDER_PARAM_API_TYPE: c_int = 1; +const MPV_RENDER_PARAM_OPENGL_INIT_PARAMS: c_int = 2; +const MPV_RENDER_PARAM_OPENGL_FBO: c_int = 3; +const MPV_RENDER_PARAM_FLIP_Y: c_int = 4; + // ─── Динамически загружаемые функции ───────────────────── struct MpvApi { _lib: Library, @@ -45,6 +78,10 @@ struct MpvApi { free: Symbol<'static, unsafe extern "C" fn(data: *mut c_void)>, set_property: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvHandle, name: *const c_char, format: MpvFormat, data: *mut c_void) -> c_int>, set_property_string: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvHandle, name: *const c_char, data: *const c_char) -> c_int>, + render_context_create: Symbol<'static, unsafe extern "C" fn(ctx: *mut *mut MpvRenderContext, handle: *mut MpvHandle, params: *mut MpvRenderParam) -> c_int>, + render_context_render: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvRenderContext, params: *mut MpvRenderParam) -> c_int>, + render_context_set_update_callback: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvRenderContext, callback: Option, callback_ctx: *mut c_void)>, + render_context_free: Symbol<'static, unsafe extern "C" fn(ctx: *mut MpvRenderContext)>, } unsafe impl Send for MpvApi {} @@ -65,6 +102,10 @@ impl MpvApi { let free = std::mem::transmute(lib.get::(b"mpv_free\0").map_err(|e| e.to_string())?); let set_property = std::mem::transmute(lib.get:: c_int>(b"mpv_set_property\0").map_err(|e| e.to_string())?); let set_property_string = std::mem::transmute(lib.get:: c_int>(b"mpv_set_property_string\0").map_err(|e| e.to_string())?); + let render_context_create = std::mem::transmute(lib.get:: c_int>(b"mpv_render_context_create\0").map_err(|e| e.to_string())?); + let render_context_render = std::mem::transmute(lib.get:: c_int>(b"mpv_render_context_render\0").map_err(|e| e.to_string())?); + let render_context_set_update_callback = std::mem::transmute(lib.get::, callback_ctx: *mut c_void)>(b"mpv_render_context_set_update_callback\0").map_err(|e| e.to_string())?); + let render_context_free = std::mem::transmute(lib.get::(b"mpv_render_context_free\0").map_err(|e| e.to_string())?); Ok(Self { _lib: lib, @@ -78,6 +119,10 @@ impl MpvApi { free, set_property, set_property_string, + render_context_create, + render_context_render, + render_context_set_update_callback, + render_context_free, }) } } @@ -85,6 +130,9 @@ impl MpvApi { /// Безопасная обёртка над контекстом mpv. pub struct MpvManager { handle: Mutex<*mut MpvHandle>, + render_context: Mutex<*mut MpvRenderContext>, + #[cfg(target_os = "linux")] + render_pending: AtomicBool, api: MpvApi, } @@ -94,23 +142,35 @@ unsafe impl Sync for MpvManager {} impl MpvManager { /// Загрузка API из первой доступной библиотеки mpv. unsafe fn load_mpv_api() -> Result { - let dll_names = [ + #[cfg(target_os = "windows")] + let library_names = [ "libmpv-2.dll", "mpv-2.dll", "libmpv-1.dll", "mpv-1.dll", ]; - for dll_name in &dll_names { - if let Ok(loaded) = MpvApi::load(dll_name) { + #[cfg(target_os = "linux")] + let library_names = { + let mut names = Vec::new(); + if let Ok(path) = std::env::var("L_MPV_LIBMPV") { + names.push(path); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + names.push(dir.join("../lib/l-mpv/lib/libmpv.so.2").to_string_lossy().into_owned()); + names.push(dir.join("libmpv.so.2").to_string_lossy().into_owned()); + } + } + names.extend(["/usr/lib/l-mpv/lib/libmpv.so.2", "libmpv.so.2", "libmpv.so.1", "libmpv.so"].map(String::from)); + names + }; + + for library_name in &library_names { + if let Ok(loaded) = MpvApi::load(library_name) { return Ok(loaded); } } - Err( - "Не найдена библиотека libmpv-2.dll / \ - mpv-2.dll. Пожалуйста, скачайте её и \ - поместите рядом с исполняемым файлом." - .to_string(), - ) + Err(format!("Не найдена библиотека libmpv (проверены: {}).", library_names.join(", "))) } /// Создание нового экземпляра менеджера mpv. @@ -155,9 +215,17 @@ impl MpvManager { Self::set_option(&api, handle, "screenshot-directory", &screenshots_dir); Self::set_option(&api, handle, "screenshot-format", "png"); - // ─── Оптимизированный рендеринг: Direct3D 11 (нативный для Windows / DWM) ─── + // ─── Оптимизированный платформенный рендеринг ─── + #[cfg(target_os = "windows")] Self::set_option(&api, handle, "vo", "gpu-next"); + #[cfg(target_os = "linux")] + Self::set_option(&api, handle, "vo", "libmpv"); + #[cfg(target_os = "windows")] Self::set_option(&api, handle, "gpu-api", "d3d11,auto"); + #[cfg(target_os = "linux")] + // GtkGLArea supplies an EGL/OpenGL context on both native Wayland + // and X11. Vulkan remains available independently to NCNN. + Self::set_option(&api, handle, "gpu-api", "opengl"); Self::set_option(&api, handle, "hwdec", "auto-safe"); // Отключаем лог-файл и снижаем уровень логирования для исключения дискового I/O @@ -179,7 +247,10 @@ impl MpvManager { Self::set_option(&api, handle, "cache-pause", "no"); // Не ставить на паузу при буферизации локальных файлов // ─── Качественный отзывчивый звук (WASAPI) ─── + #[cfg(target_os = "windows")] Self::set_option(&api, handle, "ao", "wasapi"); // Высококачественный драйвер Windows WASAPI + #[cfg(target_os = "linux")] + Self::set_option(&api, handle, "ao", "pipewire,pulse,alsa"); Self::set_option(&api, handle, "audio-buffer", "0.2"); // Отзывчивый размер буфера для плавной перемотки Self::set_option(&api, handle, "audio-channels", "auto-safe"); // Автоопределение каналов оборудования Self::set_option(&api, handle, "audio-pitch-correction", "yes"); // Сохранение тональности при изменении скорости @@ -220,6 +291,9 @@ impl MpvManager { Ok(Self { handle: Mutex::new(handle), + render_context: Mutex::new(std::ptr::null_mut()), + #[cfg(target_os = "linux")] + render_pending: AtomicBool::new(true), api, }) } @@ -295,6 +369,22 @@ impl MpvManager { }) } + pub fn get_property_i64(&self, name: &str) -> Result { + self.with_handle(|handle| { + let c_name = CString::new(name).map_err(|e| e.to_string())?; + let mut value = 0i64; + let result = unsafe { + (self.api.get_property)( + handle, + c_name.as_ptr(), + MpvFormat::Int64, + &mut value as *mut i64 as *mut c_void, + ) + }; + if result < 0 { Err(format!("Ошибка чтения свойства {name}: {result}")) } else { Ok(value) } + }) + } + pub fn set_property_string(&self, name: &str, value: &str) -> Result<(), String> { self.with_handle(|handle| { let c_name = CString::new(name).map_err(|e| format!("Ошибка CString: {}", e))?; @@ -330,6 +420,146 @@ impl MpvManager { }) } + /// Creates the Linux OpenGL render context while GtkGLArea's context is current. + #[cfg(target_os = "linux")] + pub fn initialize_opengl_renderer(&self) -> Result<(), String> { + let mut render_context = self + .render_context + .lock() + .map_err(|_| "Ошибка блокировки render context".to_string())?; + if !render_context.is_null() { + return Ok(()); + } + + unsafe extern "C" fn get_proc_address( + _ctx: *mut c_void, + name: *const c_char, + ) -> *mut c_void { + #[link(name = "dl")] + unsafe extern "C" { + fn dlsym(handle: *mut c_void, name: *const c_char) -> *mut c_void; + } + #[link(name = "EGL")] + unsafe extern "C" { + fn eglGetProcAddress(name: *const c_char) -> *mut c_void; + } + + unsafe { + let symbol = dlsym(std::ptr::null_mut(), name); + if symbol.is_null() { + eglGetProcAddress(name) + } else { + symbol + } + } + } + + let mut init_params = MpvOpenGlInitParams { + get_proc_address: Some(get_proc_address), + get_proc_address_ctx: std::ptr::null_mut(), + }; + let api_type = b"opengl\0".as_ptr() as *mut c_void; + let mut params = [ + MpvRenderParam { + param_type: MPV_RENDER_PARAM_API_TYPE, + data: api_type, + }, + MpvRenderParam { + param_type: MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, + data: &mut init_params as *mut MpvOpenGlInitParams as *mut c_void, + }, + MpvRenderParam { + param_type: MPV_RENDER_PARAM_INVALID, + data: std::ptr::null_mut(), + }, + ]; + + self.with_handle(|handle| { + let result = unsafe { + (self.api.render_context_create)(&mut *render_context, handle, params.as_mut_ptr()) + }; + if result < 0 { + Err(format!("mpv_render_context_create завершился с кодом {result}")) + } else { + unsafe extern "C" fn request_render(ctx: *mut c_void) { + let pending = unsafe { &*(ctx as *const AtomicBool) }; + pending.store(true, Ordering::Release); + } + unsafe { + (self.api.render_context_set_update_callback)( + *render_context, + Some(request_render), + &self.render_pending as *const AtomicBool as *mut c_void, + ); + } + self.render_pending.store(true, Ordering::Release); + Ok(()) + } + }) + } + + #[cfg(target_os = "linux")] + pub fn take_render_request(&self) -> bool { + self.render_pending.swap(false, Ordering::AcqRel) + } + + /// Renders the current video frame into GtkGLArea's active framebuffer. + #[cfg(target_os = "linux")] + pub fn render_opengl_frame(&self, fbo_id: i32, width: i32, height: i32) -> Result<(), String> { + let render_context = self + .render_context + .lock() + .map_err(|_| "Ошибка блокировки render context".to_string())?; + if render_context.is_null() { + return Ok(()); + } + + let mut fbo = MpvOpenGlFbo { + fbo: fbo_id, + width, + height, + internal_format: 0, + }; + let mut flip_y: c_int = 1; + let mut params = [ + MpvRenderParam { + param_type: MPV_RENDER_PARAM_OPENGL_FBO, + data: &mut fbo as *mut MpvOpenGlFbo as *mut c_void, + }, + MpvRenderParam { + param_type: MPV_RENDER_PARAM_FLIP_Y, + data: &mut flip_y as *mut c_int as *mut c_void, + }, + MpvRenderParam { + param_type: MPV_RENDER_PARAM_INVALID, + data: std::ptr::null_mut(), + }, + ]; + let result = unsafe { (self.api.render_context_render)(*render_context, params.as_mut_ptr()) }; + if result < 0 { + Err(format!("mpv_render_context_render завершился с кодом {result}")) + } else { + Ok(()) + } + } + + #[cfg(target_os = "linux")] + pub fn free_opengl_renderer(&self) { + if let Ok(mut render_context) = self.render_context.lock() { + if !render_context.is_null() { + unsafe { + (self.api.render_context_set_update_callback)( + *render_context, + None, + std::ptr::null_mut(), + ) + }; + unsafe { (self.api.render_context_free)(*render_context) }; + *render_context = std::ptr::null_mut(); + } + } + } + pub fn get_property_bool(&self, name: &str) -> Result { self.with_handle(|handle| { let c_name = CString::new(name).map_err(|e| format!("Ошибка CString: {}", e))?; diff --git a/src-tauri/src/upscale/config.rs b/src-tauri/src/upscale/config.rs index c135c31..7234aa4 100644 --- a/src-tauri/src/upscale/config.rs +++ b/src-tauri/src/upscale/config.rs @@ -7,17 +7,27 @@ use std::path::PathBuf; /// Возвращает корневой каталог приложения pub fn get_app_root_dir() -> PathBuf { - if let Ok(exe_path) = std::env::current_exe() { - if let Some(exe_dir) = exe_path.parent() { - return exe_dir.to_path_buf(); + #[cfg(target_os = "linux")] + { + return crate::commands::app_data_root(); + } + #[cfg(not(target_os = "linux"))] + { + if let Ok(exe_path) = std::env::current_exe() { + if let Some(exe_dir) = exe_path.parent() { + return exe_dir.to_path_buf(); + } } + PathBuf::from(".") } - PathBuf::from(".") } /// Возвращает путь к универсальной папке моделей `models/onnx/` pub fn get_models_dir() -> PathBuf { let root = get_app_root_dir(); + #[cfg(target_os = "linux")] + let models_dir = root.join("models").join("ncnn"); + #[cfg(not(target_os = "linux"))] let models_dir = root.join("models").join("onnx"); if !models_dir.exists() { let _ = fs::create_dir_all(&models_dir); @@ -158,6 +168,7 @@ pub fn has_compiled_engine_for_model( } /// Сканирует папку `models/onnx/` и формирует список моделей +#[cfg(not(target_os = "linux"))] pub fn scan_onnx_models_internal() -> Vec { let models_dir = get_models_dir(); let mut items = Vec::new(); @@ -273,7 +284,56 @@ pub fn scan_onnx_models_internal() -> Vec { items } +/// Linux models are distributed as preconverted NCNN `.param` + `.bin` pairs. +#[cfg(target_os = "linux")] +pub fn scan_onnx_models_internal() -> Vec { + let models_dir = get_models_dir(); + let saved_order: Vec = fs::read_to_string(get_models_order_path()) + .ok() + .and_then(|value| serde_json::from_str(&value).ok()) + .unwrap_or_default(); + let mut paths = fs::read_dir(&models_dir) + .into_iter() + .flatten() + .flatten() + .map(|entry| entry.path()) + .filter(|path| { + path.extension().and_then(|ext| ext.to_str()) == Some("param") + && path.with_extension("bin").is_file() + }) + .collect::>(); + paths.sort_by(|a, b| { + let a = a.file_name().unwrap_or_default().to_string_lossy(); + let b = b.file_name().unwrap_or_default().to_string_lossy(); + match ( + saved_order.iter().position(|item| item.eq_ignore_ascii_case(&a)), + saved_order.iter().position(|item| item.eq_ignore_ascii_case(&b)), + ) { + (Some(a), Some(b)) => a.cmp(&b), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => a.cmp(&b), + } + }); + + paths.into_iter().enumerate().map(|(index, path)| { + let filename = path.file_name().unwrap_or_default().to_string_lossy().into_owned(); + let stem = path.file_stem().unwrap_or_default().to_string_lossy(); + let size_bytes = fs::metadata(&path).map(|m| m.len()).unwrap_or(0) + + fs::metadata(path.with_extension("bin")).map(|m| m.len()).unwrap_or(0); + ModelFileItem { + filename, + display_name: stem.replace(['_', '-'], " "), + size_bytes, + slot: (index + 1) as u32, + full_path: path.to_string_lossy().into_owned(), + has_engine_1080p: true, + } + }).collect() +} + /// Проверяет физическое наличие всех требуемых библиотек инференса и статус подсистемы +#[cfg(not(target_os = "linux"))] pub fn check_upscale_status_internal() -> UpscaleStatus { let inf_dir = get_inference_dir(); let root = get_app_root_dir(); @@ -316,6 +376,8 @@ pub fn check_upscale_status_internal() -> UpscaleStatus { aji_present, directml_present, tensorrt_present, + ncnn_present: false, + platform: std::env::consts::OS.to_string(), models_count: models.len(), models_dir: models_dir.to_string_lossy().to_string(), models, @@ -323,6 +385,25 @@ pub fn check_upscale_status_internal() -> UpscaleStatus { } } +#[cfg(target_os = "linux")] +pub fn check_upscale_status_internal() -> UpscaleStatus { + let models_dir = get_models_dir(); + let models = scan_onnx_models_internal(); + let ncnn_present = super::linux::find_ncnn_plugin().is_some(); + UpscaleStatus { + filter_supported: ncnn_present, + aji_present: false, + directml_present: false, + tensorrt_present: false, + ncnn_present, + platform: "linux".to_string(), + models_count: models.len(), + models_dir: models_dir.to_string_lossy().into_owned(), + models, + gpu_info: detect_system_gpu(), + } +} + /// Записывает конфигурационный файл `upscale.conf` для нативного фильтра инференса pub fn write_upscale_conf(backend: &str, default_slot: u32) -> Result { let conf_path = get_upscale_conf_path(); diff --git a/src-tauri/src/upscale/controller.rs b/src-tauri/src/upscale/controller.rs index 69be13b..2260574 100644 --- a/src-tauri/src/upscale/controller.rs +++ b/src-tauri/src/upscale/controller.rs @@ -1,13 +1,17 @@ //! Контроллер взаимодействия с медиа-движком libmpv и OSD при апскейлинге. -use super::config::{ensure_inference_environment, get_inference_dir, get_models_dir, write_upscale_conf}; +use super::config::{get_inference_dir, get_models_dir}; +#[cfg(not(target_os = "linux"))] +use super::config::{ensure_inference_environment, write_upscale_conf}; use super::types::UpscaleSettings; use crate::commands::PlayerState; use crate::mpv_manager::MpvManager; use std::process::Command; +#[cfg(not(target_os = "linux"))] use std::sync::Mutex; use tauri::State; +#[cfg(not(target_os = "linux"))] static LAST_APPLIED_BACKEND: Mutex> = Mutex::new(None); /// Принудительно заставляет mpv перерисовать и отобразить апскейленный кадр, @@ -28,6 +32,11 @@ pub fn apply_upscale_settings_impl( state: &State<'_, PlayerState>, settings: UpscaleSettings, ) -> Result<(), String> { + #[cfg(target_os = "linux")] + return super::linux::apply(state, &settings); + + #[cfg(not(target_os = "linux"))] + { let mut active_slot = settings.active_slot; // Миграция старых CRC-слотов (2000-9999) на новые 1-9 @@ -80,6 +89,7 @@ pub fn apply_upscale_settings_impl( } Ok(()) + } } /// Переключение видов нейросетей по горячим клавишам Shift+1..4 на лету @@ -88,6 +98,11 @@ pub fn switch_upscale_network_hotkey_impl( slot: u32, backend: Option, ) -> Result<(), String> { + #[cfg(target_os = "linux")] + return super::linux::switch_model(state, slot, backend); + + #[cfg(not(target_os = "linux"))] + { let chosen_backend = backend.unwrap_or_else(|| { let last = LAST_APPLIED_BACKEND.lock().unwrap(); last.clone().unwrap_or_else(|| "DirectML".to_string()) @@ -130,12 +145,17 @@ pub fn switch_upscale_network_hotkey_impl( force_frame_refresh(&state.mpv); Ok(()) + } } /// Открытие папки моделей в системном Проводнике Windows pub fn open_models_folder_impl() -> Result<(), String> { let dir = get_models_dir(); - Command::new("explorer.exe") + #[cfg(target_os = "linux")] + let mut command = Command::new("xdg-open"); + #[cfg(not(target_os = "linux"))] + let mut command = Command::new("explorer.exe"); + command .arg(&dir) .spawn() .map_err(|e| format!("Не удалось открыть Проводник: {}", e))?; @@ -145,7 +165,11 @@ pub fn open_models_folder_impl() -> Result<(), String> { /// Открытие каталога библиотек инференса в Проводнике Windows pub fn open_inference_folder_impl() -> Result<(), String> { let dir = get_inference_dir(); - Command::new("explorer.exe") + #[cfg(target_os = "linux")] + let mut command = Command::new("xdg-open"); + #[cfg(not(target_os = "linux"))] + let mut command = Command::new("explorer.exe"); + command .arg(&dir) .spawn() .map_err(|e| format!("Не удалось открыть Проводник: {}", e))?; diff --git a/src-tauri/src/upscale/hardware.rs b/src-tauri/src/upscale/hardware.rs index de5a1d0..f900c85 100644 --- a/src-tauri/src/upscale/hardware.rs +++ b/src-tauri/src/upscale/hardware.rs @@ -212,6 +212,54 @@ pub fn detect_system_gpu() -> GpuHardwareInfo { } fn detect_system_gpu_uncached() -> GpuHardwareInfo { + #[cfg(target_os = "linux")] + { + let summary = std::process::Command::new("vulkaninfo") + .arg("--summary") + .output() + .ok() + .filter(|output| output.status.success()) + .map(|output| String::from_utf8_lossy(&output.stdout).into_owned()) + .unwrap_or_default(); + let name = summary.lines() + .filter_map(|line| line.split_once("deviceName").map(|(_, value)| value.trim_start_matches([' ', '=']).trim().to_string())) + .max_by_key(|value| { + let lower = value.to_lowercase(); + if lower.contains("nvidia") || lower.contains("radeon") || lower.contains("arc") { 2 } + else if lower.contains("intel") || lower.contains("amd") { 1 } + else { 0 } + }) + .filter(|value| !value.is_empty()) + .or_else(|| { + std::process::Command::new("lspci").output().ok().and_then(|output| { + String::from_utf8_lossy(&output.stdout).lines() + .find(|line| line.contains("VGA compatible controller") || line.contains("3D controller")) + .and_then(|line| line.split_once(": ").map(|(_, name)| name.to_string())) + }) + }) + .unwrap_or_else(|| "Vulkan GPU".to_string()); + let lower = name.to_lowercase(); + let (vendor, vendor_id) = if lower.contains("nvidia") { + ("NVIDIA", 0x10DE) + } else if lower.contains("amd") || lower.contains("radeon") { + ("AMD", 0x1002) + } else if lower.contains("intel") { + ("Intel", 0x8086) + } else { + ("Unknown", 0) + }; + return GpuHardwareInfo { + name, + vendor: vendor.to_string(), + vendor_id, + device_id: 0, + recommended_backend: "NCNN Vulkan".to_string(), + supports_tensorrt: false, + sm_architecture: "vulkan".to_string(), + vram_bytes: 0, + }; + } + #[cfg(windows)] { use windows::Win32::Graphics::Dxgi::{ @@ -305,7 +353,8 @@ fn detect_system_gpu_uncached() -> GpuHardwareInfo { } } - GpuHardwareInfo { + #[cfg(not(target_os = "linux"))] + GpuHardwareInfo { name: "Универсальный GPU".to_string(), vendor: "Unknown".to_string(), vendor_id: 0, diff --git a/src-tauri/src/upscale/linux.rs b/src-tauri/src/upscale/linux.rs new file mode 100644 index 0000000..de2f136 --- /dev/null +++ b/src-tauri/src/upscale/linux.rs @@ -0,0 +1,198 @@ +//! Linux NCNN/Vulkan integration through mpv's VapourSynth bridge. + +use super::config::{get_app_root_dir, scan_onnx_models_internal}; +use super::types::UpscaleSettings; +use crate::commands::PlayerState; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use tauri::State; + +const CURATED_MODEL_URL: &str = "https://github.com/TNTwise/Universal-NCNN-Upscaler/releases/download/Animation/2x_AnimeJaNai_V2.zip"; +const CURATED_MODEL_SHA256: &str = "8a627d2697249d64c1e461c3c4f21f8fee500ba77012d7d19936972df00202cf"; + +static WATCHDOG_GENERATION: AtomicU64 = AtomicU64::new(0); + +fn runtime_roots() -> Vec { + let mut roots = Vec::new(); + if let Some(path) = std::env::var_os("L_MPV_RUNTIME_DIR") { + roots.push(path.into()); + } + if let Some(appdir) = std::env::var_os("APPDIR") { + roots.push(PathBuf::from(appdir).join("usr/lib/l-mpv")); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(bin) = exe.parent() { + roots.push(bin.join("../lib/l-mpv")); + roots.push(bin.to_path_buf()); + } + } + roots.push(PathBuf::from("/usr/lib/l-mpv")); + roots.push(get_app_root_dir().join("runtime")); + roots +} + +fn find_runtime_file(relative: &str) -> Option { + runtime_roots() + .into_iter() + .map(|root| root.join(relative)) + .find(|path| path.is_file()) +} + +pub fn find_ncnn_plugin() -> Option { + find_runtime_file("vapoursynth/liblmpv_ncnn.so") +} + +fn mpv_quote(value: &str) -> String { + format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\"")) +} + +fn python_string(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} + +fn write_vapoursynth_script(param: &Path, plugin: &Path) -> Result { + let script_dir = get_app_root_dir().join("config"); + std::fs::create_dir_all(&script_dir).map_err(|e| e.to_string())?; + let script_path = script_dir.join("linux-ncnn.vpy"); + let bin = param.with_extension("bin"); + let content = format!( + "import vapoursynth as vs\n\ + core = vs.core\n\ + core.std.LoadPlugin(path={plugin})\n\ + rgb = core.resize.Bicubic(video_in, format=vs.RGBS, matrix_in_s='709')\n\ + video_out = core.lmpvncnn.Model(rgb, param_path={param}, model_path={bin}, scale=2, tile_size=256, overlap=16, fp16=1)\n", + plugin = python_string(plugin), + param = python_string(param), + bin = python_string(&bin), + ); + std::fs::write(&script_path, content).map_err(|e| e.to_string())?; + Ok(script_path) +} + +fn disable_all(state: &State<'_, PlayerState>) { + WATCHDOG_GENERATION.fetch_add(1, Ordering::AcqRel); + let _ = state.mpv.command("vf remove @lmpv-ai"); + let _ = state.mpv.set_property_string("glsl-shaders", ""); + let _ = state.mpv.set_property_string("hwdec", "auto-safe"); +} + +fn enable_shader_fallback(mpv: &crate::mpv_manager::MpvManager) -> Result<(), String> { + let shaders = [ + "shaders/FSRCNNX_x2_8-0-4-1.glsl", + "shaders/Anime4K_Upscale_CNN_x2_S.glsl", + ] + .into_iter() + .filter_map(find_runtime_file) + .collect::>(); + if shaders.is_empty() { + mpv.set_property_string("scale", "ewa_lanczossharp")?; + return Ok(()); + } + let list = shaders + .iter() + .map(|path| path.to_string_lossy()) + .collect::>() + .join(":"); + mpv.set_property_string("glsl-shaders", &list) +} + +fn start_performance_watchdog(state: &State<'_, PlayerState>) { + let generation = WATCHDOG_GENERATION.fetch_add(1, Ordering::AcqRel) + 1; + let mpv = state.mpv.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(4)); + let initial = mpv.get_property_i64("vo-drop-frame-count").unwrap_or(0) + + mpv.get_property_i64("decoder-frame-drop-count").unwrap_or(0); + std::thread::sleep(std::time::Duration::from_secs(3)); + if WATCHDOG_GENERATION.load(Ordering::Acquire) != generation { + return; + } + let current = mpv.get_property_i64("vo-drop-frame-count").unwrap_or(initial) + + mpv.get_property_i64("decoder-frame-drop-count").unwrap_or(0); + // More than five dropped frames in the sample means the filter cannot + // sustain smooth 24 FPS playback on this GPU/model combination. + if current.saturating_sub(initial) > 5 { + let _ = mpv.command("vf remove @lmpv-ai"); + let _ = enable_shader_fallback(&mpv); + let _ = mpv.command("show-text \"NCNN не успевает: включён шейдерный fallback\" 4000"); + eprintln!("[L-MPV][Upscale] NCNN is too slow; switched to packaged GLSL fallback"); + } + }); +} + +pub fn apply(state: &State<'_, PlayerState>, settings: &UpscaleSettings) -> Result<(), String> { + disable_all(state); + if settings.mode != "ai" { + return Ok(()); + } + let plugin = find_ncnn_plugin().ok_or_else(|| { + "NCNN/Vulkan runtime не найден. Установите полный Linux-пакет L-MPV.".to_string() + })?; + let models = scan_onnx_models_internal(); + let model = models + .iter() + .find(|model| model.filename == settings.selected_model) + .or_else(|| models.iter().find(|model| model.slot == settings.active_slot)) + .ok_or_else(|| "Не выбрана совместимая пара модели NCNN (.param + .bin).".to_string())?; + let script = write_vapoursynth_script(Path::new(&model.full_path), &plugin)?; + + // VapourSynth currently maps frames through system memory, so hardware + // decoding is disabled while the NCNN filter is active. + state.mpv.set_property_string("hwdec", "no")?; + let filter = format!( + "@lmpv-ai:vapoursynth=file={}:buffered-frames=2:concurrent-frames=1", + script.to_string_lossy() + ); + state.mpv.command(&format!("vf add {}", mpv_quote(&filter)))?; + start_performance_watchdog(state); + Ok(()) +} + +pub fn switch_model( + state: &State<'_, PlayerState>, + slot: u32, + backend: Option, +) -> Result<(), String> { + if slot == 0 { + disable_all(state); + return Ok(()); + } + apply(state, &UpscaleSettings { + mode: "ai".to_string(), + active_slot: slot, + backend: backend.unwrap_or_else(|| "NCNN Vulkan".to_string()), + selected_model: String::new(), + }) +} + +pub async fn download_curated_model() -> Result { + use sha2::{Digest, Sha256}; + use std::io::{Cursor, Read}; + + let bytes = reqwest::get(CURATED_MODEL_URL).await + .map_err(|e| format!("Не удалось скачать модель: {e}"))? + .error_for_status().map_err(|e| format!("Сервер модели вернул ошибку: {e}"))? + .bytes().await.map_err(|e| e.to_string())?; + let digest = format!("{:x}", Sha256::digest(&bytes)); + if digest != CURATED_MODEL_SHA256 { + return Err("Контрольная сумма NCNN-модели не совпала.".to_string()); + } + + let models_dir = super::config::get_models_dir(); + let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| e.to_string())?; + let mut installed = 0; + for index in 0..archive.len() { + let mut entry = archive.by_index(index).map_err(|e| e.to_string())?; + let Some(name) = Path::new(entry.name()).file_name().map(|name| name.to_owned()) else { continue }; + let extension = Path::new(&name).extension().and_then(|ext| ext.to_str()).unwrap_or_default(); + if extension != "param" && extension != "bin" { continue; } + let mut data = Vec::new(); + entry.read_to_end(&mut data).map_err(|e| e.to_string())?; + std::fs::write(models_dir.join(name), data).map_err(|e| e.to_string())?; + installed += 1; + } + if installed != 2 { + return Err("Архив не содержит полную пару .param/.bin.".to_string()); + } + Ok("AnimeJaNai V2 NCNN установлена".to_string()) +} diff --git a/src-tauri/src/upscale/mod.rs b/src-tauri/src/upscale/mod.rs index 8bc8e65..6f9f35e 100644 --- a/src-tauri/src/upscale/mod.rs +++ b/src-tauri/src/upscale/mod.rs @@ -6,6 +6,8 @@ pub mod controller; pub mod downloader; pub mod engine_builder; pub mod hardware; +#[cfg(target_os = "linux")] +pub mod linux; pub mod types; // Реэкспорт моделей данных для использования внешними модулями @@ -32,6 +34,8 @@ pub fn get_upscale_status() -> UpscaleStatus { aji_present: false, directml_present: false, tensorrt_present: false, + ncnn_present: false, + platform: std::env::consts::OS.to_string(), models_count: 0, models_dir: String::new(), models: Vec::new(), @@ -114,6 +118,14 @@ pub fn save_models_order(order: Vec) -> Result<(), String> { config::save_models_order_internal(&order) } +#[tauri::command] +pub async fn download_curated_ncnn_model() -> Result { + #[cfg(target_os = "linux")] + return linux::download_curated_model().await; + #[cfg(not(target_os = "linux"))] + Err("Каталог NCNN-моделей доступен только в Linux.".to_string()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/upscale/types.rs b/src-tauri/src/upscale/types.rs index 43cfdd2..65a0396 100644 --- a/src-tauri/src/upscale/types.rs +++ b/src-tauri/src/upscale/types.rs @@ -75,6 +75,10 @@ pub struct UpscaleStatus { pub directml_present: bool, /// Наличие полного набора библиотек NVIDIA TensorRT (aji_trt.dll, nvinfer_11.dll) pub tensorrt_present: bool, + /// Linux NCNN/Vulkan runtime and VapourSynth bridge are available. + pub ncnn_present: bool, + /// Runtime platform used to adapt the settings UI. + pub platform: String, /// Количество обнаруженных ONNX моделей в папке models/onnx/ pub models_count: usize, /// Путь к каталогу моделей @@ -120,4 +124,3 @@ pub struct UpscaleCompileProgress { /// Описание ошибки сборки (если возникла) pub error: Option, } - diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json new file mode 100644 index 0000000..6949139 --- /dev/null +++ b/src-tauri/tauri.linux.conf.json @@ -0,0 +1,37 @@ +{ + "app": { + "windows": [ + { + "label": "main", + "title": "L-MPV", + "width": 1280, + "height": 720, + "minWidth": 560, + "minHeight": 180, + "decorations": true, + "transparent": true, + "resizable": true, + "center": true, + "visible": false + }, + { + "label": "mediainfo", + "title": "L-MPV MediaInfo", + "width": 565, + "height": 685, + "minWidth": 460, + "minHeight": 360, + "decorations": false, + "transparent": false, + "resizable": true, + "alwaysOnTop": true, + "center": true, + "visible": false + } + ] + }, + "bundle": { + "targets": ["appimage"], + "resources": [] + } +} diff --git a/src/App.tsx b/src/App.tsx index 8aebd0f..472d369 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -148,17 +148,20 @@ function App() { // При каждом запуске плеера апскейлинг всегда гарантированно отключен по умолчанию localStorage.setItem("l-mpv-upscale-mode", "off"); - // Фоновая проверка обновлений (показываем ненавязчивое уведомление в правом углу) - invoke("check_launch_and_update") - .then((info) => { - if (info && info.has_update) { - setPendingUpdate(info); - setShowUpdateToast(true); - } + invoke("get_runtime_platform") + .then((platform) => { + document.documentElement.dataset.platform = platform; + if (platform !== "windows") return; + + // Portable-обновление заменяет exe/dll и поэтому доступно только в Windows. + return invoke("check_launch_and_update").then((info) => { + if (info && info.has_update) { + setPendingUpdate(info); + setShowUpdateToast(true); + } + }); }) - .catch((err) => { - console.warn("Фоновая проверка обновлений пропущена:", err); - }); + .catch((err) => console.warn("Определение платформы/проверка обновлений пропущены:", err)); }, []); // Синхронизация настройки автоскрытия панели в верхней половине окна @@ -360,7 +363,7 @@ function App() { const mode = localStorage.getItem("l-mpv-upscale-mode") || "off"; if (mode === "ai") { const slot = parseInt(localStorage.getItem("l-mpv-upscale-slot") || "1001", 10); - const backend = localStorage.getItem("l-mpv-upscale-backend") || "DirectML"; + const backend = localStorage.getItem("l-mpv-upscale-backend") || (document.documentElement.dataset.platform === "linux" ? "NCNN Vulkan" : "DirectML"); const selectedModel = localStorage.getItem("l-mpv-upscale-selected-model") || ""; invoke("apply_upscale_settings", { settings: { mode, active_slot: slot, backend, selected_model: selectedModel } @@ -701,7 +704,7 @@ function App() { case "upscaleStats": { try { const mode = localStorage.getItem("l-mpv-upscale-mode") || "off"; - const backend = localStorage.getItem("l-mpv-upscale-backend") || "DirectML"; + const backend = localStorage.getItem("l-mpv-upscale-backend") || (document.documentElement.dataset.platform === "linux" ? "NCNN Vulkan" : "DirectML"); const slot = parseInt(localStorage.getItem("l-mpv-upscale-slot") || "1001", 10); const selectedModel = localStorage.getItem("l-mpv-upscale-selected-model") || ""; @@ -761,7 +764,7 @@ function App() { } case "upscaleOff": try { - const backend = localStorage.getItem("l-mpv-upscale-backend") || "DirectML"; + const backend = localStorage.getItem("l-mpv-upscale-backend") || (document.documentElement.dataset.platform === "linux" ? "NCNN Vulkan" : "DirectML"); await invoke("switch_upscale_network_hotkey", { slot: 0, backend }); localStorage.setItem("l-mpv-upscale-mode", "off"); setOsdText("4K AI Апскейлинг: Выключен"); @@ -789,7 +792,7 @@ function App() { if (targetModel) { const slot = targetModel.slot; - const backend = localStorage.getItem("l-mpv-upscale-backend") || "DirectML"; + const backend = localStorage.getItem("l-mpv-upscale-backend") || (document.documentElement.dataset.platform === "linux" ? "NCNN Vulkan" : "DirectML"); // При выборе любой модели по хоткею гарантированно активируем режим "ai" localStorage.setItem("l-mpv-upscale-mode", "ai"); diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index ce0a04d..1a736ba 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -103,6 +103,9 @@ export { AccordionSection } from "./settings/AccordionSection"; export type { AccordionSectionProps } from "./settings/AccordionSection"; export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) { + const [runtimePlatform, setRuntimePlatform] = useState( + () => document.documentElement.dataset.platform || "windows", + ); const [screenshotDir, setScreenshotDir] = useState(""); const [uiOpacity, setUiOpacity] = useState(() => getSavedUiOpacity()); const [activeColor, setActiveColor] = useState(() => { @@ -136,6 +139,10 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) { const [controlBarStyle, setControlBarStyle] = useState(() => getSavedControlBarStyle()); const [activeTab, setActiveTab] = useState<"general" | "appearance" | "presets" | "upscaling" | "hotkeys" | "integration">("general"); + useEffect(() => { + invoke("get_runtime_platform").then(setRuntimePlatform).catch(() => {}); + }, []); + const [isClosing, setIsClosing] = useState(false); const closeTimerRef = useRef | null>(null); @@ -190,8 +197,8 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) { "presets", "upscaling", "hotkeys", - "integration", ]; + if (runtimePlatform === "windows") tabs.push("integration"); const currentIndex = tabs.indexOf(activeTab); if (e.key === "ArrowRight") { @@ -213,7 +220,7 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) { return () => { window.removeEventListener("keydown", handleKeyDown, true); }; - }, [activeTab, isRecordingHotkey, handleClose]); + }, [activeTab, isRecordingHotkey, handleClose, runtimePlatform]); // Синхронизация локальных состояний SettingsModal при применении любого пресета const handlePresetApplied = useCallback((preset: SettingsPreset) => { @@ -589,7 +596,7 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) { { id: "upscaling", label: "Апскейлинг", icon: Sparkles }, { id: "hotkeys", label: "Хоткей", icon: Keyboard }, { id: "integration", label: "Интеграция", icon: Link }, - ].map((tab) => { + ].filter((tab) => runtimePlatform === "windows" || tab.id !== "integration").map((tab) => { const Icon = tab.icon; const isActive = activeTab === tab.id; return ( @@ -732,7 +739,7 @@ export function SettingsModal({ onClose, onShowUpdate }: SettingsModalProps) { v{appVersion} - + } {updateStatus && ( )} - Портативная редакция + + {runtimePlatform === "windows" ? "Портативная редакция" : "Linux · Wayland/X11"} + diff --git a/src/components/settings/AppearanceSettingsTab.tsx b/src/components/settings/AppearanceSettingsTab.tsx index 24819d2..5b1c4d3 100644 --- a/src/components/settings/AppearanceSettingsTab.tsx +++ b/src/components/settings/AppearanceSettingsTab.tsx @@ -820,10 +820,10 @@ export function AppearanceSettingsTab(props: AppearanceSettingsTabProps) { {/* 5. Аудио-визуалайзер на панели управления */} - toggleSection("app_visualizer")} - /> + />} {/* 6. Видимость кнопок панели управления */} { + ].filter(btn => document.documentElement.dataset.platform !== "linux" || btn.id !== "visualizer").map(btn => { const isChecked = visibleButtons[btn.id] !== undefined ? visibleButtons[btn.id] : btn.defaultChecked; diff --git a/src/components/settings/UpscalingSettingsSection.tsx b/src/components/settings/UpscalingSettingsSection.tsx index d160429..a481d75 100644 --- a/src/components/settings/UpscalingSettingsSection.tsx +++ b/src/components/settings/UpscalingSettingsSection.tsx @@ -7,6 +7,7 @@ import { Layers, Eye, EyeOff, + Download, } from "lucide-react"; import { getCustomHotkeys, @@ -50,6 +51,7 @@ export const UpscalingSettingsSection: React.FC = const [errorMessage, setErrorMessage] = useState(null); const [downloadProgressText, setDownloadProgressText] = useState(null); const [downloadProgress, setDownloadProgress] = useState(null); + const [downloadingNcnnModel, setDownloadingNcnnModel] = useState(false); // Хоткеи const [customHotkeys, setCustomHotkeys] = useState>(() => getCustomHotkeys()); @@ -127,7 +129,7 @@ export const UpscalingSettingsSection: React.FC = const [settings, setSettings] = useState(() => { try { const savedMode = (localStorage.getItem("l-mpv-upscale-mode") as "off" | "ai") || "off"; - const savedBackend = (localStorage.getItem("l-mpv-upscale-backend") as "DirectML" | "TensorRT") || "DirectML"; + const savedBackend = (localStorage.getItem("l-mpv-upscale-backend") as UpscaleSettings["backend"]) || "DirectML"; const savedSlot = Number(localStorage.getItem("l-mpv-upscale-slot") || 1001); const savedModel = localStorage.getItem("l-mpv-upscale-selected-model") || ""; return { @@ -166,6 +168,10 @@ export const UpscalingSettingsSection: React.FC = const currentStatus = await invoke("get_upscale_status"); if (isMountedRef.current) { setStatus(currentStatus); + if (currentStatus.platform === "linux") { + setSettings((previous) => ({ ...previous, backend: "NCNN Vulkan" })); + localStorage.setItem("l-mpv-upscale-backend", "NCNN Vulkan"); + } // Очищаем ошибку загрузки статуса при успешном получении setErrorMessage((prev) => prev === "Не удалось получить статус компонентов апскейлинга" ? null : prev @@ -268,7 +274,7 @@ export const UpscalingSettingsSection: React.FC = if (isMountedRef.current) { setCustomHotkeys(getCustomHotkeys()); const savedMode = (localStorage.getItem("l-mpv-upscale-mode") as "off" | "ai") || "off"; - const savedBackend = (localStorage.getItem("l-mpv-upscale-backend") as "DirectML" | "TensorRT") || "DirectML"; + const savedBackend = (localStorage.getItem("l-mpv-upscale-backend") as UpscaleSettings["backend"]) || "DirectML"; const savedSlot = Number(localStorage.getItem("l-mpv-upscale-slot") || 1001); const savedModel = localStorage.getItem("l-mpv-upscale-selected-model") || ""; setSettings({ @@ -306,7 +312,9 @@ export const UpscalingSettingsSection: React.FC = // Проверяем, установлен ли движок при попытке включить режим "ai" const isDmlInstalled = !!(status?.directml_present && status?.aji_present); const isTrtInstalled = !!(status?.tensorrt_present && status?.aji_present); - const isInstalled = updated.backend === "DirectML" ? isDmlInstalled : isTrtInstalled; + const isInstalled = updated.backend === "NCNN Vulkan" + ? !!status?.ncnn_present + : updated.backend === "DirectML" ? isDmlInstalled : isTrtInstalled; if (updated.mode === "ai" && !isInstalled) { setErrorMessage(`Для включения апскейлинга необходимо сначала скачать библиотеки движка ${updated.backend}.`); @@ -325,7 +333,7 @@ export const UpscalingSettingsSection: React.FC = }; // Переключение выбранного бэкенда без лишних перерисовок кадра, если апскейл выключен - const handleSelectBackend = async (backend: "DirectML" | "TensorRT") => { + const handleSelectBackend = async (backend: UpscaleSettings["backend"]) => { const updated: UpscaleSettings = { ...settings, backend }; setSettings(updated); setErrorMessage(null); @@ -335,7 +343,9 @@ export const UpscalingSettingsSection: React.FC = if (updated.mode === "ai") { const isDmlInstalled = !!(status?.directml_present && status?.aji_present); const isTrtInstalled = !!(status?.tensorrt_present && status?.aji_present); - const isInstalled = backend === "DirectML" ? isDmlInstalled : isTrtInstalled; + const isInstalled = backend === "NCNN Vulkan" + ? !!status?.ncnn_present + : backend === "DirectML" ? isDmlInstalled : isTrtInstalled; if (isInstalled) { try { @@ -368,6 +378,20 @@ export const UpscalingSettingsSection: React.FC = } }; + const handleDownloadNcnnModel = async () => { + setDownloadingNcnnModel(true); + setErrorMessage(null); + try { + const message = await invoke("download_curated_ncnn_model"); + setEngineSuccessMessage(message); + await refreshStatus(); + } catch (error) { + setErrorMessage(String(error)); + } finally { + setDownloadingNcnnModel(false); + } + }; + // Фоновое скачивание библиотек инференса с прогресс-баром const handleDownloadEngine = async () => { if (downloadTimerRef.current) { @@ -553,14 +577,25 @@ export const UpscalingSettingsSection: React.FC =

- Папка нейросетей (models/onnx/) + Папка нейросетей ({status?.platform === "linux" ? "models/ncnn/" : "models/onnx/"})

- Обнаружено файлов ONNX-моделей: {status?.models_count || 0} + Обнаружено моделей {status?.platform === "linux" ? "NCNN" : "ONNX"}: {status?.models_count || 0}

+ {status?.platform === "linux" && ( + + )}
+ ) : isLinux ? ( +
+ NCNN runtime отсутствует +
) : ( )} - {isAnyEnginePresent && ( + {!isLinux && isAnyEnginePresent && (
)} - {/* Выбор между DirectML и TensorRT */} + {isLinux ? ( +
+
+
+ + NCNN Vulkan + + + {isNcnnInstalled ? "Установлен" : "Не установлен"} + + Рекомендуется +
+ +
+

+ Кросс-вендорный Vulkan-инференс для NVIDIA, AMD и Intel. При недостаточной скорости плеер автоматически включает FSRCNNX/Anime4K. +

+
+ ) : ( + /* Выбор между DirectML и TensorRT */
{/* Карточка DirectML */}
= ({

+ )} ); }; diff --git a/src/components/upscale/types.ts b/src/components/upscale/types.ts index 8a125ae..cbe28d5 100644 --- a/src/components/upscale/types.ts +++ b/src/components/upscale/types.ts @@ -12,7 +12,7 @@ export interface GpuHardwareInfo { vendor: string; vendor_id: number; device_id: number; - recommended_backend: "TensorRT" | "DirectML"; + recommended_backend: "TensorRT" | "DirectML" | "NCNN Vulkan"; supports_tensorrt: boolean; sm_architecture: string; vram_bytes: number; @@ -23,6 +23,8 @@ export interface UpscaleStatus { aji_present: boolean; directml_present: boolean; tensorrt_present: boolean; + ncnn_present: boolean; + platform: string; models_count: number; models_dir: string; models: ModelFileItem[]; @@ -32,7 +34,7 @@ export interface UpscaleStatus { export interface UpscaleSettings { mode: "off" | "ai"; active_slot: number; - backend: "DirectML" | "TensorRT"; + backend: "DirectML" | "TensorRT" | "NCNN Vulkan"; selected_model: string; } @@ -54,4 +56,3 @@ export interface UpscaleCompileProgress { is_finished: boolean; error?: string | null; } - diff --git a/src/index.css b/src/index.css index 3c9585c..c5d9bb3 100644 --- a/src/index.css +++ b/src/index.css @@ -44,3 +44,27 @@ /* 13. Пресеты настроек */ @import './styles/presets.css'; +/* WASAPI visualizer is intentionally unavailable in the Linux build. */ +html[data-platform='linux'] #btn-visualizer { + display: none !important; +} + +html[data-platform='linux'] .color-circle--windows { + display: none !important; +} + +/* X11 native child windows do not forward pointer motion through GtkGLArea, + so the DOM idle timer cannot reliably reveal controls again. Keep Linux + controls available; Windows retains the cinematic auto-hide behavior. */ +html[data-platform='linux'] .app-container--idle .titlebar, +html[data-platform='linux'] .app-container--hide-controls .titlebar, +html[data-platform='linux'] .app-container--idle .player-controls-wrapper, +html[data-platform='linux'] .app-container--hide-controls .player-controls-wrapper { + opacity: 1; + transform: none; + pointer-events: auto; +} + +html[data-platform='linux'] .app-container--idle { + cursor: default; +} diff --git a/vendor/tauri-runtime-wry/.cargo-ok b/vendor/tauri-runtime-wry/.cargo-ok new file mode 100644 index 0000000..5f8b795 --- /dev/null +++ b/vendor/tauri-runtime-wry/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/tauri-runtime-wry/.cargo_vcs_info.json b/vendor/tauri-runtime-wry/.cargo_vcs_info.json new file mode 100644 index 0000000..e517113 --- /dev/null +++ b/vendor/tauri-runtime-wry/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "ca90b46b2e2cbbc981dae1b809f4af4343fe0558" + }, + "path_in_vcs": "crates/tauri-runtime-wry" +} \ No newline at end of file diff --git a/vendor/tauri-runtime-wry/Cargo.lock b/vendor/tauri-runtime-wry/Cargo.lock new file mode 100644 index 0000000..30c6e0e --- /dev/null +++ b/vendor/tauri-runtime-wry/Cargo.lock @@ -0,0 +1,3593 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.7.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.7.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.7.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.7.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.60.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.7.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.7.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.7.0", + "libc", +] + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.7.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" +dependencies = [ + "bitflags 2.7.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31f4c5b5192304996badc466aeadffe1411d73a9bbd3b18b6b2ee9d048b07bd" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.7.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.7.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.7.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" +dependencies = [ + "bitflags 2.7.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" +dependencies = [ + "bitflags 2.7.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.3.0", + "objc2-quartz-core 0.3.0", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fe793adbf3b5e93686d48a05a7ed7ee53dfa65d106ced4805fae8969059b2" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" +dependencies = [ + "bitflags 2.7.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64", + "indexmap 2.11.4", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.24", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.7.0", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror 2.0.12", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.7.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics 0.24.0", + "foreign-types", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.20", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159" +dependencies = [ + "bitflags 2.7.0", + "block2 0.6.2", + "core-foundation", + "core-graphics 0.25.0", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation 0.3.0", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.4", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.12", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.4", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "tracing", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "ctor", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "regex", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "thiserror 2.0.12", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned 0.6.8", + "toml_datetime 0.6.8", + "toml_edit 0.22.24", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.11.4", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime 0.6.8", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime 0.6.8", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap 2.11.4", + "serde", + "serde_spanned 0.6.8", + "toml_datetime 0.6.8", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "serde", +] + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.0", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +dependencies = [ + "thiserror 2.0.12", + "windows", + "windows-core 0.61.0", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.0", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-version" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c12476c23a74725c539b24eae8bfc0dac4029c39cdb561d9f23616accd4ae26d" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wry" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429" +dependencies = [ + "base64", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.12", + "tracing", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] diff --git a/vendor/tauri-runtime-wry/Cargo.toml b/vendor/tauri-runtime-wry/Cargo.toml new file mode 100644 index 0000000..0b2a1b4 --- /dev/null +++ b/vendor/tauri-runtime-wry/Cargo.toml @@ -0,0 +1,151 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.77.2" +name = "tauri-runtime-wry" +version = "2.11.4" +authors = ["Tauri Programme within The Commons Conservancy"] +build = "build.rs" +exclude = [ + "CHANGELOG.md", + "/target", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Wry bindings to the Tauri runtime" +homepage = "https://tauri.app/" +readme = "README.md" +categories = [ + "gui", + "web-programming", +] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/tauri-apps/tauri" + +[features] +common-controls-v6 = [] +dbus = ["tao/dbus"] +default = [ + "x11", + "dbus", +] +devtools = [ + "wry/devtools", + "tauri-runtime/devtools", +] +macos-private-api = [ + "wry/fullscreen", + "wry/transparent", + "tauri-runtime/macos-private-api", +] +macos-proxy = ["wry/mac-proxy"] +objc-exception = [] +tracing = [ + "dep:tracing", + "wry/tracing", +] +unstable = [] +x11 = [ + "tao/x11", + "wry/x11", +] + +[lib] +name = "tauri_runtime_wry" +path = "src/lib.rs" + +[dependencies.http] +version = "1" + +[dependencies.log] +version = "0.4.21" + +[dependencies.raw-window-handle] +version = "0.6" + +[dependencies.tao] +version = "0.35.0" +features = ["rwh_06"] +default-features = false + +[dependencies.tauri-runtime] +version = "2.11.3" + +[dependencies.tauri-utils] +version = "2.9.3" + +[dependencies.tracing] +version = "0.1" +optional = true + +[dependencies.url] +version = "2" + +[dependencies.wry] +version = "0.55.0" +features = [ + "protocol", + "os-webview", + "linux-body", +] +default-features = false + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gtk] +version = "0.18" +features = ["v3_24"] + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.percent-encoding] +version = "2" + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.webkit2gtk] +version = "=2.0" +features = ["v2_40"] + +[target.'cfg(target_os = "android")'.dependencies.jni] +version = "0.21" + +[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit] +version = "0.3" +features = [ + "block2", + "NSApplication", + "NSResponder", + "NSView", + "NSWindow", + "NSScreen", + "NSGraphics", +] +default-features = false + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2] +version = "0.6" + +[target."cfg(windows)".dependencies.once_cell] +version = "1.20" + +[target."cfg(windows)".dependencies.softbuffer] +version = "0.4" +default-features = false + +[target."cfg(windows)".dependencies.webview2-com] +version = "0.38" + +[target."cfg(windows)".dependencies.windows] +version = "0.61" +features = [ + "Win32_Foundation", + "Win32_Graphics_Dwm", +] diff --git a/vendor/tauri-runtime-wry/Cargo.toml.orig b/vendor/tauri-runtime-wry/Cargo.toml.orig new file mode 100644 index 0000000..7c576de --- /dev/null +++ b/vendor/tauri-runtime-wry/Cargo.toml.orig @@ -0,0 +1,76 @@ +[package] +name = "tauri-runtime-wry" +version = "2.11.4" +description = "Wry bindings to the Tauri runtime" +exclude = ["CHANGELOG.md", "/target"] +readme = "README.md" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +wry = { version = "0.55.0", default-features = false, features = [ + "protocol", + "os-webview", + "linux-body", +] } +tao = { version = "0.35.0", default-features = false, features = ["rwh_06"] } +tauri-runtime = { version = "2.11.3", path = "../tauri-runtime" } +tauri-utils = { version = "2.9.3", path = "../tauri-utils" } +raw-window-handle = "0.6" +http = "1" +url = "2" +tracing = { version = "0.1", optional = true } +log = "0.4.21" + +[target."cfg(windows)".dependencies] +webview2-com = "0.38" +softbuffer = { version = "0.4", default-features = false } +once_cell = "1.20" + +[target."cfg(windows)".dependencies.windows] +version = "0.61" +features = ["Win32_Foundation", "Win32_Graphics_Dwm"] + +[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +gtk = { version = "0.18", features = ["v3_24"] } +webkit2gtk = { version = "=2.0", features = ["v2_40"] } +percent-encoding = "2" + +[target.'cfg(target_vendor = "apple")'.dependencies] +objc2 = "0.6" + +[target.'cfg(target_os = "macos")'.dependencies] +objc2-app-kit = { version = "0.3", default-features = false, features = [ + "block2", + "NSApplication", + "NSResponder", + "NSView", + "NSWindow", + "NSScreen", + "NSGraphics", +] } + +[target."cfg(target_os = \"android\")".dependencies] +jni = "0.21" + +[features] +default = ["x11", "dbus"] +devtools = ["wry/devtools", "tauri-runtime/devtools"] +x11 = ["tao/x11", "wry/x11"] +macos-private-api = [ + "wry/fullscreen", + "wry/transparent", + "tauri-runtime/macos-private-api", +] +# TODO: Remove in v3 - wry does not have this feature anymore +objc-exception = [] +tracing = ["dep:tracing", "wry/tracing"] +macos-proxy = ["wry/mac-proxy"] +unstable = [] +common-controls-v6 = [] +dbus = ["tao/dbus"] diff --git a/vendor/tauri-runtime-wry/LICENSE_APACHE-2.0 b/vendor/tauri-runtime-wry/LICENSE_APACHE-2.0 new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/vendor/tauri-runtime-wry/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/tauri-runtime-wry/LICENSE_MIT b/vendor/tauri-runtime-wry/LICENSE_MIT new file mode 100644 index 0000000..b08530d --- /dev/null +++ b/vendor/tauri-runtime-wry/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/tauri-runtime-wry/README.md b/vendor/tauri-runtime-wry/README.md new file mode 100644 index 0000000..a452527 --- /dev/null +++ b/vendor/tauri-runtime-wry/README.md @@ -0,0 +1,44 @@ +# tauri-runtime-wry + + + +[![status](https://img.shields.io/badge/Status-Beta-green.svg)](https://github.com/tauri-apps/tauri) +[![Chat Server](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/SpmNs4S) + +[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) + +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Opencollective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------- | +| tauri-runtime-wry | [![](https://img.shields.io/crates/v/tauri-runtime-wry?style=flat-square)](https://crates.io/crates/tauri-runtime-wry) | + +## About Tauri + +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. + +## This module + +This crate opens up direct systems-level interactions specifically for WRY, such as printing, monitor detection, and other windowing related tasks. `tauri-runtime` implementation for WRY. +None of the exposed API of this crate is stable, and it may break semver +compatibility in the future. The major version only signifies the intended Tauri version. + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + +## Semver + +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). + +## Licenses + +Code: (c) 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND + +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/vendor/tauri-runtime-wry/build.rs b/vendor/tauri-runtime-wry/build.rs new file mode 100644 index 0000000..9974c50 --- /dev/null +++ b/vendor/tauri-runtime-wry/build.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// creates a cfg alias if `has_feature` is true. +// `alias` must be a snake case string. +fn alias(alias: &str, has_feature: bool) { + println!("cargo:rustc-check-cfg=cfg({alias})"); + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } +} + +fn main() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "ios" || target_os == "android"; + alias("desktop", !mobile); + alias("mobile", mobile); +} diff --git a/vendor/tauri-runtime-wry/src/dialog/mod.rs b/vendor/tauri-runtime-wry/src/dialog/mod.rs new file mode 100644 index 0000000..83d8922 --- /dev/null +++ b/vendor/tauri-runtime-wry/src/dialog/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(windows)] +mod windows; + +// Takes a `&'static str` here since we convert clickable hyperlinks, +// DO NOT pass in untrusted input +#[cfg_attr(not(windows), allow(unused))] +pub fn error(err: &'static str) { + #[cfg(windows)] + windows::error(err); + + #[cfg(not(windows))] + { + unimplemented!("Error dialog is not implemented for this platform"); + } +} diff --git a/vendor/tauri-runtime-wry/src/dialog/windows.rs b/vendor/tauri-runtime-wry/src/dialog/windows.rs new file mode 100644 index 0000000..0f9b2c2 --- /dev/null +++ b/vendor/tauri-runtime-wry/src/dialog/windows.rs @@ -0,0 +1,124 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use windows::core::{w, HSTRING}; + +enum Level { + Error, + #[allow(unused)] + Warning, + #[allow(unused)] + Info, +} + +pub fn error(err: &'static str) { + dialog_inner(err, Level::Error); +} + +fn dialog_inner(err: &str, level: Level) { + let title = match level { + Level::Warning => w!("Warning"), + Level::Error => w!("Error"), + Level::Info => w!("Info"), + }; + + #[cfg(not(feature = "common-controls-v6"))] + { + use windows::Win32::UI::WindowsAndMessaging::*; + + let err = remove_hyperlink(err); + let err = HSTRING::from(err); + + unsafe { + MessageBoxW( + None, + &err, + title, + match level { + Level::Warning => MB_ICONWARNING, + Level::Error => MB_ICONERROR, + Level::Info => MB_ICONINFORMATION, + }, + ) + }; + } + + #[cfg(feature = "common-controls-v6")] + { + use windows::core::{HRESULT, PCWSTR}; + use windows::Win32::Foundation::*; + use windows::Win32::UI::Controls::*; + use windows::Win32::UI::Shell::*; + use windows::Win32::UI::WindowsAndMessaging::*; + + extern "system" fn task_dialog_callback( + _hwnd: HWND, + msg: TASKDIALOG_NOTIFICATIONS, + _wparam: WPARAM, + lparam: LPARAM, + _data: isize, + ) -> HRESULT { + if msg == TDN_HYPERLINK_CLICKED { + let link = PCWSTR(lparam.0 as _); + let _ = unsafe { ShellExecuteW(None, None, link, None, None, SW_SHOWNORMAL) }; + } + + S_OK + } + + let err = HSTRING::from(err); + let err = PCWSTR(err.as_ptr()); + + let task_dialog_config = TASKDIALOGCONFIG { + cbSize: std::mem::size_of::() as u32, + dwFlags: TDF_ALLOW_DIALOG_CANCELLATION | TDF_ENABLE_HYPERLINKS, + pszWindowTitle: title, + pszContent: err, + Anonymous1: TASKDIALOGCONFIG_0 { + pszMainIcon: match level { + Level::Warning => TD_WARNING_ICON, + Level::Error => TD_ERROR_ICON, + Level::Info => TD_INFORMATION_ICON, + }, + }, + dwCommonButtons: TDCBF_OK_BUTTON, + pfCallback: Some(task_dialog_callback), + ..Default::default() + }; + + let _ = unsafe { TaskDialogIndirect(&task_dialog_config, None, None, None) }; + } +} + +#[cfg(not(feature = "common-controls-v6"))] +fn remove_hyperlink(str: &str) -> String { + let mut result = String::new(); + let mut in_hyperlink = false; + + for c in str.chars() { + if c == '<' { + in_hyperlink = true; + } else if c == '>' { + in_hyperlink = false; + } else if !in_hyperlink { + result.push(c); + } + } + + result +} + +#[cfg(test)] +#[cfg(not(feature = "common-controls-v6"))] +mod tests { + use super::*; + + #[test] + fn test_remove_hyperlink() { + let input = "This is a test string."; + let expected = "This is a test string."; + let result = remove_hyperlink(input); + assert_eq!(result, expected); + } +} diff --git a/vendor/tauri-runtime-wry/src/lib.rs b/vendor/tauri-runtime-wry/src/lib.rs new file mode 100644 index 0000000..3a08bc4 --- /dev/null +++ b/vendor/tauri-runtime-wry/src/lib.rs @@ -0,0 +1,5510 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The [`wry`] Tauri [`Runtime`]. +//! +//! None of the exposed API of this crate is stable, and it may break semver +//! compatibility in the future. The major version only signifies the intended Tauri version. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] + +use self::monitor::MonitorExt; +use http::Request; +#[cfg(target_os = "macos")] +use objc2::ClassType; +use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle}; + +#[cfg(windows)] +use tauri_runtime::webview::ScrollBarStyle; +use tauri_runtime::{ + dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, + monitor::Monitor, + webview::{DetachedWebview, DownloadEvent, PendingWebview, WebviewIpcHandler}, + window::{ + CursorIcon, DetachedWindow, DetachedWindowWebview, DragDropEvent, PendingWindow, RawWindow, + WebviewEvent, WindowBuilder, WindowBuilderBase, WindowEvent, WindowId, WindowSizeConstraints, + }, + Cookie, DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, + ProgressBarState, ProgressBarStatus, Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, + UserAttentionType, UserEvent, WebviewDispatch, WebviewEventId, WindowDispatch, WindowEventId, +}; + +#[cfg(target_vendor = "apple")] +use objc2::rc::Retained; +#[cfg(target_os = "android")] +use tao::platform::android::{WindowBuilderExtAndroid, WindowExtAndroid}; +#[cfg(target_os = "macos")] +use tao::platform::macos::{EventLoopWindowTargetExtMacOS, WindowBuilderExtMacOS}; +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use tao::platform::unix::{WindowBuilderExtUnix, WindowExtUnix}; +#[cfg(windows)] +use tao::platform::windows::{WindowBuilderExtWindows, WindowExtWindows}; +#[cfg(windows)] +use webview2_com::{ContainsFullScreenElementChangedEventHandler, FocusChangedEventHandler}; +#[cfg(windows)] +use windows::Win32::Foundation::HWND; +#[cfg(target_os = "ios")] +use wry::WebViewBuilderExtIos; +#[cfg(target_os = "macos")] +use wry::WebViewBuilderExtMacos; +#[cfg(windows)] +use wry::WebViewBuilderExtWindows; +#[cfg(target_vendor = "apple")] +use wry::{WebViewBuilderExtDarwin, WebViewExtDarwin}; + +use tao::{ + dpi::{ + LogicalPosition as TaoLogicalPosition, LogicalSize as TaoLogicalSize, + PhysicalPosition as TaoPhysicalPosition, PhysicalSize as TaoPhysicalSize, + Position as TaoPosition, Size as TaoSize, + }, + event::{Event, StartCause, WindowEvent as TaoWindowEvent}, + event_loop::{ + ControlFlow, DeviceEventFilter as TaoDeviceEventFilter, EventLoop, EventLoopBuilder, + EventLoopProxy as TaoEventLoopProxy, EventLoopWindowTarget, + }, + monitor::MonitorHandle, + window::{ + CursorIcon as TaoCursorIcon, Fullscreen, Icon as TaoWindowIcon, + ProgressBarState as TaoProgressBarState, ProgressState as TaoProgressState, Theme as TaoTheme, + UserAttentionType as TaoUserAttentionType, + }, +}; +use tauri_utils::config::PreventOverflowConfig; +#[cfg(target_os = "macos")] +use tauri_utils::TitleBarStyle; +use tauri_utils::{ + config::{Color, WindowConfig}, + Theme, +}; +use url::Url; +#[cfg(windows)] +use wry::ScrollBarStyle as WryScrollBarStyle; +use wry::{ + DragDropEvent as WryDragDropEvent, ProxyConfig, ProxyEndpoint, WebContext as WryWebContext, + WebView, WebViewBuilder, +}; + +pub use tao; +pub use tao::window::{Window, WindowBuilder as TaoWindowBuilder, WindowId as TaoWindowId}; +pub use wry; +pub use wry::webview_version; + +#[cfg(windows)] +use wry::WebViewExtWindows; +#[cfg(target_os = "android")] +use wry::{ + prelude::{dispatch, find_class}, + WebViewBuilderExtAndroid, WebViewExtAndroid, +}; +#[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" +)))] +use wry::{WebViewBuilderExtUnix, WebViewExtUnix}; + +#[cfg(target_os = "ios")] +pub use tao::platform::ios::{WindowBuilderExtIOS, WindowExtIOS}; +#[cfg(target_os = "macos")] +pub use tao::platform::macos::{ + ActivationPolicy as TaoActivationPolicy, EventLoopExtMacOS, WindowExtMacOS, +}; +#[cfg(target_os = "macos")] +use tauri_runtime::ActivationPolicy; + +use std::{ + cell::RefCell, + collections::{ + hash_map::Entry::{Occupied, Vacant}, + BTreeMap, HashMap, HashSet, + }, + fmt, + ops::Deref, + path::PathBuf, + rc::Rc, + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + mpsc::{channel, Sender}, + Arc, Mutex, Weak, + }, + thread::{current as current_thread, ThreadId}, +}; + +pub type WebviewId = u32; +type IpcHandler = dyn Fn(Request) + 'static; + +#[cfg(not(debug_assertions))] +mod dialog; +mod monitor; +#[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod undecorated_resizing; +mod util; +mod webview; +mod window; + +pub use webview::Webview; +use window::WindowExt as _; + +#[derive(Debug)] +pub struct WebContext { + pub inner: WryWebContext, + pub referenced_by_webviews: HashSet, + // on Linux the custom protocols are associated with the context + // and you cannot register a URI scheme more than once + pub registered_custom_protocols: HashSet, +} + +pub type WebContextStore = Arc, WebContext>>>; +// window +pub type WindowEventHandler = Box; +pub type WindowEventListeners = Arc>>; +pub type WebviewEventHandler = Box; +pub type WebviewEventListeners = Arc>>; + +#[derive(Debug, Clone, Default)] +pub struct WindowIdStore(Arc>>); + +impl WindowIdStore { + pub fn insert(&self, w: TaoWindowId, id: WindowId) { + self.0.lock().unwrap().insert(w, id); + } + + pub fn get(&self, w: &TaoWindowId) -> Option { + self.0.lock().unwrap().get(w).copied() + } +} + +#[macro_export] +macro_rules! getter { + ($self: ident, $rx: expr, $message: expr) => {{ + $crate::send_user_message(&$self.context, $message)?; + $rx + .recv() + .map_err(|_| $crate::Error::FailedToReceiveMessage) + }}; +} + +macro_rules! window_getter { + ($self: ident, $message: expr) => {{ + let (tx, rx) = channel(); + getter!($self, rx, Message::Window($self.window_id, $message(tx))) + }}; +} + +macro_rules! event_loop_window_getter { + ($self: ident, $message: expr) => {{ + let (tx, rx) = channel(); + getter!($self, rx, Message::EventLoopWindowTarget($message(tx))) + }}; +} + +macro_rules! webview_getter { + ($self: ident, $message: expr) => {{ + let (tx, rx) = channel(); + getter!( + $self, + rx, + Message::Webview( + *$self.window_id.lock().unwrap(), + $self.webview_id, + $message(tx) + ) + ) + }}; +} + +pub(crate) fn send_user_message( + context: &Context, + message: Message, +) -> Result<()> { + if current_thread().id() == context.main_thread_id { + handle_user_message( + &context.main_thread.window_target, + message, + UserMessageContext { + window_id_map: context.window_id_map.clone(), + windows: context.main_thread.windows.clone(), + }, + ); + Ok(()) + } else { + context + .proxy + .send_event(message) + .map_err(|_| Error::FailedToSendMessage) + } +} + +#[derive(Clone)] +pub struct Context { + pub window_id_map: WindowIdStore, + main_thread_id: ThreadId, + pub proxy: TaoEventLoopProxy>, + main_thread: DispatcherMainThreadContext, + plugins: Arc + Send>>>>, + next_window_id: Arc, + next_webview_id: Arc, + next_window_event_id: Arc, + next_webview_event_id: Arc, + webview_runtime_installed: bool, +} + +impl Context { + pub fn run_threaded(&self, f: F) -> R + where + F: FnOnce(Option<&DispatcherMainThreadContext>) -> R, + { + f(if current_thread().id() == self.main_thread_id { + Some(&self.main_thread) + } else { + None + }) + } + + fn next_window_id(&self) -> WindowId { + self.next_window_id.fetch_add(1, Ordering::Relaxed).into() + } + + fn next_webview_id(&self) -> WebviewId { + self.next_webview_id.fetch_add(1, Ordering::Relaxed) + } + + fn next_window_event_id(&self) -> u32 { + self.next_window_event_id.fetch_add(1, Ordering::Relaxed) + } + + fn next_webview_event_id(&self) -> u32 { + self.next_webview_event_id.fetch_add(1, Ordering::Relaxed) + } +} + +impl Context { + fn create_window( + &self, + pending: PendingWindow>, + after_window_creation: Option, + ) -> Result>> { + let label = pending.label.clone(); + let context = self.clone(); + let window_id = self.next_window_id(); + let (webview_id, use_https_scheme) = pending + .webview + .as_ref() + .map(|w| { + ( + Some(context.next_webview_id()), + w.webview_attributes.use_https_scheme, + ) + }) + .unwrap_or((None, false)); + + send_user_message( + self, + Message::CreateWindow( + window_id, + Box::new(move |event_loop| { + create_window( + window_id, + webview_id.unwrap_or_default(), + event_loop, + &context, + pending, + after_window_creation, + ) + }), + ), + )?; + + let dispatcher = WryWindowDispatcher { + window_id, + context: self.clone(), + }; + + let detached_webview = webview_id.map(|id| { + let webview = DetachedWebview { + label: label.clone(), + dispatcher: WryWebviewDispatcher { + window_id: Arc::new(Mutex::new(window_id)), + webview_id: id, + context: self.clone(), + }, + }; + DetachedWindowWebview { + webview, + use_https_scheme, + } + }); + + Ok(DetachedWindow { + id: window_id, + label, + dispatcher, + webview: detached_webview, + }) + } + + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview>, + ) -> Result>> { + let label = pending.label.clone(); + let context = self.clone(); + + let webview_id = self.next_webview_id(); + + let window_id_wrapper = Arc::new(Mutex::new(window_id)); + let window_id_wrapper_ = window_id_wrapper.clone(); + + send_user_message( + self, + Message::CreateWebview( + window_id, + Box::new(move |window, options| { + create_webview( + WebviewKind::WindowChild, + window, + window_id_wrapper_, + webview_id, + &context, + pending, + options.focused_webview, + ) + }), + ), + )?; + + let dispatcher = WryWebviewDispatcher { + window_id: window_id_wrapper, + webview_id, + context: self.clone(), + }; + + Ok(DetachedWebview { label, dispatcher }) + } +} + +#[cfg(feature = "tracing")] +#[derive(Debug, Clone, Default)] +pub struct ActiveTraceSpanStore(Rc>>); + +#[cfg(feature = "tracing")] +impl ActiveTraceSpanStore { + pub fn remove_window_draw(&self) { + self + .0 + .borrow_mut() + .retain(|t| !matches!(t, ActiveTracingSpan::WindowDraw { id: _, span: _ })); + } +} + +#[cfg(feature = "tracing")] +#[derive(Debug)] +pub enum ActiveTracingSpan { + WindowDraw { + id: TaoWindowId, + span: tracing::span::EnteredSpan, + }, +} + +#[derive(Debug)] +pub struct WindowsStore(pub RefCell>); + +// SAFETY: we ensure this type is only used on the main thread. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for WindowsStore {} + +// SAFETY: we ensure this type is only used on the main thread. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Sync for WindowsStore {} + +#[derive(Debug, Clone)] +pub struct DispatcherMainThreadContext { + pub window_target: EventLoopWindowTarget>, + pub web_context: WebContextStore, + // changing this to an Rc will cause frequent app crashes. + pub windows: Arc, + #[cfg(feature = "tracing")] + pub active_tracing_spans: ActiveTraceSpanStore, +} + +// SAFETY: we ensure this type is only used on the main thread. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for DispatcherMainThreadContext {} + +// SAFETY: we ensure this type is only used on the main thread. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Sync for DispatcherMainThreadContext {} + +impl fmt::Debug for Context { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Context") + .field("main_thread_id", &self.main_thread_id) + .field("proxy", &self.proxy) + .field("main_thread", &self.main_thread) + .finish() + } +} + +pub struct DeviceEventFilterWrapper(pub TaoDeviceEventFilter); + +impl From for DeviceEventFilterWrapper { + fn from(item: DeviceEventFilter) -> Self { + match item { + DeviceEventFilter::Always => Self(TaoDeviceEventFilter::Always), + DeviceEventFilter::Never => Self(TaoDeviceEventFilter::Never), + DeviceEventFilter::Unfocused => Self(TaoDeviceEventFilter::Unfocused), + } + } +} + +pub struct RectWrapper(pub wry::Rect); +impl From for RectWrapper { + fn from(value: tauri_runtime::dpi::Rect) -> Self { + RectWrapper(wry::Rect { + position: value.position, + size: value.size, + }) + } +} + +/// Wrapper around a [`tao::window::Icon`] that can be created from an [`Icon`]. +pub struct TaoIcon(pub TaoWindowIcon); + +impl TryFrom> for TaoIcon { + type Error = Error; + fn try_from(icon: Icon<'_>) -> std::result::Result { + TaoWindowIcon::from_rgba(icon.rgba.to_vec(), icon.width, icon.height) + .map(Self) + .map_err(|e| Error::InvalidIcon(Box::new(e))) + } +} + +pub struct WindowEventWrapper(pub Option); + +impl WindowEventWrapper { + fn map_from_tao( + event: &TaoWindowEvent<'_>, + #[allow(unused_variables)] window: &WindowWrapper, + ) -> Self { + let event = match event { + TaoWindowEvent::Resized(size) => WindowEvent::Resized(PhysicalSizeWrapper(*size).into()), + TaoWindowEvent::Moved(position) => { + WindowEvent::Moved(PhysicalPositionWrapper(*position).into()) + } + TaoWindowEvent::Destroyed => WindowEvent::Destroyed, + TaoWindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => WindowEvent::ScaleFactorChanged { + scale_factor: *scale_factor, + new_inner_size: PhysicalSizeWrapper(**new_inner_size).into(), + }, + TaoWindowEvent::Focused(focused) => { + #[cfg(not(windows))] + return Self(Some(WindowEvent::Focused(*focused))); + // on multiwebview mode, if there's no focused webview, it means we're receiving a direct window focus change + // (without receiving a webview focus, such as when clicking the taskbar app icon or using Alt + Tab) + // in this case we must send the focus change event here + #[cfg(windows)] + #[allow(clippy::collapsible_match)] + if window.has_children.load(Ordering::Relaxed) { + const FOCUSED_WEBVIEW_MARKER: &str = "__tauriWindow?"; + let mut focused_webview = window.focused_webview.lock().unwrap(); + // when we focus a webview and the window was previously focused, we get a blur event here + // so on blur we should only send events if the current focus is owned by the window + if !*focused + && focused_webview + .as_deref() + .is_some_and(|w| w != FOCUSED_WEBVIEW_MARKER) + { + return Self(None); + } + + // reset focused_webview on blur, or set to a dummy value on focus + // (to prevent double focus event when we click a webview after focusing a window) + *focused_webview = if *focused { + Some(FOCUSED_WEBVIEW_MARKER.to_owned()) + } else { + None + }; + + WindowEvent::Focused(*focused) + } else { + // when not on multiwebview mode, we handle focus change events on the webview (add_GotFocus and add_LostFocus) + return Self(None); + } + } + TaoWindowEvent::ThemeChanged(theme) => WindowEvent::ThemeChanged(map_theme(theme)), + _ => return Self(None), + }; + Self(Some(event)) + } + + fn parse(window: &WindowWrapper, event: &TaoWindowEvent<'_>) -> Self { + match event { + // resized event from tao doesn't include a reliable size on macOS + // because wry replaces the NSView + TaoWindowEvent::Resized(_) => { + if let Some(w) = &window.inner { + let size = inner_size( + w, + &window.webviews, + window.has_children.load(Ordering::Relaxed), + ); + Self(Some(WindowEvent::Resized(PhysicalSizeWrapper(size).into()))) + } else { + Self(None) + } + } + e => Self::map_from_tao(e, window), + } + } +} + +pub fn map_theme(theme: &TaoTheme) -> Theme { + match theme { + TaoTheme::Light => Theme::Light, + TaoTheme::Dark => Theme::Dark, + _ => Theme::Light, + } +} + +#[cfg(target_os = "macos")] +fn tao_activation_policy(activation_policy: ActivationPolicy) -> TaoActivationPolicy { + match activation_policy { + ActivationPolicy::Regular => TaoActivationPolicy::Regular, + ActivationPolicy::Accessory => TaoActivationPolicy::Accessory, + ActivationPolicy::Prohibited => TaoActivationPolicy::Prohibited, + _ => unimplemented!(), + } +} + +pub struct MonitorHandleWrapper(pub MonitorHandle); + +impl From for Monitor { + fn from(monitor: MonitorHandleWrapper) -> Monitor { + Self { + name: monitor.0.name(), + position: PhysicalPositionWrapper(monitor.0.position()).into(), + size: PhysicalSizeWrapper(monitor.0.size()).into(), + work_area: monitor.0.work_area(), + scale_factor: monitor.0.scale_factor(), + } + } +} + +pub struct PhysicalPositionWrapper(pub TaoPhysicalPosition); + +impl From> for PhysicalPosition { + fn from(position: PhysicalPositionWrapper) -> Self { + Self { + x: position.0.x, + y: position.0.y, + } + } +} + +impl From> for PhysicalPositionWrapper { + fn from(position: PhysicalPosition) -> Self { + Self(TaoPhysicalPosition { + x: position.x, + y: position.y, + }) + } +} + +struct LogicalPositionWrapper(TaoLogicalPosition); + +impl From> for LogicalPositionWrapper { + fn from(position: LogicalPosition) -> Self { + Self(TaoLogicalPosition { + x: position.x, + y: position.y, + }) + } +} + +pub struct PhysicalSizeWrapper(pub TaoPhysicalSize); + +impl From> for PhysicalSize { + fn from(size: PhysicalSizeWrapper) -> Self { + Self { + width: size.0.width, + height: size.0.height, + } + } +} + +impl From> for PhysicalSizeWrapper { + fn from(size: PhysicalSize) -> Self { + Self(TaoPhysicalSize { + width: size.width, + height: size.height, + }) + } +} + +struct LogicalSizeWrapper(TaoLogicalSize); + +impl From> for LogicalSizeWrapper { + fn from(size: LogicalSize) -> Self { + Self(TaoLogicalSize { + width: size.width, + height: size.height, + }) + } +} + +pub struct SizeWrapper(pub TaoSize); + +impl From for SizeWrapper { + fn from(size: Size) -> Self { + match size { + Size::Logical(s) => Self(TaoSize::Logical(LogicalSizeWrapper::from(s).0)), + Size::Physical(s) => Self(TaoSize::Physical(PhysicalSizeWrapper::from(s).0)), + } + } +} + +pub struct PositionWrapper(pub TaoPosition); + +impl From for PositionWrapper { + fn from(position: Position) -> Self { + match position { + Position::Logical(s) => Self(TaoPosition::Logical(LogicalPositionWrapper::from(s).0)), + Position::Physical(s) => Self(TaoPosition::Physical(PhysicalPositionWrapper::from(s).0)), + } + } +} + +#[cfg(desktop)] +fn find_monitor_for_position( + monitors: impl Iterator, + window_position: TaoPosition, +) -> Option { + monitors.into_iter().find(|m| { + let monitor_pos = m.position(); + let monitor_size = m.size(); + + // type annotations required for 32bit targets. + let window_position = window_position.to_physical::(m.scale_factor()); + + monitor_pos.x <= window_position.x + && window_position.x < monitor_pos.x + monitor_size.width as i32 + && monitor_pos.y <= window_position.y + && window_position.y < monitor_pos.y + monitor_size.height as i32 + }) +} + +#[derive(Debug, Clone)] +pub struct UserAttentionTypeWrapper(pub TaoUserAttentionType); + +impl From for UserAttentionTypeWrapper { + fn from(request_type: UserAttentionType) -> Self { + let o = match request_type { + UserAttentionType::Critical => TaoUserAttentionType::Critical, + UserAttentionType::Informational => TaoUserAttentionType::Informational, + }; + Self(o) + } +} + +#[derive(Debug)] +pub struct CursorIconWrapper(pub TaoCursorIcon); + +impl From for CursorIconWrapper { + fn from(icon: CursorIcon) -> Self { + use CursorIcon::*; + let i = match icon { + Default => TaoCursorIcon::Default, + Crosshair => TaoCursorIcon::Crosshair, + Hand => TaoCursorIcon::Hand, + Arrow => TaoCursorIcon::Arrow, + Move => TaoCursorIcon::Move, + Text => TaoCursorIcon::Text, + Wait => TaoCursorIcon::Wait, + Help => TaoCursorIcon::Help, + Progress => TaoCursorIcon::Progress, + NotAllowed => TaoCursorIcon::NotAllowed, + ContextMenu => TaoCursorIcon::ContextMenu, + Cell => TaoCursorIcon::Cell, + VerticalText => TaoCursorIcon::VerticalText, + Alias => TaoCursorIcon::Alias, + Copy => TaoCursorIcon::Copy, + NoDrop => TaoCursorIcon::NoDrop, + Grab => TaoCursorIcon::Grab, + Grabbing => TaoCursorIcon::Grabbing, + AllScroll => TaoCursorIcon::AllScroll, + ZoomIn => TaoCursorIcon::ZoomIn, + ZoomOut => TaoCursorIcon::ZoomOut, + EResize => TaoCursorIcon::EResize, + NResize => TaoCursorIcon::NResize, + NeResize => TaoCursorIcon::NeResize, + NwResize => TaoCursorIcon::NwResize, + SResize => TaoCursorIcon::SResize, + SeResize => TaoCursorIcon::SeResize, + SwResize => TaoCursorIcon::SwResize, + WResize => TaoCursorIcon::WResize, + EwResize => TaoCursorIcon::EwResize, + NsResize => TaoCursorIcon::NsResize, + NeswResize => TaoCursorIcon::NeswResize, + NwseResize => TaoCursorIcon::NwseResize, + ColResize => TaoCursorIcon::ColResize, + RowResize => TaoCursorIcon::RowResize, + _ => TaoCursorIcon::Default, + }; + Self(i) + } +} + +pub struct ProgressStateWrapper(pub TaoProgressState); + +impl From for ProgressStateWrapper { + fn from(status: ProgressBarStatus) -> Self { + let state = match status { + ProgressBarStatus::None => TaoProgressState::None, + ProgressBarStatus::Normal => TaoProgressState::Normal, + ProgressBarStatus::Indeterminate => TaoProgressState::Indeterminate, + ProgressBarStatus::Paused => TaoProgressState::Paused, + ProgressBarStatus::Error => TaoProgressState::Error, + }; + Self(state) + } +} + +pub struct ProgressBarStateWrapper(pub TaoProgressBarState); + +impl From for ProgressBarStateWrapper { + fn from(progress_state: ProgressBarState) -> Self { + Self(TaoProgressBarState { + progress: progress_state.progress, + state: progress_state + .status + .map(|state| ProgressStateWrapper::from(state).0), + desktop_filename: progress_state.desktop_filename, + }) + } +} + +#[derive(Clone, Default)] +pub struct WindowBuilderWrapper { + inner: TaoWindowBuilder, + center: bool, + prevent_overflow: Option, + #[cfg(target_os = "macos")] + tabbing_identifier: Option, +} + +impl std::fmt::Debug for WindowBuilderWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut s = f.debug_struct("WindowBuilderWrapper"); + s.field("inner", &self.inner) + .field("center", &self.center) + .field("prevent_overflow", &self.prevent_overflow); + #[cfg(target_os = "macos")] + { + s.field("tabbing_identifier", &self.tabbing_identifier); + } + s.finish() + } +} + +// SAFETY: this type is `Send` since `menu_items` are read only here +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for WindowBuilderWrapper {} + +impl WindowBuilderBase for WindowBuilderWrapper {} +impl WindowBuilder for WindowBuilderWrapper { + fn new() -> Self { + #[allow(unused_mut)] + let mut builder = Self::default().focused(true); + + #[cfg(target_os = "macos")] + { + // TODO: find a proper way to prevent webview being pushed out of the window. + // Workaround for issue: https://github.com/tauri-apps/tauri/issues/10225 + // The window requires `NSFullSizeContentViewWindowMask` flag to prevent devtools + // pushing the content view out of the window. + // By setting the default style to `TitleBarStyle::Visible` should fix the issue for most of the users. + builder = builder.title_bar_style(TitleBarStyle::Visible); + } + + builder = builder.title("Tauri App"); + + #[cfg(windows)] + { + builder = builder.window_classname("Tauri Window"); + } + + builder + } + + fn with_config(config: &WindowConfig) -> Self { + let mut window = WindowBuilderWrapper::new(); + + #[cfg(target_os = "macos")] + { + window = window + .hidden_title(config.hidden_title) + .title_bar_style(config.title_bar_style); + if let Some(identifier) = &config.tabbing_identifier { + window = window.tabbing_identifier(identifier); + } + if let Some(position) = &config.traffic_light_position { + window = window.traffic_light_position(tauri_runtime::dpi::LogicalPosition::new( + position.x, position.y, + )); + } + } + + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + { + window = window.transparent(config.transparent); + } + #[cfg(all( + target_os = "macos", + not(feature = "macos-private-api"), + debug_assertions + ))] + if config.transparent { + eprintln!( + "The window is set to be transparent but the `macos-private-api` is not enabled. + This can be enabled via the `tauri.macOSPrivateApi` configuration property + "); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + // Mouse event is disabled on Linux since sudden event bursts could block event loop. + window.inner = window.inner.with_cursor_moved_event(false); + } + + #[cfg(target_os = "android")] + { + if let Some(activity_name) = &config.activity_name { + window.inner = window.inner.with_activity_name(activity_name.clone()); + } + if let Some(activity_name) = &config.created_by_activity_name { + window.inner = window + .inner + .with_created_by_activity_name(activity_name.clone()); + } + } + + #[cfg(target_os = "ios")] + { + if let Some(scene_identifier) = &config.requested_by_scene_identifier { + window.inner = window + .inner + .with_requesting_scene_identifier(scene_identifier.clone()); + } + } + + // ignore size from config for mobile for backward compatibility + #[cfg(not(any(target_os = "ios", target_os = "android")))] + { + window = window.inner_size(config.width, config.height); + } + + window = window + .title(config.title.to_string()) + .focused(config.focus) + .focusable(config.focusable) + .visible(config.visible) + .resizable(config.resizable) + .fullscreen(config.fullscreen) + .decorations(config.decorations) + .maximized(config.maximized) + .always_on_bottom(config.always_on_bottom) + .always_on_top(config.always_on_top) + .visible_on_all_workspaces(config.visible_on_all_workspaces) + .content_protected(config.content_protected) + .skip_taskbar(config.skip_taskbar) + .theme(config.theme) + .closable(config.closable) + .maximizable(config.maximizable) + .minimizable(config.minimizable) + .shadow(config.shadow); + + let mut constraints = WindowSizeConstraints::default(); + + if let Some(min_width) = config.min_width { + constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into()); + } + if let Some(min_height) = config.min_height { + constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into()); + } + if let Some(max_width) = config.max_width { + constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into()); + } + if let Some(max_height) = config.max_height { + constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into()); + } + if let Some(color) = config.background_color { + window = window.background_color(color); + } + window = window.inner_size_constraints(constraints); + + if let (Some(x), Some(y)) = (config.x, config.y) { + window = window.position(x, y); + } + + if config.center { + window = window.center(); + } + + if let Some(window_classname) = &config.window_classname { + window = window.window_classname(window_classname); + } + + if let Some(prevent_overflow) = &config.prevent_overflow { + window = match prevent_overflow { + PreventOverflowConfig::Enable(true) => window.prevent_overflow(), + PreventOverflowConfig::Margin(margin) => window + .prevent_overflow_with_margin(TaoPhysicalSize::new(margin.width, margin.height).into()), + _ => window, + }; + } + + window + } + + fn center(mut self) -> Self { + self.center = true; + self + } + + fn position(mut self, x: f64, y: f64) -> Self { + self.inner = self.inner.with_position(TaoLogicalPosition::new(x, y)); + self + } + + fn inner_size(mut self, width: f64, height: f64) -> Self { + self.inner = self + .inner + .with_inner_size(TaoLogicalSize::new(width, height)); + self + } + + fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { + self.inner = self + .inner + .with_min_inner_size(TaoLogicalSize::new(min_width, min_height)); + self + } + + fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { + self.inner = self + .inner + .with_max_inner_size(TaoLogicalSize::new(max_width, max_height)); + self + } + + fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self { + self.inner.window.inner_size_constraints = tao::window::WindowSizeConstraints { + min_width: constraints.min_width, + min_height: constraints.min_height, + max_width: constraints.max_width, + max_height: constraints.max_height, + }; + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) on creation + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + fn prevent_overflow(mut self) -> Self { + self + .prevent_overflow + .replace(PhysicalSize::new(0, 0).into()); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + fn prevent_overflow_with_margin(mut self, margin: Size) -> Self { + self.prevent_overflow.replace(margin); + self + } + + fn resizable(mut self, resizable: bool) -> Self { + self.inner = self.inner.with_resizable(resizable); + self + } + + fn maximizable(mut self, maximizable: bool) -> Self { + self.inner = self.inner.with_maximizable(maximizable); + self + } + + fn minimizable(mut self, minimizable: bool) -> Self { + self.inner = self.inner.with_minimizable(minimizable); + self + } + + fn closable(mut self, closable: bool) -> Self { + self.inner = self.inner.with_closable(closable); + self + } + + fn title>(mut self, title: S) -> Self { + self.inner = self.inner.with_title(title.into()); + self + } + + fn fullscreen(mut self, fullscreen: bool) -> Self { + self.inner = if fullscreen { + self + .inner + .with_fullscreen(Some(Fullscreen::Borderless(None))) + } else { + self.inner.with_fullscreen(None) + }; + self + } + + fn focused(mut self, focused: bool) -> Self { + self.inner = self.inner.with_focused(focused); + self + } + + fn focusable(mut self, focusable: bool) -> Self { + self.inner = self.inner.with_focusable(focusable); + self + } + + fn maximized(mut self, maximized: bool) -> Self { + self.inner = self.inner.with_maximized(maximized); + self + } + + fn visible(mut self, visible: bool) -> Self { + self.inner = self.inner.with_visible(visible); + self + } + + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + fn transparent(mut self, transparent: bool) -> Self { + self.inner = self.inner.with_transparent(transparent); + self + } + + fn decorations(mut self, decorations: bool) -> Self { + self.inner = self.inner.with_decorations(decorations); + self + } + + fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { + self.inner = self.inner.with_always_on_bottom(always_on_bottom); + self + } + + fn always_on_top(mut self, always_on_top: bool) -> Self { + self.inner = self.inner.with_always_on_top(always_on_top); + self + } + + fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { + self.inner = self + .inner + .with_visible_on_all_workspaces(visible_on_all_workspaces); + self + } + + fn content_protected(mut self, protected: bool) -> Self { + self.inner = self.inner.with_content_protection(protected); + self + } + + fn shadow(#[allow(unused_mut)] mut self, _enable: bool) -> Self { + #[cfg(windows)] + { + self.inner = self.inner.with_undecorated_shadow(_enable); + } + #[cfg(target_os = "macos")] + { + self.inner = self.inner.with_has_shadow(_enable); + } + self + } + + #[cfg(windows)] + fn owner(mut self, owner: HWND) -> Self { + self.inner = self.inner.with_owner_window(owner.0 as _); + self + } + + #[cfg(windows)] + fn parent(mut self, parent: HWND) -> Self { + self.inner = self.inner.with_parent_window(parent.0 as _); + self + } + + #[cfg(target_os = "macos")] + fn parent(mut self, parent: *mut std::ffi::c_void) -> Self { + self.inner = self.inner.with_parent_window(parent); + self + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn transient_for(mut self, parent: &impl gtk::glib::IsA) -> Self { + self.inner = self.inner.with_transient_for(parent); + self + } + + #[cfg(windows)] + fn drag_and_drop(mut self, enabled: bool) -> Self { + self.inner = self.inner.with_drag_and_drop(enabled); + self + } + + #[cfg(target_os = "macos")] + fn title_bar_style(mut self, style: TitleBarStyle) -> Self { + match style { + TitleBarStyle::Visible => { + self.inner = self.inner.with_titlebar_transparent(false); + // Fixes rendering issue when resizing window with devtools open (https://github.com/tauri-apps/tauri/issues/3914) + self.inner = self.inner.with_fullsize_content_view(true); + } + TitleBarStyle::Transparent => { + self.inner = self.inner.with_titlebar_transparent(true); + self.inner = self.inner.with_fullsize_content_view(false); + } + TitleBarStyle::Overlay => { + self.inner = self.inner.with_titlebar_transparent(true); + self.inner = self.inner.with_fullsize_content_view(true); + } + unknown => { + #[cfg(feature = "tracing")] + tracing::warn!("unknown title bar style applied: {unknown}"); + + #[cfg(not(feature = "tracing"))] + eprintln!("unknown title bar style applied: {unknown}"); + } + } + self + } + + #[cfg(target_os = "macos")] + fn traffic_light_position>(mut self, position: P) -> Self { + self.inner = self.inner.with_traffic_light_inset(position.into()); + self + } + + #[cfg(target_os = "macos")] + fn hidden_title(mut self, hidden: bool) -> Self { + self.inner = self.inner.with_title_hidden(hidden); + self + } + + #[cfg(target_os = "macos")] + fn tabbing_identifier(mut self, identifier: &str) -> Self { + self.inner = self.inner.with_tabbing_identifier(identifier); + self.tabbing_identifier.replace(identifier.into()); + self + } + + fn icon(mut self, icon: Icon) -> Result { + self.inner = self + .inner + .with_window_icon(Some(TaoIcon::try_from(icon)?.0)); + Ok(self) + } + + fn background_color(mut self, color: Color) -> Self { + self.inner = self.inner.with_background_color(color.into()); + self + } + + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn skip_taskbar(mut self, skip: bool) -> Self { + self.inner = self.inner.with_skip_taskbar(skip); + self + } + + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] + fn skip_taskbar(self, _skip: bool) -> Self { + self + } + + fn theme(mut self, theme: Option) -> Self { + self.inner = self.inner.with_theme(if let Some(t) = theme { + match t { + Theme::Dark => Some(TaoTheme::Dark), + _ => Some(TaoTheme::Light), + } + } else { + None + }); + + self + } + + fn has_icon(&self) -> bool { + self.inner.window.window_icon.is_some() + } + + fn get_theme(&self) -> Option { + self.inner.window.preferred_theme.map(|theme| match theme { + TaoTheme::Dark => Theme::Dark, + _ => Theme::Light, + }) + } + + #[cfg(windows)] + fn window_classname>(mut self, window_classname: S) -> Self { + self.inner = self.inner.with_window_classname(window_classname); + self + } + #[cfg(not(windows))] + fn window_classname>(self, _window_classname: S) -> Self { + self + } + + #[cfg(target_os = "android")] + fn activity_name>(mut self, class_name: S) -> Self { + self.inner = self.inner.with_activity_name(class_name.into()); + self + } + + #[cfg(target_os = "android")] + fn created_by_activity_name>(mut self, class_name: S) -> Self { + self.inner = self.inner.with_created_by_activity_name(class_name.into()); + self + } + + #[cfg(target_os = "ios")] + fn requested_by_scene_identifier>(mut self, identifier: S) -> Self { + self.inner = self + .inner + .with_requesting_scene_identifier(identifier.into()); + self + } +} + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +pub struct GtkWindow(pub gtk::ApplicationWindow); +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for GtkWindow {} + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +pub struct GtkBox(pub gtk::Box); +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for GtkBox {} + +pub struct SendRawWindowHandle(pub raw_window_handle::RawWindowHandle); +unsafe impl Send for SendRawWindowHandle {} + +pub enum ApplicationMessage { + #[cfg(target_os = "macos")] + Show, + #[cfg(target_os = "macos")] + Hide, + #[cfg(any(target_os = "macos", target_os = "ios"))] + FetchDataStoreIdentifiers(Box) + Send + 'static>), + #[cfg(any(target_os = "macos", target_os = "ios"))] + RemoveDataStore([u8; 16], Box) + Send + 'static>), +} + +pub enum WindowMessage { + AddEventListener(WindowEventId, Box), + // Getters + ScaleFactor(Sender), + InnerPosition(Sender>>), + OuterPosition(Sender>>), + InnerSize(Sender>), + OuterSize(Sender>), + IsFullscreen(Sender), + IsMinimized(Sender), + IsMaximized(Sender), + IsFocused(Sender), + IsDecorated(Sender), + IsResizable(Sender), + IsMaximizable(Sender), + IsMinimizable(Sender), + IsClosable(Sender), + IsVisible(Sender), + Title(Sender), + CurrentMonitor(Sender>), + PrimaryMonitor(Sender>), + MonitorFromPoint(Sender>, (f64, f64)), + AvailableMonitors(Sender>), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + GtkWindow(Sender), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + GtkBox(Sender), + #[cfg(target_os = "android")] + ActivityName(Sender), + #[cfg(target_os = "ios")] + SceneIdentifier(Sender), + RawWindowHandle(Sender>), + Theme(Sender), + IsEnabled(Sender), + IsAlwaysOnTop(Sender), + // Setters + Center, + RequestUserAttention(Option), + SetEnabled(bool), + SetResizable(bool), + SetMaximizable(bool), + SetMinimizable(bool), + SetClosable(bool), + SetTitle(String), + Maximize, + Unmaximize, + Minimize, + Unminimize, + Show, + Hide, + Close, + Destroy, + SetDecorations(bool), + SetShadow(bool), + SetAlwaysOnBottom(bool), + SetAlwaysOnTop(bool), + SetVisibleOnAllWorkspaces(bool), + SetContentProtected(bool), + SetSize(Size), + SetMinSize(Option), + SetMaxSize(Option), + SetSizeConstraints(WindowSizeConstraints), + SetPosition(Position), + SetFullscreen(bool), + #[cfg(target_os = "macos")] + SetSimpleFullscreen(bool), + SetFocus, + SetFocusable(bool), + SetIcon(TaoWindowIcon), + SetSkipTaskbar(bool), + SetCursorGrab(bool), + SetCursorVisible(bool), + SetCursorIcon(CursorIcon), + SetCursorPosition(Position), + SetIgnoreCursorEvents(bool), + SetBadgeCount(Option, Option), + SetBadgeLabel(Option), + SetOverlayIcon(Option), + SetProgressBar(ProgressBarState), + SetTitleBarStyle(tauri_utils::TitleBarStyle), + SetTrafficLightPosition(Position), + SetTheme(Option), + SetBackgroundColor(Option), + DragWindow, + ResizeDragWindow(tauri_runtime::ResizeDirection), + RequestRedraw, +} + +#[derive(Debug, Clone)] +pub enum SynthesizedWindowEvent { + Focused(bool), + DragDrop(DragDropEvent), +} + +impl From for WindowEventWrapper { + fn from(event: SynthesizedWindowEvent) -> Self { + let event = match event { + SynthesizedWindowEvent::Focused(focused) => WindowEvent::Focused(focused), + SynthesizedWindowEvent::DragDrop(event) => WindowEvent::DragDrop(event), + }; + Self(Some(event)) + } +} + +pub enum WebviewMessage { + AddEventListener(WebviewEventId, Box), + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + EvaluateScript(String), + #[cfg(all(feature = "tracing", not(target_os = "android")))] + EvaluateScript(String, Sender<()>, tracing::Span), + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + EvaluateScriptWithCallback(String, Box), + #[cfg(all(feature = "tracing", not(target_os = "android")))] + EvaluateScriptWithCallback( + String, + Box, + Sender<()>, + tracing::Span, + ), + CookiesForUrl(Url, Sender>>>), + Cookies(Sender>>>), + SetCookie(tauri_runtime::Cookie<'static>), + DeleteCookie(tauri_runtime::Cookie<'static>), + WebviewEvent(WebviewEvent), + SynthesizedWindowEvent(SynthesizedWindowEvent), + Navigate(Url), + Reload, + Print, + Close, + Show, + Hide, + SetPosition(Position), + SetSize(Size), + SetBounds(tauri_runtime::dpi::Rect), + SetFocus, + Reparent(WindowId, Sender>), + SetAutoResize(bool), + SetZoom(f64), + SetBackgroundColor(Option), + ClearAllBrowsingData, + // Getters + Url(Sender>), + Bounds(Sender>), + Position(Sender>>), + Size(Sender>>), + WithWebview(Box), + // Devtools + #[cfg(any(debug_assertions, feature = "devtools"))] + OpenDevTools, + #[cfg(any(debug_assertions, feature = "devtools"))] + CloseDevTools, + #[cfg(any(debug_assertions, feature = "devtools"))] + IsDevToolsOpen(Sender), +} + +pub enum EventLoopWindowTargetMessage { + CursorPosition(Sender>>), + SetTheme(Option), + SetDeviceEventFilter(DeviceEventFilter), +} + +pub type CreateWindowClosure = + Box>) -> Result + Send>; + +pub type CreateWebviewClosure = + Box Result + Send>; + +pub struct CreateWebviewOptions { + pub focused_webview: Arc>>, +} + +pub enum Message { + Task(Box), + #[cfg(target_os = "macos")] + SetActivationPolicy(ActivationPolicy), + #[cfg(target_os = "macos")] + SetDockVisibility(bool), + RequestExit(i32), + Application(ApplicationMessage), + Window(WindowId, WindowMessage), + Webview(WindowId, WebviewId, WebviewMessage), + EventLoopWindowTarget(EventLoopWindowTargetMessage), + CreateWebview(WindowId, CreateWebviewClosure), + CreateWindow(WindowId, CreateWindowClosure), + CreateRawWindow( + WindowId, + Box (String, TaoWindowBuilder) + Send>, + Sender>>, + ), + UserEvent(T), +} + +impl Clone for Message { + fn clone(&self) -> Self { + match self { + Self::UserEvent(t) => Self::UserEvent(t.clone()), + _ => unimplemented!(), + } + } +} + +/// The Tauri [`WebviewDispatch`] for [`Wry`]. +#[derive(Debug, Clone)] +pub struct WryWebviewDispatcher { + window_id: Arc>, + webview_id: WebviewId, + context: Context, +} + +impl WebviewDispatch for WryWebviewDispatcher { + type Runtime = Wry; + + fn run_on_main_thread(&self, f: F) -> Result<()> { + send_user_message(&self.context, Message::Task(Box::new(f))) + } + + fn on_webview_event(&self, f: F) -> WindowEventId { + let id = self.context.next_webview_event_id(); + let _ = self.context.proxy.send_event(Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::AddEventListener(id, Box::new(f)), + )); + id + } + + fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::WithWebview(Box::new(move |webview| f(Box::new(webview)))), + ), + ) + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn open_devtools(&self) { + let _ = send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::OpenDevTools, + ), + ); + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn close_devtools(&self) { + let _ = send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::CloseDevTools, + ), + ); + } + + /// Gets the devtools window's current open state. + #[cfg(any(debug_assertions, feature = "devtools"))] + fn is_devtools_open(&self) -> Result { + webview_getter!(self, WebviewMessage::IsDevToolsOpen) + } + + // Getters + + fn url(&self) -> Result { + webview_getter!(self, WebviewMessage::Url)? + } + + fn bounds(&self) -> Result { + webview_getter!(self, WebviewMessage::Bounds)? + } + + fn position(&self) -> Result> { + webview_getter!(self, WebviewMessage::Position)? + } + + fn size(&self) -> Result> { + webview_getter!(self, WebviewMessage::Size)? + } + + // Setters + + fn navigate(&self, url: Url) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::Navigate(url), + ), + ) + } + + fn reload(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::Reload, + ), + ) + } + + fn print(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::Print, + ), + ) + } + + fn close(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::Close, + ), + ) + } + + fn set_bounds(&self, bounds: tauri_runtime::dpi::Rect) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::SetBounds(bounds), + ), + ) + } + + fn set_size(&self, size: Size) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::SetSize(size), + ), + ) + } + + fn set_position(&self, position: Position) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::SetPosition(position), + ), + ) + } + + fn set_focus(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::SetFocus, + ), + ) + } + + fn reparent(&self, window_id: WindowId) -> Result<()> { + let mut current_window_id = self.window_id.lock().unwrap(); + let (tx, rx) = channel(); + send_user_message( + &self.context, + Message::Webview( + *current_window_id, + self.webview_id, + WebviewMessage::Reparent(window_id, tx), + ), + )?; + + rx.recv().unwrap()?; + + *current_window_id = window_id; + Ok(()) + } + + fn cookies_for_url(&self, url: Url) -> Result>> { + let (tx, rx) = channel(); + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::CookiesForUrl(url, tx), + ), + )?; + + rx.recv().unwrap() + } + + fn cookies(&self) -> Result>> { + webview_getter!(self, WebviewMessage::Cookies)? + } + + fn set_cookie(&self, cookie: Cookie<'_>) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::SetCookie(cookie.into_owned()), + ), + )?; + Ok(()) + } + + fn delete_cookie(&self, cookie: Cookie<'_>) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::DeleteCookie(cookie.into_owned()), + ), + )?; + Ok(()) + } + + fn set_auto_resize(&self, auto_resize: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::SetAutoResize(auto_resize), + ), + ) + } + + #[cfg(all(feature = "tracing", not(target_os = "android")))] + fn eval_script>(&self, script: S) -> Result<()> { + // use a channel so the EvaluateScript task uses the current span as parent + let (tx, rx) = channel(); + getter!( + self, + rx, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::EvaluateScript(script.into(), tx, tracing::Span::current()), + ) + ) + } + + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + fn eval_script>(&self, script: S) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::EvaluateScript(script.into()), + ), + ) + } + + #[cfg(all(feature = "tracing", not(target_os = "android")))] + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + // use a channel so the EvaluateScript task uses the current span as parent + let (tx, rx) = channel(); + getter!( + self, + rx, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::EvaluateScriptWithCallback( + script.into(), + Box::new(callback), + tx, + tracing::Span::current(), + ), + ) + ) + } + + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::EvaluateScriptWithCallback(script.into(), Box::new(callback)), + ), + ) + } + + fn set_zoom(&self, scale_factor: f64) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::SetZoom(scale_factor), + ), + ) + } + + fn clear_all_browsing_data(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::ClearAllBrowsingData, + ), + ) + } + + fn hide(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::Hide, + ), + ) + } + + fn show(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::Show, + ), + ) + } + + fn set_background_color(&self, color: Option) -> Result<()> { + send_user_message( + &self.context, + Message::Webview( + *self.window_id.lock().unwrap(), + self.webview_id, + WebviewMessage::SetBackgroundColor(color), + ), + ) + } +} + +/// The Tauri [`WindowDispatch`] for [`Wry`]. +#[derive(Debug, Clone)] +pub struct WryWindowDispatcher { + window_id: WindowId, + context: Context, +} + +// SAFETY: this is safe since the `Context` usage is guarded on `send_user_message`. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Sync for WryWindowDispatcher {} + +fn get_raw_window_handle( + dispatcher: &WryWindowDispatcher, +) -> Result> { + window_getter!(dispatcher, WindowMessage::RawWindowHandle) +} + +impl WindowDispatch for WryWindowDispatcher { + type Runtime = Wry; + type WindowBuilder = WindowBuilderWrapper; + + fn run_on_main_thread(&self, f: F) -> Result<()> { + send_user_message(&self.context, Message::Task(Box::new(f))) + } + + fn on_window_event(&self, f: F) -> WindowEventId { + let id = self.context.next_window_event_id(); + let _ = self.context.proxy.send_event(Message::Window( + self.window_id, + WindowMessage::AddEventListener(id, Box::new(f)), + )); + id + } + + // Getters + + fn scale_factor(&self) -> Result { + window_getter!(self, WindowMessage::ScaleFactor) + } + + fn inner_position(&self) -> Result> { + window_getter!(self, WindowMessage::InnerPosition)? + } + + fn outer_position(&self) -> Result> { + window_getter!(self, WindowMessage::OuterPosition)? + } + + fn inner_size(&self) -> Result> { + window_getter!(self, WindowMessage::InnerSize) + } + + fn outer_size(&self) -> Result> { + window_getter!(self, WindowMessage::OuterSize) + } + + fn is_fullscreen(&self) -> Result { + window_getter!(self, WindowMessage::IsFullscreen) + } + + fn is_minimized(&self) -> Result { + window_getter!(self, WindowMessage::IsMinimized) + } + + fn is_maximized(&self) -> Result { + window_getter!(self, WindowMessage::IsMaximized) + } + + fn is_focused(&self) -> Result { + window_getter!(self, WindowMessage::IsFocused) + } + + /// Gets the window's current decoration state. + fn is_decorated(&self) -> Result { + window_getter!(self, WindowMessage::IsDecorated) + } + + /// Gets the window's current resizable state. + fn is_resizable(&self) -> Result { + window_getter!(self, WindowMessage::IsResizable) + } + + /// Gets the current native window's maximize button state + fn is_maximizable(&self) -> Result { + window_getter!(self, WindowMessage::IsMaximizable) + } + + /// Gets the current native window's minimize button state + fn is_minimizable(&self) -> Result { + window_getter!(self, WindowMessage::IsMinimizable) + } + + /// Gets the current native window's close button state + fn is_closable(&self) -> Result { + window_getter!(self, WindowMessage::IsClosable) + } + + fn is_visible(&self) -> Result { + window_getter!(self, WindowMessage::IsVisible) + } + + fn title(&self) -> Result { + window_getter!(self, WindowMessage::Title) + } + + fn current_monitor(&self) -> Result> { + Ok(window_getter!(self, WindowMessage::CurrentMonitor)?.map(|m| MonitorHandleWrapper(m).into())) + } + + fn primary_monitor(&self) -> Result> { + Ok(window_getter!(self, WindowMessage::PrimaryMonitor)?.map(|m| MonitorHandleWrapper(m).into())) + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Result> { + let (tx, rx) = channel(); + + let _ = send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::MonitorFromPoint(tx, (x, y))), + ); + + Ok( + rx.recv() + .map_err(|_| crate::Error::FailedToReceiveMessage)? + .map(|m| MonitorHandleWrapper(m).into()), + ) + } + + fn available_monitors(&self) -> Result> { + Ok( + window_getter!(self, WindowMessage::AvailableMonitors)? + .into_iter() + .map(|m| MonitorHandleWrapper(m).into()) + .collect(), + ) + } + + fn theme(&self) -> Result { + window_getter!(self, WindowMessage::Theme) + } + + fn is_enabled(&self) -> Result { + window_getter!(self, WindowMessage::IsEnabled) + } + + fn is_always_on_top(&self) -> Result { + window_getter!(self, WindowMessage::IsAlwaysOnTop) + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn gtk_window(&self) -> Result { + window_getter!(self, WindowMessage::GtkWindow).map(|w| w.0) + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn default_vbox(&self) -> Result { + window_getter!(self, WindowMessage::GtkBox).map(|w| w.0) + } + + /// Returns the name of the Android activity associated with this window. + #[cfg(target_os = "android")] + fn activity_name(&self) -> Result { + window_getter!(self, WindowMessage::ActivityName) + } + + /// Returns the identifier of the UIScene tied to this UIWindow. + #[cfg(target_os = "ios")] + fn scene_identifier(&self) -> Result { + window_getter!(self, WindowMessage::SceneIdentifier) + } + + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + get_raw_window_handle(self) + .map_err(|_| raw_window_handle::HandleError::Unavailable) + .and_then(|r| r.map(|h| unsafe { raw_window_handle::WindowHandle::borrow_raw(h.0) })) + } + + // Setters + + fn center(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::Center), + ) + } + + fn request_user_attention(&self, request_type: Option) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::RequestUserAttention(request_type.map(Into::into)), + ), + ) + } + + // Creates a window by dispatching a message to the event loop. + // Note that this must be called from a separate thread, otherwise the channel will introduce a deadlock. + fn create_window( + &mut self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + self.context.create_window(pending, after_window_creation) + } + + // Creates a webview by dispatching a message to the event loop. + // Note that this must be called from a separate thread, otherwise the channel will introduce a deadlock. + fn create_webview( + &mut self, + pending: PendingWebview, + ) -> Result> { + self.context.create_webview(self.window_id, pending) + } + + fn set_resizable(&self, resizable: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetResizable(resizable)), + ) + } + + fn set_enabled(&self, enabled: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetEnabled(enabled)), + ) + } + + fn set_maximizable(&self, maximizable: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetMaximizable(maximizable)), + ) + } + + fn set_minimizable(&self, minimizable: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetMinimizable(minimizable)), + ) + } + + fn set_closable(&self, closable: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetClosable(closable)), + ) + } + + fn set_title>(&self, title: S) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetTitle(title.into())), + ) + } + + fn maximize(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::Maximize), + ) + } + + fn unmaximize(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::Unmaximize), + ) + } + + fn minimize(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::Minimize), + ) + } + + fn unminimize(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::Unminimize), + ) + } + + fn show(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::Show), + ) + } + + fn hide(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::Hide), + ) + } + + fn close(&self) -> Result<()> { + // NOTE: close cannot use the `send_user_message` function because it accesses the event loop callback + self + .context + .proxy + .send_event(Message::Window(self.window_id, WindowMessage::Close)) + .map_err(|_| Error::FailedToSendMessage) + } + + fn destroy(&self) -> Result<()> { + // NOTE: destroy cannot use the `send_user_message` function because it accesses the event loop callback + self + .context + .proxy + .send_event(Message::Window(self.window_id, WindowMessage::Destroy)) + .map_err(|_| Error::FailedToSendMessage) + } + + fn set_decorations(&self, decorations: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetDecorations(decorations)), + ) + } + + fn set_shadow(&self, enable: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetShadow(enable)), + ) + } + + fn set_always_on_bottom(&self, always_on_bottom: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetAlwaysOnBottom(always_on_bottom), + ), + ) + } + + fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetAlwaysOnTop(always_on_top)), + ) + } + + fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetVisibleOnAllWorkspaces(visible_on_all_workspaces), + ), + ) + } + + fn set_content_protected(&self, protected: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetContentProtected(protected), + ), + ) + } + + fn set_size(&self, size: Size) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetSize(size)), + ) + } + + fn set_min_size(&self, size: Option) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetMinSize(size)), + ) + } + + fn set_max_size(&self, size: Option) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetMaxSize(size)), + ) + } + + fn set_size_constraints(&self, constraints: WindowSizeConstraints) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetSizeConstraints(constraints), + ), + ) + } + + fn set_position(&self, position: Position) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetPosition(position)), + ) + } + + fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetFullscreen(fullscreen)), + ) + } + + #[cfg(target_os = "macos")] + fn set_simple_fullscreen(&self, enable: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetSimpleFullscreen(enable)), + ) + } + + fn set_focus(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetFocus), + ) + } + + fn set_focusable(&self, focusable: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetFocusable(focusable)), + ) + } + + fn set_icon(&self, icon: Icon) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetIcon(TaoIcon::try_from(icon)?.0), + ), + ) + } + + fn set_skip_taskbar(&self, skip: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetSkipTaskbar(skip)), + ) + } + + fn set_cursor_grab(&self, grab: bool) -> crate::Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetCursorGrab(grab)), + ) + } + + fn set_cursor_visible(&self, visible: bool) -> crate::Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetCursorVisible(visible)), + ) + } + + fn set_cursor_icon(&self, icon: CursorIcon) -> crate::Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetCursorIcon(icon)), + ) + } + + fn set_cursor_position>(&self, position: Pos) -> crate::Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetCursorPosition(position.into()), + ), + ) + } + + fn set_ignore_cursor_events(&self, ignore: bool) -> crate::Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetIgnoreCursorEvents(ignore)), + ) + } + + fn start_dragging(&self) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::DragWindow), + ) + } + + fn start_resize_dragging(&self, direction: tauri_runtime::ResizeDirection) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::ResizeDragWindow(direction)), + ) + } + + fn set_badge_count(&self, count: Option, desktop_filename: Option) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetBadgeCount(count, desktop_filename), + ), + ) + } + + fn set_badge_label(&self, label: Option) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetBadgeLabel(label)), + ) + } + + fn set_overlay_icon(&self, icon: Option) -> Result<()> { + let icon: Result> = icon.map_or(Ok(None), |x| Ok(Some(TaoIcon::try_from(x)?))); + + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetOverlayIcon(icon?)), + ) + } + + fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetProgressBar(progress_state), + ), + ) + } + + fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetTitleBarStyle(style)), + ) + } + + fn set_traffic_light_position(&self, position: Position) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetTrafficLightPosition(position), + ), + ) + } + + fn set_theme(&self, theme: Option) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetTheme(theme)), + ) + } + + fn set_background_color(&self, color: Option) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetBackgroundColor(color)), + ) + } +} + +#[derive(Clone)] +pub struct WebviewWrapper { + label: String, + id: WebviewId, + inner: Rc, + context_store: WebContextStore, + webview_event_listeners: WebviewEventListeners, + // the key of the WebContext if it's not shared + context_key: Option, + bounds: Arc>>, +} + +impl Deref for WebviewWrapper { + type Target = WebView; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Drop for WebviewWrapper { + fn drop(&mut self) { + if Rc::get_mut(&mut self.inner).is_some() { + let mut context_store = self.context_store.lock().unwrap(); + + if let Some(web_context) = context_store.get_mut(&self.context_key) { + web_context.referenced_by_webviews.remove(&self.label); + + // https://github.com/tauri-apps/tauri/issues/14626 + // Because WebKit does not close its network process even when no webviews are running, + // we need to ensure to re-use the existing process on Linux by keeping the WebContext + // alive for the lifetime of the app. + // WebKit on macOS handles this itself. + #[cfg(not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + )))] + if web_context.referenced_by_webviews.is_empty() { + context_store.remove(&self.context_key); + } + } + } + } +} + +pub struct WindowWrapper { + label: String, + inner: Option>, + // whether this window has child webviews + // or it's just a container for a single webview + has_children: AtomicBool, + webviews: Vec, + window_event_listeners: WindowEventListeners, + #[cfg(windows)] + background_color: Option, + #[cfg(windows)] + is_window_transparent: bool, + #[cfg(windows)] + surface: Option, Arc>>, + focused_webview: Arc>>, +} + +impl WindowWrapper { + pub fn label(&self) -> &str { + &self.label + } +} + +impl fmt::Debug for WindowWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WindowWrapper") + .field("label", &self.label) + .field("inner", &self.inner) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct EventProxy(TaoEventLoopProxy>); + +#[cfg(target_os = "ios")] +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Sync for EventProxy {} + +impl EventLoopProxy for EventProxy { + fn send_event(&self, event: T) -> Result<()> { + self + .0 + .send_event(Message::UserEvent(event)) + .map_err(|_| Error::EventLoopClosed) + } +} + +pub trait PluginBuilder { + type Plugin: Plugin; + fn build(self, context: Context) -> Self::Plugin; +} + +pub trait Plugin { + fn on_event( + &mut self, + event: &Event>, + event_loop: &EventLoopWindowTarget>, + proxy: &TaoEventLoopProxy>, + control_flow: &mut ControlFlow, + context: EventLoopIterationContext<'_, T>, + web_context: &WebContextStore, + ) -> bool; +} + +/// A Tauri [`Runtime`] wrapper around wry. +pub struct Wry { + context: Context, + event_loop: EventLoop>, +} + +impl fmt::Debug for Wry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Wry") + .field("main_thread_id", &self.context.main_thread_id) + .field("event_loop", &self.event_loop) + .field("windows", &self.context.main_thread.windows) + .field("web_context", &self.context.main_thread.web_context) + .finish() + } +} + +/// A handle to the Wry runtime. +#[derive(Debug, Clone)] +pub struct WryHandle { + context: Context, +} + +// SAFETY: this is safe since the `Context` usage is guarded on `send_user_message`. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Sync for WryHandle {} + +impl WryHandle { + /// Creates a new tao window using a callback, and returns its window id. + pub fn create_tao_window (String, TaoWindowBuilder) + Send + 'static>( + &self, + f: F, + ) -> Result> { + let id = self.context.next_window_id(); + let (tx, rx) = channel(); + send_user_message(&self.context, Message::CreateRawWindow(id, Box::new(f), tx))?; + rx.recv().unwrap() + } + + /// Gets the [`WindowId`] associated with the given [`TaoWindowId`]. + pub fn window_id(&self, window_id: TaoWindowId) -> WindowId { + self.context.window_id_map.get(&window_id).unwrap() + } + + /// Send a message to the event loop. + pub fn send_event(&self, message: Message) -> Result<()> { + self + .context + .proxy + .send_event(message) + .map_err(|_| Error::FailedToSendMessage)?; + Ok(()) + } + + pub fn plugin + 'static>(&mut self, plugin: P) + where +

>::Plugin: Send, + { + self + .context + .plugins + .lock() + .unwrap() + .push(Box::new(plugin.build(self.context.clone()))); + } +} + +impl RuntimeHandle for WryHandle { + type Runtime = Wry; + + fn create_proxy(&self) -> EventProxy { + EventProxy(self.context.proxy.clone()) + } + + #[cfg(target_os = "macos")] + fn set_activation_policy(&self, activation_policy: ActivationPolicy) -> Result<()> { + send_user_message( + &self.context, + Message::SetActivationPolicy(activation_policy), + ) + } + + #[cfg(target_os = "macos")] + fn set_dock_visibility(&self, visible: bool) -> Result<()> { + send_user_message(&self.context, Message::SetDockVisibility(visible)) + } + + fn request_exit(&self, code: i32) -> Result<()> { + // NOTE: request_exit cannot use the `send_user_message` function because it accesses the event loop callback + self + .context + .proxy + .send_event(Message::RequestExit(code)) + .map_err(|_| Error::FailedToSendMessage) + } + + // Creates a window by dispatching a message to the event loop. + // Note that this must be called from a separate thread, otherwise the channel will introduce a deadlock. + fn create_window( + &self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + self.context.create_window(pending, after_window_creation) + } + + // Creates a webview by dispatching a message to the event loop. + // Note that this must be called from a separate thread, otherwise the channel will introduce a deadlock. + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + self.context.create_webview(window_id, pending) + } + + fn run_on_main_thread(&self, f: F) -> Result<()> { + send_user_message(&self.context, Message::Task(Box::new(f))) + } + + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.context.main_thread.window_target.display_handle() + } + + fn primary_monitor(&self) -> Option { + self + .context + .main_thread + .window_target + .primary_monitor() + .map(|m| MonitorHandleWrapper(m).into()) + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Option { + self + .context + .main_thread + .window_target + .monitor_from_point(x, y) + .map(|m| MonitorHandleWrapper(m).into()) + } + + fn available_monitors(&self) -> Vec { + self + .context + .main_thread + .window_target + .available_monitors() + .map(|m| MonitorHandleWrapper(m).into()) + .collect() + } + + fn cursor_position(&self) -> Result> { + event_loop_window_getter!(self, EventLoopWindowTargetMessage::CursorPosition)? + .map(PhysicalPositionWrapper) + .map(Into::into) + .map_err(|_| Error::FailedToGetCursorPosition) + } + + fn set_theme(&self, theme: Option) { + let _ = send_user_message( + &self.context, + Message::EventLoopWindowTarget(EventLoopWindowTargetMessage::SetTheme(theme)), + ); + } + + #[cfg(target_os = "macos")] + fn show(&self) -> tauri_runtime::Result<()> { + send_user_message( + &self.context, + Message::Application(ApplicationMessage::Show), + ) + } + + #[cfg(target_os = "macos")] + fn hide(&self) -> tauri_runtime::Result<()> { + send_user_message( + &self.context, + Message::Application(ApplicationMessage::Hide), + ) + } + + fn set_device_event_filter(&self, filter: DeviceEventFilter) { + let _ = send_user_message( + &self.context, + Message::EventLoopWindowTarget(EventLoopWindowTargetMessage::SetDeviceEventFilter(filter)), + ); + } + + #[cfg(target_os = "android")] + fn find_class<'a>( + &self, + env: &mut jni::JNIEnv<'a>, + activity: &jni::objects::JObject<'_>, + name: impl Into, + ) -> std::result::Result, jni::errors::Error> { + find_class(env, activity, name.into()) + } + + #[cfg(target_os = "android")] + fn run_on_android_context(&self, f: F) + where + F: FnOnce(&mut jni::JNIEnv, &jni::objects::JObject, &jni::objects::JObject) + Send + 'static, + { + dispatch(f) + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn fetch_data_store_identifiers) + Send + 'static>( + &self, + cb: F, + ) -> Result<()> { + send_user_message( + &self.context, + Message::Application(ApplicationMessage::FetchDataStoreIdentifiers(Box::new(cb))), + ) + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn remove_data_store) + Send + 'static>( + &self, + uuid: [u8; 16], + cb: F, + ) -> Result<()> { + send_user_message( + &self.context, + Message::Application(ApplicationMessage::RemoveDataStore(uuid, Box::new(cb))), + ) + } +} + +impl Wry { + fn init_with_builder( + mut event_loop_builder: EventLoopBuilder>, + #[allow(unused_variables)] args: RuntimeInitArgs, + ) -> Result { + #[cfg(windows)] + if let Some(hook) = args.msg_hook { + use tao::platform::windows::EventLoopBuilderExtWindows; + event_loop_builder.with_msg_hook(hook); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Some(app_id) = args.app_id { + use tao::platform::unix::EventLoopBuilderExtUnix; + event_loop_builder.with_app_id(app_id); + } + Self::init(event_loop_builder.build()) + } + + fn init(event_loop: EventLoop>) -> Result { + let main_thread_id = current_thread().id(); + let web_context = WebContextStore::default(); + + let windows = Arc::new(WindowsStore(RefCell::new(BTreeMap::default()))); + let window_id_map = WindowIdStore::default(); + + let context = Context { + window_id_map, + main_thread_id, + proxy: event_loop.create_proxy(), + main_thread: DispatcherMainThreadContext { + window_target: event_loop.deref().clone(), + web_context, + windows, + #[cfg(feature = "tracing")] + active_tracing_spans: Default::default(), + }, + plugins: Default::default(), + next_window_id: Default::default(), + next_webview_id: Default::default(), + next_window_event_id: Default::default(), + next_webview_event_id: Default::default(), + webview_runtime_installed: wry::webview_version().is_ok(), + }; + + Ok(Self { + context, + event_loop, + }) + } +} + +impl Runtime for Wry { + type WindowDispatcher = WryWindowDispatcher; + type WebviewDispatcher = WryWebviewDispatcher; + type Handle = WryHandle; + + type EventLoopProxy = EventProxy; + + fn new(args: RuntimeInitArgs) -> Result { + Self::init_with_builder(EventLoopBuilder::>::with_user_event(), args) + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn new_any_thread(args: RuntimeInitArgs) -> Result { + use tao::platform::unix::EventLoopBuilderExtUnix; + let mut event_loop_builder = EventLoopBuilder::>::with_user_event(); + event_loop_builder.with_any_thread(true); + Self::init_with_builder(event_loop_builder, args) + } + + #[cfg(windows)] + fn new_any_thread(args: RuntimeInitArgs) -> Result { + use tao::platform::windows::EventLoopBuilderExtWindows; + let mut event_loop_builder = EventLoopBuilder::>::with_user_event(); + event_loop_builder.with_any_thread(true); + Self::init_with_builder(event_loop_builder, args) + } + + fn create_proxy(&self) -> EventProxy { + EventProxy(self.event_loop.create_proxy()) + } + + fn handle(&self) -> Self::Handle { + WryHandle { + context: self.context.clone(), + } + } + + fn create_window( + &self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + let label = pending.label.clone(); + let window_id = self.context.next_window_id(); + let (webview_id, use_https_scheme) = pending + .webview + .as_ref() + .map(|w| { + ( + Some(self.context.next_webview_id()), + w.webview_attributes.use_https_scheme, + ) + }) + .unwrap_or((None, false)); + + let window = create_window( + window_id, + webview_id.unwrap_or_default(), + &self.event_loop, + &self.context, + pending, + after_window_creation, + )?; + + let dispatcher = WryWindowDispatcher { + window_id, + context: self.context.clone(), + }; + + self + .context + .main_thread + .windows + .0 + .borrow_mut() + .insert(window_id, window); + + let detached_webview = webview_id.map(|id| { + let webview = DetachedWebview { + label: label.clone(), + dispatcher: WryWebviewDispatcher { + window_id: Arc::new(Mutex::new(window_id)), + webview_id: id, + context: self.context.clone(), + }, + }; + DetachedWindowWebview { + webview, + use_https_scheme, + } + }); + + Ok(DetachedWindow { + id: window_id, + label, + dispatcher, + webview: detached_webview, + }) + } + + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + let label = pending.label.clone(); + + let window = self + .context + .main_thread + .windows + .0 + .borrow() + .get(&window_id) + .map(|w| (w.inner.clone(), w.focused_webview.clone())); + if let Some((Some(window), focused_webview)) = window { + let window_id_wrapper = Arc::new(Mutex::new(window_id)); + + let webview_id = self.context.next_webview_id(); + + let webview = create_webview( + WebviewKind::WindowChild, + &window, + window_id_wrapper.clone(), + webview_id, + &self.context, + pending, + focused_webview, + )?; + + if let Some(w) = self + .context + .main_thread + .windows + .0 + .borrow_mut() + .get_mut(&window_id) + { + w.webviews.push(webview); + w.has_children.store(true, Ordering::Relaxed); + } + + let dispatcher = WryWebviewDispatcher { + window_id: window_id_wrapper, + webview_id, + context: self.context.clone(), + }; + + Ok(DetachedWebview { label, dispatcher }) + } else { + Err(Error::WindowNotFound) + } + } + + fn primary_monitor(&self) -> Option { + self + .context + .main_thread + .window_target + .primary_monitor() + .map(|m| MonitorHandleWrapper(m).into()) + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Option { + self + .context + .main_thread + .window_target + .monitor_from_point(x, y) + .map(|m| MonitorHandleWrapper(m).into()) + } + + fn available_monitors(&self) -> Vec { + self + .context + .main_thread + .window_target + .available_monitors() + .map(|m| MonitorHandleWrapper(m).into()) + .collect() + } + + fn cursor_position(&self) -> Result> { + self + .context + .main_thread + .window_target + .cursor_position() + .map(PhysicalPositionWrapper) + .map(Into::into) + .map_err(|_| Error::FailedToGetCursorPosition) + } + + fn set_theme(&self, theme: Option) { + self.event_loop.set_theme(to_tao_theme(theme)); + } + + #[cfg(target_os = "macos")] + fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) { + self + .event_loop + .set_activation_policy(tao_activation_policy(activation_policy)); + } + + #[cfg(target_os = "macos")] + fn set_dock_visibility(&mut self, visible: bool) { + self.event_loop.set_dock_visibility(visible); + } + + #[cfg(target_os = "macos")] + fn show(&self) { + self.event_loop.show_application(); + } + + #[cfg(target_os = "macos")] + fn hide(&self) { + self.event_loop.hide_application(); + } + + fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { + self + .event_loop + .set_device_event_filter(DeviceEventFilterWrapper::from(filter).0); + } + + #[cfg(desktop)] + fn run_iteration) + 'static>(&mut self, mut callback: F) { + use tao::platform::run_return::EventLoopExtRunReturn; + let windows = self.context.main_thread.windows.clone(); + let window_id_map = self.context.window_id_map.clone(); + let web_context = &self.context.main_thread.web_context; + let plugins = self.context.plugins.clone(); + + #[cfg(feature = "tracing")] + let active_tracing_spans = self.context.main_thread.active_tracing_spans.clone(); + + let proxy = self.event_loop.create_proxy(); + + self + .event_loop + .run_return(|event, event_loop, control_flow| { + *control_flow = ControlFlow::Wait; + if let Event::MainEventsCleared = &event { + *control_flow = ControlFlow::Exit; + } + + for p in plugins.lock().unwrap().iter_mut() { + let prevent_default = p.on_event( + &event, + event_loop, + &proxy, + control_flow, + EventLoopIterationContext { + callback: &mut callback, + window_id_map: window_id_map.clone(), + windows: windows.clone(), + #[cfg(feature = "tracing")] + active_tracing_spans: active_tracing_spans.clone(), + }, + web_context, + ); + if prevent_default { + return; + } + } + + handle_event_loop( + event, + event_loop, + control_flow, + EventLoopIterationContext { + callback: &mut callback, + windows: windows.clone(), + window_id_map: window_id_map.clone(), + #[cfg(feature = "tracing")] + active_tracing_spans: active_tracing_spans.clone(), + }, + ); + }); + } + + fn run) + 'static>(self, callback: F) { + let event_handler = make_event_handler(&self, callback); + + self.event_loop.run(event_handler) + } + + #[cfg(not(target_os = "ios"))] + fn run_return) + 'static>(mut self, callback: F) -> i32 { + use tao::platform::run_return::EventLoopExtRunReturn; + + let event_handler = make_event_handler(&self, callback); + + self.event_loop.run_return(event_handler) + } + + #[cfg(target_os = "ios")] + fn run_return) + 'static>(self, callback: F) -> i32 { + self.run(callback); + 0 + } +} + +fn make_event_handler( + runtime: &Wry, + mut callback: F, +) -> impl FnMut(Event<'_, Message>, &EventLoopWindowTarget>, &mut ControlFlow) +where + T: UserEvent, + F: FnMut(RunEvent) + 'static, +{ + let windows = runtime.context.main_thread.windows.clone(); + let window_id_map = runtime.context.window_id_map.clone(); + let web_context = runtime.context.main_thread.web_context.clone(); + let plugins = runtime.context.plugins.clone(); + + #[cfg(feature = "tracing")] + let active_tracing_spans = runtime.context.main_thread.active_tracing_spans.clone(); + let proxy = runtime.event_loop.create_proxy(); + + move |event, event_loop, control_flow| { + for p in plugins.lock().unwrap().iter_mut() { + let prevent_default = p.on_event( + &event, + event_loop, + &proxy, + control_flow, + EventLoopIterationContext { + callback: &mut callback, + window_id_map: window_id_map.clone(), + windows: windows.clone(), + #[cfg(feature = "tracing")] + active_tracing_spans: active_tracing_spans.clone(), + }, + &web_context, + ); + if prevent_default { + return; + } + } + handle_event_loop( + event, + event_loop, + control_flow, + EventLoopIterationContext { + callback: &mut callback, + window_id_map: window_id_map.clone(), + windows: windows.clone(), + #[cfg(feature = "tracing")] + active_tracing_spans: active_tracing_spans.clone(), + }, + ); + } +} + +pub struct EventLoopIterationContext<'a, T: UserEvent> { + pub callback: &'a mut (dyn FnMut(RunEvent) + 'static), + pub window_id_map: WindowIdStore, + pub windows: Arc, + #[cfg(feature = "tracing")] + pub active_tracing_spans: ActiveTraceSpanStore, +} + +struct UserMessageContext { + windows: Arc, + window_id_map: WindowIdStore, +} + +fn handle_user_message( + event_loop: &EventLoopWindowTarget>, + message: Message, + context: UserMessageContext, +) { + let UserMessageContext { + window_id_map, + windows, + } = context; + match message { + Message::Task(task) => task(), + #[cfg(target_os = "macos")] + Message::SetActivationPolicy(activation_policy) => { + event_loop.set_activation_policy_at_runtime(tao_activation_policy(activation_policy)) + } + #[cfg(target_os = "macos")] + Message::SetDockVisibility(visible) => event_loop.set_dock_visibility(visible), + Message::RequestExit(_code) => panic!("cannot handle RequestExit on the main thread"), + Message::Application(application_message) => match application_message { + #[cfg(target_os = "macos")] + ApplicationMessage::Show => { + event_loop.show_application(); + } + #[cfg(target_os = "macos")] + ApplicationMessage::Hide => { + event_loop.hide_application(); + } + #[cfg(any(target_os = "macos", target_os = "ios"))] + ApplicationMessage::FetchDataStoreIdentifiers(cb) => { + if let Err(e) = WebView::fetch_data_store_identifiers(cb) { + // this shouldn't ever happen because we're running on the main thread + // but let's be safe and warn here + log::error!("failed to fetch data store identifiers: {e}"); + } + } + #[cfg(any(target_os = "macos", target_os = "ios"))] + ApplicationMessage::RemoveDataStore(uuid, cb) => { + WebView::remove_data_store(&uuid, move |res| { + cb(res.map_err(|_| Error::FailedToRemoveDataStore)) + }) + } + }, + Message::Window(id, window_message) => { + let w = windows.0.borrow().get(&id).map(|w| { + ( + w.inner.clone(), + w.webviews.clone(), + w.has_children.load(Ordering::Relaxed), + w.window_event_listeners.clone(), + ) + }); + if let Some((Some(window), webviews, has_children, window_event_listeners)) = w { + match window_message { + WindowMessage::AddEventListener(id, listener) => { + window_event_listeners.lock().unwrap().insert(id, listener); + } + + // Getters + WindowMessage::ScaleFactor(tx) => tx.send(window.scale_factor()).unwrap(), + WindowMessage::InnerPosition(tx) => tx + .send( + window + .inner_position() + .map(|p| PhysicalPositionWrapper(p).into()) + .map_err(|_| Error::FailedToSendMessage), + ) + .unwrap(), + WindowMessage::OuterPosition(tx) => tx + .send( + window + .outer_position() + .map(|p| PhysicalPositionWrapper(p).into()) + .map_err(|_| Error::FailedToSendMessage), + ) + .unwrap(), + WindowMessage::InnerSize(tx) => tx + .send(PhysicalSizeWrapper(inner_size(&window, &webviews, has_children)).into()) + .unwrap(), + WindowMessage::OuterSize(tx) => tx + .send(PhysicalSizeWrapper(window.outer_size()).into()) + .unwrap(), + WindowMessage::IsFullscreen(tx) => tx.send(window.fullscreen().is_some()).unwrap(), + WindowMessage::IsMinimized(tx) => tx.send(window.is_minimized()).unwrap(), + WindowMessage::IsMaximized(tx) => tx.send(window.is_maximized()).unwrap(), + WindowMessage::IsFocused(tx) => tx.send(window.is_focused()).unwrap(), + WindowMessage::IsDecorated(tx) => tx.send(window.is_decorated()).unwrap(), + WindowMessage::IsResizable(tx) => tx.send(window.is_resizable()).unwrap(), + WindowMessage::IsMaximizable(tx) => tx.send(window.is_maximizable()).unwrap(), + WindowMessage::IsMinimizable(tx) => tx.send(window.is_minimizable()).unwrap(), + WindowMessage::IsClosable(tx) => tx.send(window.is_closable()).unwrap(), + WindowMessage::IsVisible(tx) => tx.send(window.is_visible()).unwrap(), + WindowMessage::Title(tx) => tx.send(window.title()).unwrap(), + WindowMessage::CurrentMonitor(tx) => tx.send(window.current_monitor()).unwrap(), + WindowMessage::PrimaryMonitor(tx) => tx.send(window.primary_monitor()).unwrap(), + WindowMessage::MonitorFromPoint(tx, (x, y)) => { + tx.send(window.monitor_from_point(x, y)).unwrap() + } + WindowMessage::AvailableMonitors(tx) => { + tx.send(window.available_monitors().collect()).unwrap() + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + WindowMessage::GtkWindow(tx) => tx.send(GtkWindow(window.gtk_window().clone())).unwrap(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + WindowMessage::GtkBox(tx) => tx + .send(GtkBox(window.default_vbox().unwrap().clone())) + .unwrap(), + #[cfg(target_os = "android")] + WindowMessage::ActivityName(tx) => { + tx.send(window.activity_name()).unwrap(); + } + #[cfg(target_os = "ios")] + WindowMessage::SceneIdentifier(tx) => { + tx.send(window.scene_identifier()).unwrap(); + } + WindowMessage::RawWindowHandle(tx) => tx + .send( + window + .window_handle() + .map(|h| SendRawWindowHandle(h.as_raw())), + ) + .unwrap(), + WindowMessage::Theme(tx) => { + tx.send(map_theme(&window.theme())).unwrap(); + } + WindowMessage::IsEnabled(tx) => tx.send(window.is_enabled()).unwrap(), + WindowMessage::IsAlwaysOnTop(tx) => tx.send(window.is_always_on_top()).unwrap(), + // Setters + WindowMessage::Center => window.center(), + WindowMessage::RequestUserAttention(request_type) => { + window.request_user_attention(request_type.map(|r| r.0)); + } + WindowMessage::SetResizable(resizable) => { + window.set_resizable(resizable); + #[cfg(windows)] + if !resizable { + undecorated_resizing::detach_resize_handler(window.hwnd()); + } else if !window.is_decorated() { + undecorated_resizing::attach_resize_handler( + window.hwnd(), + window.has_undecorated_shadow(), + ); + } + } + WindowMessage::SetMaximizable(maximizable) => window.set_maximizable(maximizable), + WindowMessage::SetMinimizable(minimizable) => window.set_minimizable(minimizable), + WindowMessage::SetClosable(closable) => window.set_closable(closable), + WindowMessage::SetTitle(title) => window.set_title(&title), + WindowMessage::Maximize => window.set_maximized(true), + WindowMessage::Unmaximize => window.set_maximized(false), + WindowMessage::Minimize => window.set_minimized(true), + WindowMessage::Unminimize => window.set_minimized(false), + WindowMessage::SetEnabled(enabled) => window.set_enabled(enabled), + WindowMessage::Show => window.set_visible(true), + WindowMessage::Hide => window.set_visible(false), + WindowMessage::Close => { + panic!("cannot handle `WindowMessage::Close` on the main thread") + } + WindowMessage::Destroy => { + panic!("cannot handle `WindowMessage::Destroy` on the main thread") + } + WindowMessage::SetDecorations(decorations) => { + window.set_decorations(decorations); + #[cfg(windows)] + if decorations { + undecorated_resizing::detach_resize_handler(window.hwnd()); + } else if window.is_resizable() { + undecorated_resizing::attach_resize_handler( + window.hwnd(), + window.has_undecorated_shadow(), + ); + } + } + WindowMessage::SetShadow(_enable) => { + #[cfg(windows)] + { + window.set_undecorated_shadow(_enable); + undecorated_resizing::update_drag_hwnd_rgn_for_undecorated(window.hwnd(), _enable); + } + #[cfg(target_os = "macos")] + window.set_has_shadow(_enable); + } + WindowMessage::SetAlwaysOnBottom(always_on_bottom) => { + window.set_always_on_bottom(always_on_bottom) + } + WindowMessage::SetAlwaysOnTop(always_on_top) => window.set_always_on_top(always_on_top), + WindowMessage::SetVisibleOnAllWorkspaces(visible_on_all_workspaces) => { + window.set_visible_on_all_workspaces(visible_on_all_workspaces) + } + WindowMessage::SetContentProtected(protected) => window.set_content_protection(protected), + WindowMessage::SetSize(size) => { + window.set_inner_size(SizeWrapper::from(size).0); + } + WindowMessage::SetMinSize(size) => { + window.set_min_inner_size(size.map(|s| SizeWrapper::from(s).0)); + } + WindowMessage::SetMaxSize(size) => { + window.set_max_inner_size(size.map(|s| SizeWrapper::from(s).0)); + } + WindowMessage::SetSizeConstraints(constraints) => { + window.set_inner_size_constraints(tao::window::WindowSizeConstraints { + min_width: constraints.min_width, + min_height: constraints.min_height, + max_width: constraints.max_width, + max_height: constraints.max_height, + }); + } + WindowMessage::SetPosition(position) => { + window.set_outer_position(PositionWrapper::from(position).0) + } + WindowMessage::SetFullscreen(fullscreen) => { + if fullscreen { + window.set_fullscreen(Some(Fullscreen::Borderless(None))) + } else { + window.set_fullscreen(None) + } + } + + #[cfg(target_os = "macos")] + WindowMessage::SetSimpleFullscreen(enable) => { + window.set_simple_fullscreen(enable); + } + + WindowMessage::SetFocus => { + window.set_focus(); + } + WindowMessage::SetFocusable(focusable) => { + window.set_focusable(focusable); + } + WindowMessage::SetIcon(icon) => { + window.set_window_icon(Some(icon)); + } + #[allow(unused_variables)] + WindowMessage::SetSkipTaskbar(skip) => { + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + let _ = window.set_skip_taskbar(skip); + } + WindowMessage::SetCursorGrab(grab) => { + let _ = window.set_cursor_grab(grab); + } + WindowMessage::SetCursorVisible(visible) => { + window.set_cursor_visible(visible); + } + WindowMessage::SetCursorIcon(icon) => { + window.set_cursor_icon(CursorIconWrapper::from(icon).0); + } + WindowMessage::SetCursorPosition(position) => { + let _ = window.set_cursor_position(PositionWrapper::from(position).0); + } + WindowMessage::SetIgnoreCursorEvents(ignore) => { + let _ = window.set_ignore_cursor_events(ignore); + } + WindowMessage::DragWindow => { + let _ = window.drag_window(); + } + WindowMessage::ResizeDragWindow(direction) => { + let _ = window.drag_resize_window(match direction { + tauri_runtime::ResizeDirection::East => tao::window::ResizeDirection::East, + tauri_runtime::ResizeDirection::North => tao::window::ResizeDirection::North, + tauri_runtime::ResizeDirection::NorthEast => tao::window::ResizeDirection::NorthEast, + tauri_runtime::ResizeDirection::NorthWest => tao::window::ResizeDirection::NorthWest, + tauri_runtime::ResizeDirection::South => tao::window::ResizeDirection::South, + tauri_runtime::ResizeDirection::SouthEast => tao::window::ResizeDirection::SouthEast, + tauri_runtime::ResizeDirection::SouthWest => tao::window::ResizeDirection::SouthWest, + tauri_runtime::ResizeDirection::West => tao::window::ResizeDirection::West, + }); + } + WindowMessage::RequestRedraw => { + window.request_redraw(); + } + WindowMessage::SetBadgeCount(_count, _desktop_filename) => { + #[cfg(target_os = "ios")] + window.set_badge_count( + _count.map_or(0, |x| x.clamp(i32::MIN as i64, i32::MAX as i64) as i32), + ); + + #[cfg(target_os = "macos")] + window.set_badge_label(_count.map(|x| x.to_string())); + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + window.set_badge_count(_count, _desktop_filename); + } + WindowMessage::SetBadgeLabel(_label) => { + #[cfg(target_os = "macos")] + window.set_badge_label(_label); + } + WindowMessage::SetOverlayIcon(_icon) => { + #[cfg(windows)] + window.set_overlay_icon(_icon.map(|x| x.0).as_ref()); + } + WindowMessage::SetProgressBar(progress_state) => { + window.set_progress_bar(ProgressBarStateWrapper::from(progress_state).0); + } + WindowMessage::SetTitleBarStyle(_style) => { + #[cfg(target_os = "macos")] + match _style { + TitleBarStyle::Visible => { + window.set_titlebar_transparent(false); + window.set_fullsize_content_view(true); + } + TitleBarStyle::Transparent => { + window.set_titlebar_transparent(true); + window.set_fullsize_content_view(false); + } + TitleBarStyle::Overlay => { + window.set_titlebar_transparent(true); + window.set_fullsize_content_view(true); + } + unknown => { + #[cfg(feature = "tracing")] + tracing::warn!("unknown title bar style applied: {unknown}"); + + #[cfg(not(feature = "tracing"))] + eprintln!("unknown title bar style applied: {unknown}"); + } + }; + } + WindowMessage::SetTrafficLightPosition(_position) => { + #[cfg(target_os = "macos")] + window.set_traffic_light_inset(_position); + } + WindowMessage::SetTheme(theme) => { + window.set_theme(to_tao_theme(theme)); + } + WindowMessage::SetBackgroundColor(color) => { + window.set_background_color(color.map(Into::into)) + } + } + } + } + Message::Webview(window_id, webview_id, webview_message) => { + #[cfg(any( + target_os = "macos", + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let WebviewMessage::Reparent(new_parent_window_id, tx) = webview_message { + let webview_handle = windows.0.borrow_mut().get_mut(&window_id).and_then(|w| { + w.webviews + .iter() + .position(|w| w.id == webview_id) + .map(|webview_index| w.webviews.remove(webview_index)) + }); + + if let Some(webview) = webview_handle { + if let Some((Some(new_parent_window), new_parent_window_webviews)) = windows + .0 + .borrow_mut() + .get_mut(&new_parent_window_id) + .map(|w| (w.inner.clone(), &mut w.webviews)) + { + #[cfg(target_os = "macos")] + let reparent_result = { + use wry::WebViewExtMacOS; + webview.inner.reparent(new_parent_window.ns_window() as _) + }; + #[cfg(windows)] + let reparent_result = { webview.inner.reparent(new_parent_window.hwnd()) }; + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + let reparent_result = { + if let Some(container) = new_parent_window.default_vbox() { + webview.inner.reparent(container) + } else { + Err(wry::Error::MessageSender) + } + }; + + match reparent_result { + Ok(_) => { + new_parent_window_webviews.push(webview); + tx.send(Ok(())).unwrap(); + } + Err(e) => { + log::error!("failed to reparent webview: {e}"); + tx.send(Err(Error::FailedToSendMessage)).unwrap(); + } + } + } + } else { + tx.send(Err(Error::FailedToSendMessage)).unwrap(); + } + + return; + } + + let webview_handle = windows.0.borrow().get(&window_id).map(|w| { + ( + w.inner.clone(), + w.webviews.iter().find(|w| w.id == webview_id).cloned(), + ) + }); + if let Some((Some(window), Some(webview))) = webview_handle { + match webview_message { + WebviewMessage::WebviewEvent(_) => { /* already handled */ } + WebviewMessage::SynthesizedWindowEvent(_) => { /* already handled */ } + WebviewMessage::Reparent(_window_id, _tx) => { /* already handled */ } + WebviewMessage::AddEventListener(id, listener) => { + webview + .webview_event_listeners + .lock() + .unwrap() + .insert(id, listener); + } + + #[cfg(all(feature = "tracing", not(target_os = "android")))] + WebviewMessage::EvaluateScript(script, tx, span) => { + let _span = span.entered(); + if let Err(e) = webview.evaluate_script(&script) { + log::error!("{e}"); + } + tx.send(()).unwrap(); + } + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + WebviewMessage::EvaluateScript(script) => { + if let Err(e) = webview.evaluate_script(&script) { + log::error!("{e}"); + } + } + #[cfg(all(feature = "tracing", not(target_os = "android")))] + WebviewMessage::EvaluateScriptWithCallback(script, callback, tx, span) => { + let _span = span.entered(); + if let Err(e) = webview.evaluate_script_with_callback(&script, callback) { + log::error!("{e}"); + } + tx.send(()).unwrap(); + } + #[cfg(not(all(feature = "tracing", not(target_os = "android"))))] + WebviewMessage::EvaluateScriptWithCallback(script, callback) => { + if let Err(e) = webview.evaluate_script_with_callback(&script, callback) { + log::error!("{e}"); + } + } + WebviewMessage::Navigate(url) => { + if let Err(e) = webview.load_url(url.as_str()) { + log::error!("failed to navigate to url {}: {}", url, e); + } + } + WebviewMessage::Reload => { + if let Err(e) = webview.reload() { + log::error!("failed to reload: {e}"); + } + } + WebviewMessage::Show => { + if let Err(e) = webview.set_visible(true) { + log::error!("failed to change webview visibility: {e}"); + } + } + WebviewMessage::Hide => { + if let Err(e) = webview.set_visible(false) { + log::error!("failed to change webview visibility: {e}"); + } + } + WebviewMessage::Print => { + let _ = webview.print(); + } + WebviewMessage::Close => { + #[allow(unknown_lints, clippy::manual_inspect)] + windows.0.borrow_mut().get_mut(&window_id).map(|window| { + if let Some(i) = window.webviews.iter().position(|w| w.id == webview.id) { + window.webviews.remove(i); + } + window + }); + } + WebviewMessage::SetBounds(bounds) => { + let bounds: RectWrapper = bounds.into(); + let bounds = bounds.0; + + if let Some(b) = &mut *webview.bounds.lock().unwrap() { + let scale_factor = window.scale_factor(); + let size = bounds.size.to_logical::(scale_factor); + let position = bounds.position.to_logical::(scale_factor); + let window_size = window.inner_size().to_logical::(scale_factor); + b.width_rate = size.width / window_size.width; + b.height_rate = size.height / window_size.height; + b.x_rate = position.x / window_size.width; + b.y_rate = position.y / window_size.height; + } + + if let Err(e) = webview.set_bounds(bounds) { + log::error!("failed to set webview size: {e}"); + } + } + WebviewMessage::SetSize(size) => match webview.bounds() { + Ok(mut bounds) => { + bounds.size = size; + + let scale_factor = window.scale_factor(); + let size = size.to_logical::(scale_factor); + + if let Some(b) = &mut *webview.bounds.lock().unwrap() { + let window_size = window.inner_size().to_logical::(scale_factor); + b.width_rate = size.width / window_size.width; + b.height_rate = size.height / window_size.height; + } + + if let Err(e) = webview.set_bounds(bounds) { + log::error!("failed to set webview size: {e}"); + } + } + Err(e) => { + log::error!("failed to get webview bounds: {e}"); + } + }, + WebviewMessage::SetPosition(position) => match webview.bounds() { + Ok(mut bounds) => { + bounds.position = position; + + let scale_factor = window.scale_factor(); + let position = position.to_logical::(scale_factor); + + if let Some(b) = &mut *webview.bounds.lock().unwrap() { + let window_size = window.inner_size().to_logical::(scale_factor); + b.x_rate = position.x / window_size.width; + b.y_rate = position.y / window_size.height; + } + + if let Err(e) = webview.set_bounds(bounds) { + log::error!("failed to set webview position: {e}"); + } + } + Err(e) => { + log::error!("failed to get webview bounds: {e}"); + } + }, + WebviewMessage::SetZoom(scale_factor) => { + if let Err(e) = webview.zoom(scale_factor) { + log::error!("failed to set webview zoom: {e}"); + } + } + WebviewMessage::SetBackgroundColor(color) => { + if let Err(e) = + webview.set_background_color(color.map(Into::into).unwrap_or((255, 255, 255, 255))) + { + log::error!("failed to set webview background color: {e}"); + } + } + WebviewMessage::ClearAllBrowsingData => { + if let Err(e) = webview.clear_all_browsing_data() { + log::error!("failed to clear webview browsing data: {e}"); + } + } + // Getters + WebviewMessage::Url(tx) => { + tx.send( + webview + .url() + .map(|u| u.parse().expect("invalid webview URL")) + .map_err(|_| Error::FailedToSendMessage), + ) + .unwrap(); + } + + WebviewMessage::Cookies(tx) => { + tx.send(webview.cookies().map_err(|_| Error::FailedToSendMessage)) + .unwrap(); + } + + WebviewMessage::SetCookie(cookie) => { + if let Err(e) = webview.set_cookie(&cookie) { + log::error!("failed to set webview cookie: {e}"); + } + } + + WebviewMessage::DeleteCookie(cookie) => { + if let Err(e) = webview.delete_cookie(&cookie) { + log::error!("failed to delete webview cookie: {e}"); + } + } + + WebviewMessage::CookiesForUrl(url, tx) => { + let webview_cookies = webview + .cookies_for_url(url.as_str()) + .map_err(|_| Error::FailedToSendMessage); + tx.send(webview_cookies).unwrap(); + } + + WebviewMessage::Bounds(tx) => { + tx.send( + webview + .bounds() + .map(|bounds| tauri_runtime::dpi::Rect { + size: bounds.size, + position: bounds.position, + }) + .map_err(|_| Error::FailedToSendMessage), + ) + .unwrap(); + } + WebviewMessage::Position(tx) => { + tx.send( + webview + .bounds() + .map(|bounds| bounds.position.to_physical(window.scale_factor())) + .map_err(|_| Error::FailedToSendMessage), + ) + .unwrap(); + } + WebviewMessage::Size(tx) => { + tx.send( + webview + .bounds() + .map(|bounds| bounds.size.to_physical(window.scale_factor())) + .map_err(|_| Error::FailedToSendMessage), + ) + .unwrap(); + } + WebviewMessage::SetFocus => { + if let Err(e) = webview.focus() { + log::error!("failed to focus webview: {e}"); + } + } + WebviewMessage::SetAutoResize(auto_resize) => match webview.bounds() { + Ok(bounds) => { + let scale_factor = window.scale_factor(); + let window_size = window.inner_size().to_logical::(scale_factor); + *webview.bounds.lock().unwrap() = if auto_resize { + let size = bounds.size.to_logical::(scale_factor); + let position = bounds.position.to_logical::(scale_factor); + Some(WebviewBounds { + x_rate: position.x / window_size.width, + y_rate: position.y / window_size.height, + width_rate: size.width / window_size.width, + height_rate: size.height / window_size.height, + }) + } else { + None + }; + } + Err(e) => { + log::error!("failed to get webview bounds: {e}"); + } + }, + WebviewMessage::WithWebview(f) => { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + f(webview.webview()); + } + #[cfg(target_os = "macos")] + { + use wry::WebViewExtMacOS; + f(Webview { + webview: Retained::into_raw(webview.webview()) as *mut objc2::runtime::AnyObject + as *mut std::ffi::c_void, + manager: Retained::into_raw(webview.manager()) as *mut objc2::runtime::AnyObject + as *mut std::ffi::c_void, + ns_window: Retained::into_raw(webview.ns_window()) as *mut objc2::runtime::AnyObject + as *mut std::ffi::c_void, + }); + } + #[cfg(target_os = "ios")] + { + use wry::WebViewExtIOS; + + f(Webview { + webview: Retained::into_raw(webview.inner.webview()) + as *mut objc2::runtime::AnyObject + as *mut std::ffi::c_void, + manager: Retained::into_raw(webview.inner.manager()) + as *mut objc2::runtime::AnyObject + as *mut std::ffi::c_void, + view_controller: window.ui_view_controller(), + }); + } + #[cfg(windows)] + { + f(Webview { + controller: webview.controller(), + environment: webview.environment(), + }); + } + #[cfg(target_os = "android")] + { + f(webview.handle()) + } + } + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::OpenDevTools => { + webview.open_devtools(); + } + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::CloseDevTools => { + webview.close_devtools(); + } + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::IsDevToolsOpen(tx) => { + tx.send(webview.is_devtools_open()).unwrap(); + } + } + } + } + Message::CreateWebview(window_id, handler) => { + let window = windows + .0 + .borrow() + .get(&window_id) + .map(|w| (w.inner.clone(), w.focused_webview.clone())); + if let Some((Some(window), focused_webview)) = window { + match handler(&window, CreateWebviewOptions { focused_webview }) { + Ok(webview) => { + if let Some(w) = windows.0.borrow_mut().get_mut(&window_id) { + w.webviews.push(webview); + w.has_children.store(true, Ordering::Relaxed); + } + } + Err(e) => { + log::error!("{e}"); + } + } + } + } + Message::CreateWindow(window_id, handler) => match handler(event_loop) { + Ok(webview) => { + windows.0.borrow_mut().insert(window_id, webview); + } + Err(e) => { + log::error!("{e}"); + } + }, + Message::CreateRawWindow(window_id, handler, sender) => { + let (label, builder) = handler(); + + #[cfg(windows)] + let background_color = builder.window.background_color; + #[cfg(windows)] + let is_window_transparent = builder.window.transparent; + + if let Ok(window) = builder.build(event_loop) { + window_id_map.insert(window.id(), window_id); + + let window = Arc::new(window); + + #[cfg(windows)] + let surface = if is_window_transparent { + if let Ok(context) = softbuffer::Context::new(window.clone()) { + if let Ok(mut surface) = softbuffer::Surface::new(&context, window.clone()) { + window.draw_surface(&mut surface, background_color); + Some(surface) + } else { + None + } + } else { + None + } + } else { + None + }; + + windows.0.borrow_mut().insert( + window_id, + WindowWrapper { + label, + has_children: AtomicBool::new(false), + inner: Some(window.clone()), + window_event_listeners: Default::default(), + webviews: Vec::new(), + #[cfg(windows)] + background_color, + #[cfg(windows)] + is_window_transparent, + #[cfg(windows)] + surface, + focused_webview: Default::default(), + }, + ); + sender.send(Ok(Arc::downgrade(&window))).unwrap(); + } else { + sender.send(Err(Error::CreateWindow)).unwrap(); + } + } + + Message::UserEvent(_) => (), + Message::EventLoopWindowTarget(message) => match message { + EventLoopWindowTargetMessage::CursorPosition(sender) => { + let pos = event_loop + .cursor_position() + .map_err(|_| Error::FailedToSendMessage); + sender.send(pos).unwrap(); + } + EventLoopWindowTargetMessage::SetTheme(theme) => { + event_loop.set_theme(to_tao_theme(theme)); + } + EventLoopWindowTargetMessage::SetDeviceEventFilter(filter) => { + event_loop.set_device_event_filter(DeviceEventFilterWrapper::from(filter).0); + } + }, + } +} + +fn handle_event_loop( + event: Event<'_, Message>, + event_loop: &EventLoopWindowTarget>, + control_flow: &mut ControlFlow, + context: EventLoopIterationContext<'_, T>, +) { + let EventLoopIterationContext { + callback, + window_id_map, + windows, + #[cfg(feature = "tracing")] + active_tracing_spans, + } = context; + if *control_flow != ControlFlow::Exit { + *control_flow = ControlFlow::Wait; + } + + match event { + Event::NewEvents(StartCause::Init) => { + callback(RunEvent::Ready); + } + + Event::NewEvents(StartCause::Poll) => { + callback(RunEvent::Resumed); + } + + Event::MainEventsCleared => { + callback(RunEvent::MainEventsCleared); + } + + Event::LoopDestroyed => { + callback(RunEvent::Exit); + } + + #[cfg(windows)] + Event::RedrawRequested(id) => { + if let Some(window_id) = window_id_map.get(&id) { + let mut windows_ref = windows.0.borrow_mut(); + if let Some(window) = windows_ref.get_mut(&window_id) { + if window.is_window_transparent { + let background_color = window.background_color; + if let Some(surface) = &mut window.surface { + if let Some(window) = &window.inner { + window.draw_surface(surface, background_color); + } + } + } + } + } + } + + #[cfg(feature = "tracing")] + Event::RedrawEventsCleared => { + active_tracing_spans.remove_window_draw(); + } + + Event::UserEvent(Message::Webview( + window_id, + webview_id, + WebviewMessage::WebviewEvent(event), + )) => { + let windows_ref = windows.0.borrow(); + if let Some(window) = windows_ref.get(&window_id) { + if let Some(webview) = window.webviews.iter().find(|w| w.id == webview_id) { + let label = webview.label.clone(); + let webview_event_listeners = webview.webview_event_listeners.clone(); + + drop(windows_ref); + + callback(RunEvent::WebviewEvent { + label, + event: event.clone(), + }); + let listeners = webview_event_listeners.lock().unwrap(); + let handlers = listeners.values(); + for handler in handlers { + handler(&event); + } + } + } + } + + Event::UserEvent(Message::Webview( + window_id, + _webview_id, + WebviewMessage::SynthesizedWindowEvent(event), + )) => { + if let Some(event) = WindowEventWrapper::from(event).0 { + let windows_ref = windows.0.borrow(); + let window = windows_ref.get(&window_id); + if let Some(window) = window { + let label = window.label.clone(); + let window_event_listeners = window.window_event_listeners.clone(); + + drop(windows_ref); + + callback(RunEvent::WindowEvent { + label, + event: event.clone(), + }); + + let listeners = window_event_listeners.lock().unwrap(); + let handlers = listeners.values(); + for handler in handlers { + handler(&event); + } + } + } + } + + Event::WindowEvent { + event, window_id, .. + } => { + if let Some(window_id) = window_id_map.get(&window_id) { + { + let windows_ref = windows.0.borrow(); + if let Some(window) = windows_ref.get(&window_id) { + if let Some(event) = WindowEventWrapper::parse(window, &event).0 { + let label = window.label.clone(); + let window_event_listeners = window.window_event_listeners.clone(); + + drop(windows_ref); + + callback(RunEvent::WindowEvent { + label, + event: event.clone(), + }); + let listeners = window_event_listeners.lock().unwrap(); + let handlers = listeners.values(); + for handler in handlers { + handler(&event); + } + } + } + } + + match event { + #[cfg(windows)] + TaoWindowEvent::ThemeChanged(theme) => { + if let Some(window) = windows.0.borrow().get(&window_id) { + for webview in &window.webviews { + let theme = match theme { + TaoTheme::Dark => wry::Theme::Dark, + TaoTheme::Light => wry::Theme::Light, + _ => wry::Theme::Light, + }; + if let Err(e) = webview.set_theme(theme) { + log::error!("failed to set theme: {e}"); + } + } + } + } + TaoWindowEvent::CloseRequested => { + on_close_requested(callback, window_id, windows); + } + TaoWindowEvent::Destroyed => { + let removed = windows.0.borrow_mut().remove(&window_id).is_some(); + if removed { + let is_empty = windows.0.borrow().is_empty(); + if is_empty { + let (tx, rx) = channel(); + callback(RunEvent::ExitRequested { code: None, tx }); + + let recv = rx.try_recv(); + let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); + + if !should_prevent { + *control_flow = ControlFlow::Exit; + } + } + } + } + TaoWindowEvent::Resized(size) => { + if let Some((Some(window), webviews)) = windows + .0 + .borrow() + .get(&window_id) + .map(|w| (w.inner.clone(), w.webviews.clone())) + { + let size = size.to_logical::(window.scale_factor()); + for webview in webviews { + if let Some(b) = &*webview.bounds.lock().unwrap() { + if let Err(e) = webview.set_bounds(wry::Rect { + position: LogicalPosition::new(size.width * b.x_rate, size.height * b.y_rate) + .into(), + size: LogicalSize::new(size.width * b.width_rate, size.height * b.height_rate) + .into(), + }) { + log::error!("failed to autoresize webview: {e}"); + } + } + } + } + } + _ => {} + } + } + } + Event::UserEvent(message) => match message { + Message::RequestExit(code) => { + let (tx, rx) = channel(); + callback(RunEvent::ExitRequested { + code: Some(code), + tx, + }); + + let recv = rx.try_recv(); + let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); + + if !should_prevent { + *control_flow = ControlFlow::Exit; + } + } + Message::Window(id, WindowMessage::Close) => { + on_close_requested(callback, id, windows); + } + Message::Window(id, WindowMessage::Destroy) => { + on_window_close(id, windows); + } + Message::UserEvent(t) => callback(RunEvent::UserEvent(t)), + message => { + handle_user_message( + event_loop, + message, + UserMessageContext { + window_id_map, + windows, + }, + ); + } + }, + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] + Event::Opened { urls } => { + callback(RunEvent::Opened { urls }); + } + #[cfg(target_os = "macos")] + Event::Reopen { + has_visible_windows, + .. + } => callback(RunEvent::Reopen { + has_visible_windows, + }), + #[cfg(target_os = "ios")] + Event::SceneRequested { scene, options } => { + callback(RunEvent::SceneRequested { scene, options }); + } + #[cfg(mobile)] + e @ Event::Resumed | e @ Event::Suspended => { + let event = match e { + Event::Resumed => WindowEvent::Resumed, + Event::Suspended => WindowEvent::Suspended, + _ => unreachable!(), + }; + + // Collect the per-window listener handles and release the `windows` + // borrow before dispatching: handlers and the `RunEvent` callback may + // create or close windows (`windows.0.borrow_mut()`), which would panic + // the `RefCell` if we held the borrow across them. The desktop + // `WindowEvent` branches drop the borrow before dispatching for the same + // reason; this mobile `Resumed`/`Suspended` branch was the exception. + let targets = windows + .0 + .borrow() + .values() + .map(|w| (w.label.clone(), w.window_event_listeners.clone())) + .collect::>(); + + for (label, window_event_listeners) in targets { + let listeners = window_event_listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&event); + } + + callback(RunEvent::WindowEvent { + label, + event: event.clone(), + }); + } + } + _ => (), + } +} + +fn on_close_requested<'a, T: UserEvent>( + callback: &'a mut (dyn FnMut(RunEvent) + 'static), + window_id: WindowId, + windows: Arc, +) { + let (tx, rx) = channel(); + let windows_ref = windows.0.borrow(); + if let Some(w) = windows_ref.get(&window_id) { + let label = w.label.clone(); + let window_event_listeners = w.window_event_listeners.clone(); + + drop(windows_ref); + + let listeners = window_event_listeners.lock().unwrap(); + let handlers = listeners.values(); + for handler in handlers { + handler(&WindowEvent::CloseRequested { + signal_tx: tx.clone(), + }); + } + callback(RunEvent::WindowEvent { + label, + event: WindowEvent::CloseRequested { signal_tx: tx }, + }); + if let Ok(true) = rx.try_recv() { + } else { + on_window_close(window_id, windows); + } + } +} + +fn on_window_close(window_id: WindowId, windows: Arc) { + if let Some(window_wrapper) = windows.0.borrow_mut().get_mut(&window_id) { + window_wrapper.inner = None; + #[cfg(windows)] + window_wrapper.surface.take(); + } +} + +fn parse_proxy_url(url: &Url) -> Result { + let host = url.host().map(|h| h.to_string()).unwrap_or_default(); + let port = url.port().map(|p| p.to_string()).unwrap_or_default(); + + if url.scheme() == "http" { + let config = ProxyConfig::Http(ProxyEndpoint { host, port }); + + Ok(config) + } else if url.scheme() == "socks5" { + let config = ProxyConfig::Socks5(ProxyEndpoint { host, port }); + + Ok(config) + } else { + Err(Error::InvalidProxyUrl) + } +} + +fn create_window( + window_id: WindowId, + webview_id: u32, + event_loop: &EventLoopWindowTarget>, + context: &Context, + pending: PendingWindow>, + after_window_creation: Option, +) -> Result { + #[allow(unused_mut)] + let PendingWindow { + mut window_builder, + label, + webview, + } = pending; + + #[cfg(feature = "tracing")] + let _webview_create_span = tracing::debug_span!("wry::webview::create").entered(); + #[cfg(feature = "tracing")] + let window_draw_span = tracing::debug_span!("wry::window::draw").entered(); + #[cfg(feature = "tracing")] + let window_create_span = + tracing::debug_span!(parent: &window_draw_span, "wry::window::create").entered(); + + let window_event_listeners = WindowEventListeners::default(); + + #[cfg(windows)] + let background_color = window_builder.inner.window.background_color; + #[cfg(windows)] + let is_window_transparent = window_builder.inner.window.transparent; + + #[cfg(target_os = "macos")] + { + if window_builder.tabbing_identifier.is_none() + || window_builder.inner.window.transparent + || !window_builder.inner.window.decorations + { + window_builder.inner = window_builder.inner.with_automatic_window_tabbing(false); + } + } + + #[cfg(desktop)] + if window_builder.prevent_overflow.is_some() || window_builder.center { + let monitor = if let Some(window_position) = &window_builder.inner.window.position { + find_monitor_for_position(event_loop.available_monitors(), *window_position) + } else { + event_loop.primary_monitor() + }; + if let Some(monitor) = monitor { + let scale_factor = monitor.scale_factor(); + let desired_size = window_builder + .inner + .window + .inner_size + .unwrap_or_else(|| TaoPhysicalSize::new(800, 600).into()); + let mut inner_size = window_builder + .inner + .window + .inner_size_constraints + .clamp(desired_size, scale_factor) + .to_physical::(scale_factor); + let mut window_size = inner_size; + #[allow(unused_mut)] + // Left and right window shadow counts as part of the window on Windows + // We need to include it when calculating positions, but not size + let mut shadow_width = 0; + #[cfg(windows)] + if window_builder.inner.window.decorations { + use windows::Win32::UI::WindowsAndMessaging::{AdjustWindowRect, WS_OVERLAPPEDWINDOW}; + let mut rect = windows::Win32::Foundation::RECT::default(); + let result = unsafe { AdjustWindowRect(&mut rect, WS_OVERLAPPEDWINDOW, false) }; + if result.is_ok() { + shadow_width = (rect.right - rect.left) as u32; + // rect.bottom is made out of shadow, and we don't care about it + window_size.height += -rect.top as u32; + } + } + + if let Some(margin) = window_builder.prevent_overflow { + let work_area = monitor.work_area(); + let margin = margin.to_physical::(scale_factor); + let constraint = PhysicalSize::new( + work_area.size.width - margin.width, + work_area.size.height - margin.height, + ); + if window_size.width > constraint.width || window_size.height > constraint.height { + if window_size.width > constraint.width { + inner_size.width = inner_size + .width + .saturating_sub(window_size.width - constraint.width); + window_size.width = constraint.width; + } + if window_size.height > constraint.height { + inner_size.height = inner_size + .height + .saturating_sub(window_size.height - constraint.height); + window_size.height = constraint.height; + } + window_builder.inner.window.inner_size = Some(inner_size.into()); + } + } + + if window_builder.center { + window_size.width += shadow_width; + let position = window::calculate_window_center_position(window_size, monitor); + let logical_position = position.to_logical::(scale_factor); + window_builder = window_builder.position(logical_position.x, logical_position.y); + } + } + }; + + #[cfg(any(target_os = "macos", target_os = "linux"))] + let (initial_position, is_fullscreen) = ( + window_builder.inner.window.position, + window_builder.inner.window.fullscreen.is_some(), + ); + + // If fullscreen is requested with an explicit position, resolve the target + // monitor up front so the window is created fullscreen on that display. + #[cfg(any(target_os = "macos", target_os = "linux"))] + if let (true, Some(position)) = (is_fullscreen, initial_position) { + if let Some(target_monitor) = + find_monitor_for_position(event_loop.available_monitors(), position) + { + window_builder.inner.window.fullscreen = Some(Fullscreen::Borderless(Some(target_monitor))); + } + } + + let window = window_builder + .inner + .build(event_loop) + .inspect_err(|e| log::error!("Error creating window: {e:?}")) + .map_err(|_| Error::CreateWindow)?; + + // On macOS, `with_position` uses the content origin; the title bar is added + // above it. `set_outer_position` is needed for precise window placement. + #[cfg(target_os = "macos")] + if !is_fullscreen { + if let Some(position) = initial_position { + window.set_outer_position(position); + } + } + + #[cfg(feature = "tracing")] + { + drop(window_create_span); + + context + .main_thread + .active_tracing_spans + .0 + .borrow_mut() + .push(ActiveTracingSpan::WindowDraw { + id: window.id(), + span: window_draw_span, + }); + } + + context.window_id_map.insert(window.id(), window_id); + + if let Some(handler) = after_window_creation { + let raw = RawWindow { + #[cfg(windows)] + hwnd: window.hwnd(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + gtk_window: window.gtk_window(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + default_vbox: window.default_vbox(), + _marker: &std::marker::PhantomData, + }; + handler(raw); + } + + let mut webviews = Vec::new(); + + let focused_webview = Arc::new(Mutex::new(None)); + + if let Some(webview) = webview { + webviews.push(create_webview( + #[cfg(feature = "unstable")] + WebviewKind::WindowChild, + #[cfg(not(feature = "unstable"))] + WebviewKind::WindowContent, + &window, + Arc::new(Mutex::new(window_id)), + webview_id, + context, + webview, + focused_webview.clone(), + )?); + } + + let window = Arc::new(window); + + #[cfg(windows)] + let surface = if is_window_transparent { + if let Ok(context) = softbuffer::Context::new(window.clone()) { + if let Ok(mut surface) = softbuffer::Surface::new(&context, window.clone()) { + window.draw_surface(&mut surface, background_color); + Some(surface) + } else { + None + } + } else { + None + } + } else { + None + }; + + Ok(WindowWrapper { + label, + has_children: AtomicBool::new(false), + inner: Some(window), + webviews, + window_event_listeners, + #[cfg(windows)] + background_color, + #[cfg(windows)] + is_window_transparent, + #[cfg(windows)] + surface, + focused_webview, + }) +} + +/// the kind of the webview +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +enum WebviewKind { + /// webview is the entire window content + WindowContent, + /// webview is a child of the window, which can contain other webviews too + WindowChild, +} + +#[derive(Debug, Clone)] +struct WebviewBounds { + x_rate: f32, + y_rate: f32, + width_rate: f32, + height_rate: f32, +} + +fn create_webview( + kind: WebviewKind, + window: &Window, + window_id: Arc>, + id: WebviewId, + context: &Context, + pending: PendingWebview>, + #[allow(unused_variables)] focused_webview: Arc>>, +) -> Result { + if !context.webview_runtime_installed { + #[cfg(all(not(debug_assertions), windows))] + dialog::error( + r#"Could not find the WebView2 Runtime. + +Make sure it is installed or download it from https://developer.microsoft.com/en-us/microsoft-edge/webview2 + +You may have it installed on another user account, but it is not available for this one. +"#, + ); + + if cfg!(target_os = "macos") { + log::warn!("WebKit webview runtime not found, attempting to create webview anyway."); + } else { + return Err(Error::WebviewRuntimeNotInstalled); + } + } + + #[allow(unused_mut)] + let PendingWebview { + webview_attributes, + uri_scheme_protocols, + label, + ipc_handler, + url, + .. + } = pending; + + let mut web_context = context + .main_thread + .web_context + .lock() + .expect("poisoned WebContext store"); + let is_first_context = web_context.is_empty(); + // the context must be stored on the HashMap because it must outlive the WebView on macOS + let automation_enabled = std::env::var("TAURI_WEBVIEW_AUTOMATION").as_deref() == Ok("true"); + let web_context_key = webview_attributes.data_directory; + let entry = web_context.entry(web_context_key.clone()); + let web_context = match entry { + Occupied(occupied) => { + let occupied = occupied.into_mut(); + occupied.referenced_by_webviews.insert(label.clone()); + occupied + } + Vacant(vacant) => { + let mut web_context = WryWebContext::new(web_context_key.clone()); + web_context.set_allows_automation(if automation_enabled { + is_first_context + } else { + false + }); + vacant.insert(WebContext { + inner: web_context, + referenced_by_webviews: [label.clone()].into(), + registered_custom_protocols: HashSet::new(), + }) + } + }; + + let mut webview_transparent = webview_attributes.transparent; + #[cfg(target_os = "linux")] + use gtk::glib::ObjectExt; + #[cfg(target_os = "linux")] + if label == "main" + && gtk::gdk::Display::default() + .map(|display| display.type_().name() == "GdkX11Display") + .unwrap_or(false) + { + // X11 cannot alpha-compose WebKitGTK over GtkGLArea. The video surface is + // an overlay there, so an opaque WebView base keeps controls/title visible + // in the area outside the video rectangle. + webview_transparent = false; + } + + let mut webview_builder = WebViewBuilder::new_with_web_context(&mut web_context.inner) + .with_id(&label) + .with_focused(webview_attributes.focus) + .with_transparent(webview_transparent) + .with_accept_first_mouse(webview_attributes.accept_first_mouse) + .with_incognito(webview_attributes.incognito) + .with_clipboard(webview_attributes.clipboard) + .with_hotkeys_zoom(webview_attributes.zoom_hotkeys_enabled) + .with_general_autofill_enabled(webview_attributes.general_autofill_enabled); + + if url != "about:blank" { + webview_builder = webview_builder.with_url(&url); + } + + #[cfg(target_os = "macos")] + if let Some(webview_configuration) = webview_attributes.webview_configuration { + webview_builder = webview_builder.with_webview_configuration(webview_configuration); + } + + #[cfg(any(target_os = "windows", target_os = "android"))] + { + webview_builder = webview_builder.with_https_scheme(webview_attributes.use_https_scheme); + } + + if let Some(background_throttling) = webview_attributes.background_throttling { + webview_builder = webview_builder.with_background_throttling(match background_throttling { + tauri_utils::config::BackgroundThrottlingPolicy::Disabled => { + wry::BackgroundThrottlingPolicy::Disabled + } + tauri_utils::config::BackgroundThrottlingPolicy::Suspend => { + wry::BackgroundThrottlingPolicy::Suspend + } + tauri_utils::config::BackgroundThrottlingPolicy::Throttle => { + wry::BackgroundThrottlingPolicy::Throttle + } + }); + } + + if webview_attributes.javascript_disabled { + webview_builder = webview_builder.with_javascript_disabled(); + } + + if let Some(color) = webview_attributes.background_color { + webview_builder = webview_builder.with_background_color(color.into()); + } + + if webview_attributes.drag_drop_handler_enabled { + let proxy = context.proxy.clone(); + let window_id_ = window_id.clone(); + webview_builder = webview_builder.with_drag_drop_handler(move |event| { + let event = match event { + WryDragDropEvent::Enter { + paths, + position: (x, y), + } => DragDropEvent::Enter { + paths, + position: PhysicalPosition::new(x as _, y as _), + }, + WryDragDropEvent::Over { position: (x, y) } => DragDropEvent::Over { + position: PhysicalPosition::new(x as _, y as _), + }, + WryDragDropEvent::Drop { + paths, + position: (x, y), + } => DragDropEvent::Drop { + paths, + position: PhysicalPosition::new(x as _, y as _), + }, + WryDragDropEvent::Leave => DragDropEvent::Leave, + _ => unimplemented!(), + }; + + let message = if kind == WebviewKind::WindowContent { + WebviewMessage::SynthesizedWindowEvent(SynthesizedWindowEvent::DragDrop(event)) + } else { + WebviewMessage::WebviewEvent(WebviewEvent::DragDrop(event)) + }; + + let _ = proxy.send_event(Message::Webview(*window_id_.lock().unwrap(), id, message)); + true + }); + } + + if let Some(navigation_handler) = pending.navigation_handler { + webview_builder = webview_builder.with_navigation_handler(move |url| { + url + .parse() + .map(|url| navigation_handler(&url)) + .unwrap_or(true) + }); + } + + if let Some(new_window_handler) = pending.new_window_handler { + #[cfg(desktop)] + let context = context.clone(); + webview_builder = webview_builder.with_new_window_req_handler(move |url, features| { + let Ok(url) = url.parse() else { + return wry::NewWindowResponse::Deny; + }; + let response = new_window_handler( + url, + tauri_runtime::webview::NewWindowFeatures::new( + features.size, + features.position, + tauri_runtime::webview::NewWindowOpener { + #[cfg(desktop)] + webview: features.opener.webview, + #[cfg(windows)] + environment: features.opener.environment, + #[cfg(target_os = "macos")] + target_configuration: features.opener.target_configuration, + }, + ), + ); + match response { + tauri_runtime::webview::NewWindowResponse::Allow => wry::NewWindowResponse::Allow, + #[cfg(desktop)] + tauri_runtime::webview::NewWindowResponse::Create { window_id } => { + let windows = &context.main_thread.windows.0; + let webview = windows + .borrow() + .get(&window_id) + .unwrap() + .webviews + .first() + .unwrap() + .clone(); + + #[cfg(desktop)] + wry::NewWindowResponse::Create { + #[cfg(target_os = "macos")] + webview: wry::WebViewExtMacOS::webview(&*webview).as_super().into(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + webview: webview.webview(), + #[cfg(windows)] + webview: webview.webview(), + } + } + tauri_runtime::webview::NewWindowResponse::Deny => wry::NewWindowResponse::Deny, + } + }); + } + + if let Some(document_title_changed_handler) = pending.document_title_changed_handler { + webview_builder = + webview_builder.with_document_title_changed_handler(document_title_changed_handler) + } + + let webview_bounds = if let Some(bounds) = webview_attributes.bounds { + let bounds: RectWrapper = bounds.into(); + let bounds = bounds.0; + + let scale_factor = window.scale_factor(); + let position = bounds.position.to_logical::(scale_factor); + let size = bounds.size.to_logical::(scale_factor); + + webview_builder = webview_builder.with_bounds(bounds); + + let window_size = window.inner_size().to_logical::(scale_factor); + + if webview_attributes.auto_resize { + Some(WebviewBounds { + x_rate: position.x / window_size.width, + y_rate: position.y / window_size.height, + width_rate: size.width / window_size.width, + height_rate: size.height / window_size.height, + }) + } else { + None + } + } else { + #[cfg(feature = "unstable")] + { + webview_builder = webview_builder.with_bounds(wry::Rect { + position: LogicalPosition::new(0, 0).into(), + size: window.inner_size().into(), + }); + Some(WebviewBounds { + x_rate: 0., + y_rate: 0., + width_rate: 1., + height_rate: 1., + }) + } + #[cfg(not(feature = "unstable"))] + None + }; + + if let Some(download_handler) = pending.download_handler { + let download_handler_ = download_handler.clone(); + webview_builder = webview_builder.with_download_started_handler(move |url, path| { + if let Ok(url) = url.parse() { + download_handler_(DownloadEvent::Requested { + url, + destination: path, + }) + } else { + false + } + }); + webview_builder = webview_builder.with_download_completed_handler(move |url, path, success| { + if let Ok(url) = url.parse() { + download_handler(DownloadEvent::Finished { url, path, success }); + } + }); + } + + if let Some(page_load_handler) = pending.on_page_load_handler { + webview_builder = webview_builder.with_on_page_load_handler(move |event, url| { + if let Ok(url) = url.parse() { + page_load_handler( + url, + match event { + wry::PageLoadEvent::Started => tauri_runtime::webview::PageLoadEvent::Started, + wry::PageLoadEvent::Finished => tauri_runtime::webview::PageLoadEvent::Finished, + }, + ) + }; + }); + } + + if let Some(user_agent) = webview_attributes.user_agent { + webview_builder = webview_builder.with_user_agent(&user_agent); + } + + if let Some(proxy_url) = webview_attributes.proxy_url { + let config = parse_proxy_url(&proxy_url)?; + + webview_builder = webview_builder.with_proxy_config(config); + } + + #[cfg(windows)] + { + if let Some(additional_browser_args) = webview_attributes.additional_browser_args { + webview_builder = webview_builder.with_additional_browser_args(&additional_browser_args); + } + + if let Some(environment) = webview_attributes.environment { + webview_builder = webview_builder.with_environment(environment); + } + + webview_builder = webview_builder.with_theme(match window.theme() { + TaoTheme::Dark => wry::Theme::Dark, + TaoTheme::Light => wry::Theme::Light, + _ => wry::Theme::Light, + }); + + webview_builder = + webview_builder.with_scroll_bar_style(match webview_attributes.scroll_bar_style { + ScrollBarStyle::Default => WryScrollBarStyle::Default, + ScrollBarStyle::FluentOverlay => WryScrollBarStyle::FluentOverlay, + _ => unreachable!(), + }); + } + + #[cfg(windows)] + { + webview_builder = webview_builder + .with_browser_extensions_enabled(webview_attributes.browser_extensions_enabled); + } + + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + if let Some(path) = &webview_attributes.extensions_path { + webview_builder = webview_builder.with_extensions_path(path); + } + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + if let Some(related_view) = webview_attributes.related_view { + webview_builder = webview_builder.with_related_view(related_view); + } + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + { + if let Some(data_store_identifier) = &webview_attributes.data_store_identifier { + webview_builder = webview_builder.with_data_store_identifier(*data_store_identifier); + } + + webview_builder = + webview_builder.with_allow_link_preview(webview_attributes.allow_link_preview); + + if let Some(on_web_content_process_terminate_handler) = + pending.on_web_content_process_terminate_handler + { + webview_builder = webview_builder + .with_on_web_content_process_terminate_handler(on_web_content_process_terminate_handler); + } else { + log::debug!("web content process terminated"); + let context_ = context.clone(); + let window_id_ = window_id.clone(); + webview_builder = webview_builder.with_on_web_content_process_terminate_handler(move || { + if let Ok(windows) = &context_.main_thread.windows.0.try_borrow() { + if let Some(window) = windows.get(&*window_id_.lock().unwrap()) { + if let Some(webview) = window.webviews.iter().find(|w| w.id == id) { + match webview.reload() { + Ok(_) => log::debug!("webview reloaded"), + Err(e) => log::error!("failed to reload webview: {e}"), + } + } else { + log::error!("failed to find webview") + } + } else { + log::error!("failed to get window") + } + } else { + log::error!("failed to borrow windows") + } + }); + } + } + + #[cfg(target_os = "ios")] + { + if let Some(input_accessory_view_builder) = webview_attributes.input_accessory_view_builder { + webview_builder = webview_builder + .with_input_accessory_view_builder(move |webview| input_accessory_view_builder.0(webview)); + } + } + + #[cfg(target_os = "macos")] + { + if let Some(position) = &webview_attributes.traffic_light_position { + webview_builder = webview_builder.with_traffic_light_inset(*position); + } + } + + webview_builder = webview_builder.with_ipc_handler(create_ipc_handler( + window_id.clone(), + id, + context.clone(), + label.clone(), + ipc_handler, + )); + + for script in webview_attributes.initialization_scripts { + webview_builder = webview_builder + .with_initialization_script_for_main_only(script.script, script.for_main_frame_only); + } + + for (scheme, protocol) in uri_scheme_protocols { + // on Linux the custom protocols are associated with the web context + // and you cannot register a scheme more than once + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + if web_context.registered_custom_protocols.contains(&scheme) { + continue; + } + + web_context + .registered_custom_protocols + .insert(scheme.clone()); + } + + webview_builder = webview_builder.with_asynchronous_custom_protocol( + scheme, + move |webview_id, request, responder| { + protocol( + webview_id, + request, + Box::new(move |response| responder.respond(response)), + ) + }, + ); + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + { + webview_builder = webview_builder.with_devtools(webview_attributes.devtools.unwrap_or(true)); + } + + #[cfg(target_os = "android")] + { + if let Some(on_webview_created) = pending.on_webview_created { + webview_builder = webview_builder.on_webview_created(move |ctx| { + on_webview_created(tauri_runtime::webview::CreationContext { + env: ctx.env, + activity: ctx.activity, + webview: ctx.webview, + }) + }); + } + } + + let webview = match kind { + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + WebviewKind::WindowChild => { + // only way to account for menu bar height, and also works for multiwebviews :) + let vbox = window.default_vbox().unwrap(); + if label == "main" && std::env::var("L_MPV_NATIVE_VIDEO").as_deref() != Ok("0") { + use gtk::glib::ObjectExt; + use gtk::prelude::{BoxExt, ContainerExt, OverlayExt, WidgetExt}; + + let overlay = gtk::Overlay::new(); + let video = gtk::GLArea::new(); + let webview_host = gtk::Box::new(gtk::Orientation::Vertical, 0); + video.set_widget_name("l-mpv-video"); + video.set_hexpand(true); + video.set_vexpand(true); + webview_host.set_hexpand(true); + webview_host.set_vexpand(true); + let is_x11 = gtk::gdk::Display::default() + .map(|display| display.type_().name() == "GdkX11Display") + .unwrap_or(false); + if is_x11 { + overlay.add(&webview_host); + overlay.add_overlay(&video); + } else { + overlay.add(&video); + overlay.add_overlay(&webview_host); + } + vbox.pack_start(&overlay, true, true, 0); + webview_builder.build_gtk(&webview_host) + } else { + webview_builder.build_gtk(vbox) + } + } + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + WebviewKind::WindowChild => webview_builder.build_as_child(&window), + WebviewKind::WindowContent => { + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let builder = webview_builder.build(&window); + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let builder = { + let vbox = window.default_vbox().unwrap(); + if label == "main" && std::env::var("L_MPV_NATIVE_VIDEO").as_deref() != Ok("0") { + use gtk::glib::ObjectExt; + use gtk::prelude::{BoxExt, ContainerExt, OverlayExt, WidgetExt}; + + let overlay = gtk::Overlay::new(); + let video = gtk::GLArea::new(); + let webview_host = gtk::Box::new(gtk::Orientation::Vertical, 0); + video.set_widget_name("l-mpv-video"); + video.set_hexpand(true); + video.set_vexpand(true); + webview_host.set_hexpand(true); + webview_host.set_vexpand(true); + let is_x11 = gtk::gdk::Display::default() + .map(|display| display.type_().name() == "GdkX11Display") + .unwrap_or(false); + if is_x11 { + overlay.add(&webview_host); + overlay.add_overlay(&video); + } else { + overlay.add(&video); + overlay.add_overlay(&webview_host); + } + vbox.pack_start(&overlay, true, true, 0); + webview_builder.build_gtk(&webview_host) + } else { + webview_builder.build_gtk(vbox) + } + }; + builder + } + } + .map_err(|e| Error::CreateWebview(Box::new(e)))?; + + if kind == WebviewKind::WindowContent { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + undecorated_resizing::attach_resize_handler(&webview); + #[cfg(windows)] + if window.is_resizable() && !window.is_decorated() { + undecorated_resizing::attach_resize_handler(window.hwnd(), window.has_undecorated_shadow()); + } + } + + #[cfg(windows)] + { + let controller = webview.controller(); + let proxy_clone = context.proxy.clone(); + let window_id_ = window_id.clone(); + let mut token = 0; + unsafe { + let label_ = label.clone(); + let focused_webview_ = focused_webview.clone(); + controller.add_GotFocus( + &FocusChangedEventHandler::create(Box::new(move |_, _| { + let mut focused_webview = focused_webview_.lock().unwrap(); + // when using multiwebview mode, we should check if the focus change is actually a "webview focus change" + // instead of a window focus change (here we're patching window events, so we only care about the actual window changing focus) + let already_focused = focused_webview.is_some(); + focused_webview.replace(label_.clone()); + + if !already_focused { + let _ = proxy_clone.send_event(Message::Webview( + *window_id_.lock().unwrap(), + id, + WebviewMessage::SynthesizedWindowEvent(SynthesizedWindowEvent::Focused(true)), + )); + } + Ok(()) + })), + &mut token, + ) + } + .unwrap(); + unsafe { + let label_ = label.clone(); + let window_id_ = window_id.clone(); + let proxy_clone = context.proxy.clone(); + controller.add_LostFocus( + &FocusChangedEventHandler::create(Box::new(move |_, _| { + let mut focused_webview = focused_webview.lock().unwrap(); + // when using multiwebview mode, we should handle webview focus changes + // so we check is the currently focused webview matches this webview's + // (in this case, it means we lost the window focus) + // + // on multiwebview mode if we change focus to a different webview + // we get the gotFocus event of the other webview before the lostFocus + // so this check makes sense + let lost_window_focus = focused_webview.as_ref().map_or(true, |w| w == &label_); + // TODO: Use `is_none_or` instead when MSRV gets raised above 1.82 + // let lost_window_focus = focused_webview.as_ref().is_none_or(|t| t == &label_); + + if lost_window_focus { + // only reset when we lost window focus - otherwise some other webview is focused + *focused_webview = None; + let _ = proxy_clone.send_event(Message::Webview( + *window_id_.lock().unwrap(), + id, + WebviewMessage::SynthesizedWindowEvent(SynthesizedWindowEvent::Focused(false)), + )); + } + Ok(()) + })), + &mut token, + ) + } + .unwrap(); + + if let Ok(webview) = unsafe { controller.CoreWebView2() } { + let proxy_clone = context.proxy.clone(); + unsafe { + let _ = webview.add_ContainsFullScreenElementChanged( + &ContainsFullScreenElementChangedEventHandler::create(Box::new(move |sender, _| { + let mut contains_fullscreen_element = windows::core::BOOL::default(); + sender + .ok_or_else(windows::core::Error::empty)? + .ContainsFullScreenElement(&mut contains_fullscreen_element)?; + let _ = proxy_clone.send_event(Message::Window( + *window_id.lock().unwrap(), + WindowMessage::SetFullscreen(contains_fullscreen_element.as_bool()), + )); + Ok(()) + })), + &mut token, + ); + } + } + } + + Ok(WebviewWrapper { + label, + id, + inner: Rc::new(webview), + context_store: context.main_thread.web_context.clone(), + webview_event_listeners: Default::default(), + context_key: if automation_enabled { + None + } else { + web_context_key + }, + bounds: Arc::new(Mutex::new(webview_bounds)), + }) +} + +/// Create a wry ipc handler from a tauri ipc handler. +fn create_ipc_handler( + window_id: Arc>, + webview_id: WebviewId, + context: Context, + label: String, + ipc_handler: Option>>, +) -> Box { + Box::new(move |request| { + if let Some(handler) = &ipc_handler { + handler( + DetachedWebview { + label: label.clone(), + dispatcher: WryWebviewDispatcher { + window_id: window_id.clone(), + webview_id, + context: context.clone(), + }, + }, + request, + ); + } + }) +} + +#[cfg(target_os = "macos")] +fn inner_size( + window: &Window, + webviews: &[WebviewWrapper], + has_children: bool, +) -> TaoPhysicalSize { + if !has_children && !webviews.is_empty() { + use wry::WebViewExtMacOS; + let webview = webviews.first().unwrap(); + let view = unsafe { Retained::cast_unchecked::(webview.webview()) }; + let view_frame = view.frame(); + let logical: TaoLogicalSize = (view_frame.size.width, view_frame.size.height).into(); + return logical.to_physical(window.scale_factor()); + } + + window.inner_size() +} + +#[cfg(not(target_os = "macos"))] +#[allow(unused_variables)] +fn inner_size( + window: &Window, + webviews: &[WebviewWrapper], + has_children: bool, +) -> TaoPhysicalSize { + window.inner_size() +} + +fn to_tao_theme(theme: Option) -> Option { + match theme { + Some(Theme::Light) => Some(TaoTheme::Light), + Some(Theme::Dark) => Some(TaoTheme::Dark), + _ => None, + } +} diff --git a/vendor/tauri-runtime-wry/src/monitor/linux.rs b/vendor/tauri-runtime-wry/src/monitor/linux.rs new file mode 100644 index 0000000..62f87eb --- /dev/null +++ b/vendor/tauri-runtime-wry/src/monitor/linux.rs @@ -0,0 +1,18 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use gtk::prelude::MonitorExt; +use tao::platform::unix::MonitorHandleExtUnix; +use tauri_runtime::dpi::{LogicalPosition, LogicalSize, PhysicalRect}; + +impl super::MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + let rect = self.gdk_monitor().workarea(); + let scale_factor = self.scale_factor(); + PhysicalRect { + size: LogicalSize::new(rect.width() as u32, rect.height() as u32).to_physical(scale_factor), + position: LogicalPosition::new(rect.x(), rect.y()).to_physical(scale_factor), + } + } +} diff --git a/vendor/tauri-runtime-wry/src/monitor/macos.rs b/vendor/tauri-runtime-wry/src/monitor/macos.rs new file mode 100644 index 0000000..aca8e33 --- /dev/null +++ b/vendor/tauri-runtime-wry/src/monitor/macos.rs @@ -0,0 +1,36 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::dpi::{LogicalSize, PhysicalRect}; + +impl super::MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + use objc2_app_kit::NSScreen; + use tao::platform::macos::MonitorHandleExtMacOS; + if let Some(ns_screen) = self.ns_screen() { + let ns_screen: &NSScreen = unsafe { &*ns_screen.cast() }; + let screen_frame = ns_screen.frame(); + let visible_frame = ns_screen.visibleFrame(); + + let scale_factor = self.scale_factor(); + + let mut position = self.position().to_logical::(scale_factor); + + position.x += visible_frame.origin.x - screen_frame.origin.x; + + position.y += (screen_frame.origin.y + screen_frame.size.height) + - (visible_frame.origin.y + visible_frame.size.height); + PhysicalRect { + size: LogicalSize::new(visible_frame.size.width, visible_frame.size.height) + .to_physical(scale_factor), + position: position.to_physical(scale_factor), + } + } else { + PhysicalRect { + size: self.size(), + position: self.position(), + } + } + } +} diff --git a/vendor/tauri-runtime-wry/src/monitor/mod.rs b/vendor/tauri-runtime-wry/src/monitor/mod.rs new file mode 100644 index 0000000..4322c3c --- /dev/null +++ b/vendor/tauri-runtime-wry/src/monitor/mod.rs @@ -0,0 +1,37 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::dpi::PhysicalRect; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; + +pub trait MonitorExt { + /// Get the work area of this monitor + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Unsupported. + fn work_area(&self) -> PhysicalRect; +} + +#[cfg(mobile)] +impl MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + PhysicalRect { + size: self.size(), + position: self.position(), + } + } +} diff --git a/vendor/tauri-runtime-wry/src/monitor/windows.rs b/vendor/tauri-runtime-wry/src/monitor/windows.rs new file mode 100644 index 0000000..8547ac9 --- /dev/null +++ b/vendor/tauri-runtime-wry/src/monitor/windows.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tao::dpi::{PhysicalPosition, PhysicalSize}; +use tauri_runtime::dpi::PhysicalRect; + +impl super::MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + use tao::platform::windows::MonitorHandleExtWindows; + use windows::Win32::Graphics::Gdi::{GetMonitorInfoW, HMONITOR, MONITORINFO}; + let mut monitor_info = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let status = unsafe { GetMonitorInfoW(HMONITOR(self.hmonitor() as _), &mut monitor_info) }; + if status.as_bool() { + PhysicalRect { + size: PhysicalSize::new( + (monitor_info.rcWork.right - monitor_info.rcWork.left) as u32, + (monitor_info.rcWork.bottom - monitor_info.rcWork.top) as u32, + ), + position: PhysicalPosition::new(monitor_info.rcWork.left, monitor_info.rcWork.top), + } + } else { + PhysicalRect { + size: self.size(), + position: self.position(), + } + } + } +} diff --git a/vendor/tauri-runtime-wry/src/undecorated_resizing.rs b/vendor/tauri-runtime-wry/src/undecorated_resizing.rs new file mode 100644 index 0000000..92b1f8b --- /dev/null +++ b/vendor/tauri-runtime-wry/src/undecorated_resizing.rs @@ -0,0 +1,637 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] + +const CLIENT: isize = 0b0000; +const LEFT: isize = 0b0001; +const RIGHT: isize = 0b0010; +const TOP: isize = 0b0100; +const BOTTOM: isize = 0b1000; +const TOPLEFT: isize = TOP | LEFT; +const TOPRIGHT: isize = TOP | RIGHT; +const BOTTOMLEFT: isize = BOTTOM | LEFT; +const BOTTOMRIGHT: isize = BOTTOM | RIGHT; + +#[cfg(not(windows))] +pub use self::gtk::*; +#[cfg(windows)] +pub use self::windows::*; + +#[cfg(windows)] +type WindowPositions = i32; +#[cfg(not(windows))] +type WindowPositions = f64; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum HitTestResult { + Client, + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + NoWhere, +} + +#[allow(clippy::too_many_arguments)] +fn hit_test( + left: WindowPositions, + top: WindowPositions, + right: WindowPositions, + bottom: WindowPositions, + cx: WindowPositions, + cy: WindowPositions, + border_x: WindowPositions, + border_y: WindowPositions, +) -> HitTestResult { + #[rustfmt::skip] + let result = (LEFT * (cx < left + border_x) as isize) + | (RIGHT * (cx >= right - border_x) as isize) + | (TOP * (cy < top + border_y) as isize) + | (BOTTOM * (cy >= bottom - border_y) as isize); + + match result { + CLIENT => HitTestResult::Client, + LEFT => HitTestResult::Left, + RIGHT => HitTestResult::Right, + TOP => HitTestResult::Top, + BOTTOM => HitTestResult::Bottom, + TOPLEFT => HitTestResult::TopLeft, + TOPRIGHT => HitTestResult::TopRight, + BOTTOMLEFT => HitTestResult::BottomLeft, + BOTTOMRIGHT => HitTestResult::BottomRight, + _ => HitTestResult::NoWhere, + } +} + +#[cfg(windows)] +mod windows { + use crate::util; + + use super::{hit_test, HitTestResult}; + + use windows::core::*; + use windows::Win32::System::LibraryLoader::*; + use windows::Win32::UI::WindowsAndMessaging::*; + use windows::Win32::{Foundation::*, UI::Shell::SetWindowSubclass}; + use windows::Win32::{Graphics::Gdi::*, UI::Shell::DefSubclassProc}; + + impl HitTestResult { + fn to_win32(self) -> i32 { + match self { + HitTestResult::Left => HTLEFT as _, + HitTestResult::Right => HTRIGHT as _, + HitTestResult::Top => HTTOP as _, + HitTestResult::Bottom => HTBOTTOM as _, + HitTestResult::TopLeft => HTTOPLEFT as _, + HitTestResult::TopRight => HTTOPRIGHT as _, + HitTestResult::BottomLeft => HTBOTTOMLEFT as _, + HitTestResult::BottomRight => HTBOTTOMRIGHT as _, + _ => HTTRANSPARENT, + } + } + } + + const CLASS_NAME: PCWSTR = w!("TAURI_DRAG_RESIZE_BORDERS"); + const WINDOW_NAME: PCWSTR = w!("TAURI_DRAG_RESIZE_WINDOW"); + + const WM_UPDATE_UNDECORATED_SHADOWS: u32 = WM_USER + 100; + + struct UndecoratedResizingData { + child: HWND, + has_undecorated_shadows: bool, + } + + pub fn attach_resize_handler(hwnd: isize, has_undecorated_shadows: bool) { + let parent = HWND(hwnd as _); + + // return early if we already attached + if unsafe { FindWindowExW(Some(parent), None, CLASS_NAME, WINDOW_NAME) }.is_ok() { + return; + } + + let class = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: WNDCLASS_STYLES::default(), + lpfnWndProc: Some(drag_resize_window_proc), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: unsafe { HINSTANCE(GetModuleHandleW(PCWSTR::null()).unwrap_or_default().0) }, + hIcon: HICON::default(), + hCursor: HCURSOR::default(), + hbrBackground: HBRUSH::default(), + lpszMenuName: PCWSTR::null(), + lpszClassName: CLASS_NAME, + hIconSm: HICON::default(), + }; + + unsafe { RegisterClassExW(&class) }; + + let mut rect = RECT::default(); + unsafe { GetClientRect(parent, &mut rect).unwrap() }; + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + let data = UndecoratedResizingData { + child: HWND::default(), + has_undecorated_shadows, + }; + + let Ok(drag_window) = (unsafe { + CreateWindowExW( + WINDOW_EX_STYLE::default(), + CLASS_NAME, + WINDOW_NAME, + WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS, + 0, + 0, + width, + height, + Some(parent), + None, + GetModuleHandleW(None).ok().map(|h| HINSTANCE(h.0)), + Some(Box::into_raw(Box::new(data)) as _), + ) + }) else { + return; + }; + + unsafe { + set_drag_hwnd_rgn(drag_window, width, height, has_undecorated_shadows); + + let _ = SetWindowPos( + drag_window, + Some(HWND_TOP), + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOSIZE, + ); + + let data = UndecoratedResizingData { + child: drag_window, + has_undecorated_shadows, + }; + + let _ = SetWindowSubclass( + parent, + Some(subclass_parent), + (WM_USER + 1) as _, + Box::into_raw(Box::new(data)) as _, + ); + } + } + + unsafe extern "system" fn subclass_parent( + parent: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _: usize, + data: usize, + ) -> LRESULT { + match msg { + WM_SIZE => { + let data = data as *mut UndecoratedResizingData; + let data = &*data; + let child = data.child; + let has_undecorated_shadows = data.has_undecorated_shadows; + + // when parent is maximized, remove the undecorated window drag resize region + if is_maximized(parent).unwrap_or(false) { + let _ = SetWindowPos( + child, + Some(HWND_TOP), + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOMOVE, + ); + } else { + // otherwise updat the cutout region + let mut rect = RECT::default(); + if GetClientRect(parent, &mut rect).is_ok() { + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + let _ = SetWindowPos( + child, + Some(HWND_TOP), + 0, + 0, + width, + height, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOMOVE, + ); + + set_drag_hwnd_rgn(child, width, height, has_undecorated_shadows); + } + } + } + + WM_UPDATE_UNDECORATED_SHADOWS => { + let data = data as *mut UndecoratedResizingData; + let data = &mut *data; + data.has_undecorated_shadows = wparam.0 != 0; + } + + WM_NCDESTROY => { + let data = data as *mut UndecoratedResizingData; + drop(Box::from_raw(data)); + } + + _ => {} + } + + DefSubclassProc(parent, msg, wparam, lparam) + } + + unsafe extern "system" fn drag_resize_window_proc( + child: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + match msg { + WM_CREATE => { + let data = lparam.0 as *mut CREATESTRUCTW; + let data = (*data).lpCreateParams as *mut UndecoratedResizingData; + (*data).child = child; + SetWindowLongPtrW(child, GWLP_USERDATA, data as _); + } + + WM_NCHITTEST => { + let data = GetWindowLongPtrW(child, GWLP_USERDATA); + let data = &*(data as *mut UndecoratedResizingData); + + let Ok(parent) = GetParent(child) else { + return DefWindowProcW(child, msg, wparam, lparam); + }; + let style = GetWindowLongPtrW(parent, GWL_STYLE); + let style = WINDOW_STYLE(style as u32); + + let is_resizable = (style & WS_SIZEBOX).0 != 0; + if !is_resizable { + return DefWindowProcW(child, msg, wparam, lparam); + } + + // if the window has undecorated shadows, + // it should always be the top border, + // ensured by the cutout drag window + if data.has_undecorated_shadows { + return LRESULT(HTTOP as _); + } + + let mut rect = RECT::default(); + if GetWindowRect(child, &mut rect).is_err() { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let (cx, cy) = (GET_X_LPARAM(lparam) as i32, GET_Y_LPARAM(lparam) as i32); + + let dpi = unsafe { util::hwnd_dpi(child) }; + let border_x = util::get_system_metrics_for_dpi(SM_CXFRAME, dpi); + let border_y = util::get_system_metrics_for_dpi(SM_CYFRAME, dpi); + + let res = hit_test( + rect.left, + rect.top, + rect.right, + rect.bottom, + cx, + cy, + border_x, + border_y, + ); + + return LRESULT(res.to_win32() as _); + } + + WM_NCLBUTTONDOWN => { + let data = GetWindowLongPtrW(child, GWLP_USERDATA); + let data = &*(data as *mut UndecoratedResizingData); + + let Ok(parent) = GetParent(child) else { + return DefWindowProcW(child, msg, wparam, lparam); + }; + let style = GetWindowLongPtrW(parent, GWL_STYLE); + let style = WINDOW_STYLE(style as u32); + + let is_resizable = (style & WS_SIZEBOX).0 != 0; + if !is_resizable { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let (cx, cy) = (GET_X_LPARAM(lparam) as i32, GET_Y_LPARAM(lparam) as i32); + + // if the window has undecorated shadows, + // it should always be the top border, + // ensured by the cutout drag window + let res = if data.has_undecorated_shadows { + HitTestResult::Top + } else { + let mut rect = RECT::default(); + if GetWindowRect(child, &mut rect).is_err() { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let dpi = unsafe { util::hwnd_dpi(child) }; + let border_x = util::get_system_metrics_for_dpi(SM_CXFRAME, dpi); + let border_y = util::get_system_metrics_for_dpi(SM_CYFRAME, dpi); + + hit_test( + rect.left, + rect.top, + rect.right, + rect.bottom, + cx, + cy, + border_x, + border_y, + ) + }; + + if res != HitTestResult::NoWhere { + let points = POINTS { + x: cx as i16, + y: cy as i16, + }; + + let _ = PostMessageW( + Some(parent), + WM_NCLBUTTONDOWN, + WPARAM(res.to_win32() as _), + LPARAM(&points as *const _ as _), + ); + } + + return LRESULT(0); + } + + WM_UPDATE_UNDECORATED_SHADOWS => { + let data = GetWindowLongPtrW(child, GWLP_USERDATA); + let data = &mut *(data as *mut UndecoratedResizingData); + data.has_undecorated_shadows = wparam.0 != 0; + } + + WM_NCDESTROY => { + let data = GetWindowLongPtrW(child, GWLP_USERDATA); + let data = data as *mut UndecoratedResizingData; + drop(Box::from_raw(data)); + } + + _ => {} + } + + DefWindowProcW(child, msg, wparam, lparam) + } + + pub fn detach_resize_handler(hwnd: isize) { + let hwnd = HWND(hwnd as _); + + let Ok(child) = (unsafe { FindWindowExW(Some(hwnd), None, CLASS_NAME, WINDOW_NAME) }) else { + return; + }; + + let _ = unsafe { DestroyWindow(child) }; + } + + unsafe fn set_drag_hwnd_rgn(hwnd: HWND, width: i32, height: i32, only_top: bool) { + // The window used for drag resizing an undecorated window + // is a child HWND that covers the whole window + // and so we need create a cut out in the middle for the parent and other child + // windows like the webview can receive mouse events. + + let dpi = unsafe { util::hwnd_dpi(hwnd) }; + let border_x = util::get_system_metrics_for_dpi(SM_CXFRAME, dpi); + let border_y = util::get_system_metrics_for_dpi(SM_CYFRAME, dpi); + + // hrgn1 must be mutable to call .free() later + let mut hrgn1 = CreateRectRgn(0, 0, width, height); + + let x1 = if only_top { 0 } else { border_x }; + let y1 = border_y; + let x2 = if only_top { width } else { width - border_x }; + let y2 = if only_top { height } else { height - border_y }; + + // Wrap hrgn2 in Owned so it is automatically freed when going out of scope + let hrgn2 = Owned::new(CreateRectRgn(x1, y1, x2, y2)); + + CombineRgn(Some(hrgn1), Some(hrgn1), Some(*hrgn2), RGN_DIFF); + + // Try to set the window region + if SetWindowRgn(hwnd, Some(hrgn1), true) == 0 { + // If it fails, we must free hrgn1 manually + hrgn1.free(); + } + } + + pub fn update_drag_hwnd_rgn_for_undecorated(hwnd: isize, has_undecorated_shadows: bool) { + let hwnd = HWND(hwnd as _); + + let mut rect = RECT::default(); + let Ok(_) = (unsafe { GetClientRect(hwnd, &mut rect) }) else { + return; + }; + + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + let Ok(child) = (unsafe { FindWindowExW(Some(hwnd), None, CLASS_NAME, WINDOW_NAME) }) else { + return; + }; + + unsafe { set_drag_hwnd_rgn(child, width, height, has_undecorated_shadows) }; + + unsafe { + SendMessageW( + hwnd, + WM_UPDATE_UNDECORATED_SHADOWS, + None, + Some(LPARAM(has_undecorated_shadows as _)), + ) + }; + unsafe { + SendMessageW( + child, + WM_UPDATE_UNDECORATED_SHADOWS, + None, + Some(LPARAM(has_undecorated_shadows as _)), + ) + }; + } + + fn is_maximized(window: HWND) -> windows::core::Result { + let mut placement = WINDOWPLACEMENT { + length: std::mem::size_of::() as u32, + ..WINDOWPLACEMENT::default() + }; + unsafe { GetWindowPlacement(window, &mut placement)? }; + Ok(placement.showCmd == SW_MAXIMIZE.0 as u32) + } + + /// Implementation of the `GET_X_LPARAM` macro. + #[allow(non_snake_case)] + #[inline] + fn GET_X_LPARAM(lparam: LPARAM) -> i16 { + ((lparam.0 as usize) & 0xFFFF) as u16 as i16 + } + + /// Implementation of the `GET_Y_LPARAM` macro. + #[allow(non_snake_case)] + #[inline] + fn GET_Y_LPARAM(lparam: LPARAM) -> i16 { + (((lparam.0 as usize) & 0xFFFF_0000) >> 16) as u16 as i16 + } +} + +#[cfg(not(windows))] +mod gtk { + use super::{hit_test, HitTestResult}; + + const BORDERLESS_RESIZE_INSET: i32 = 5; + + impl HitTestResult { + fn to_gtk_edge(self) -> gtk::gdk::WindowEdge { + match self { + HitTestResult::Client | HitTestResult::NoWhere => gtk::gdk::WindowEdge::__Unknown(0), + HitTestResult::Left => gtk::gdk::WindowEdge::West, + HitTestResult::Right => gtk::gdk::WindowEdge::East, + HitTestResult::Top => gtk::gdk::WindowEdge::North, + HitTestResult::Bottom => gtk::gdk::WindowEdge::South, + HitTestResult::TopLeft => gtk::gdk::WindowEdge::NorthWest, + HitTestResult::TopRight => gtk::gdk::WindowEdge::NorthEast, + HitTestResult::BottomLeft => gtk::gdk::WindowEdge::SouthWest, + HitTestResult::BottomRight => gtk::gdk::WindowEdge::SouthEast, + } + } + } + + pub fn attach_resize_handler(webview: &wry::WebView) { + use gtk::{ + gdk::{prelude::*, WindowEdge}, + glib::Propagation, + prelude::*, + }; + use wry::WebViewExtUnix; + + let webview = webview.webview(); + + webview.add_events( + gtk::gdk::EventMask::BUTTON1_MOTION_MASK + | gtk::gdk::EventMask::BUTTON_PRESS_MASK + | gtk::gdk::EventMask::TOUCH_MASK, + ); + + webview.connect_button_press_event( + move |webview: &webkit2gtk::WebView, event: >k::gdk::EventButton| { + if event.button() == 1 { + // Custom Linux shells may place the WebView inside GtkOverlay (for + // example, over a GtkGLArea used by libmpv). Walk the hierarchy + // instead of assuming WebView -> GtkBox -> GtkWindow. + let mut ancestor = webview.parent(); + let mut gtk_window = None; + while let Some(widget) = ancestor { + if let Ok(window) = widget.clone().downcast::() { + gtk_window = Some(window); + break; + } + ancestor = widget.parent(); + } + if let Some(window) = gtk_window { + if !window.is_decorated() && window.is_resizable() && !window.is_maximized() { + if let Some(window) = window.window() { + let (root_x, root_y) = event.root(); + let (window_x, window_y) = window.position(); + let (client_x, client_y) = (root_x - window_x as f64, root_y - window_y as f64); + let border = window.scale_factor() * BORDERLESS_RESIZE_INSET; + let edge = hit_test( + 0.0, + 0.0, + window.width() as f64, + window.height() as f64, + client_x, + client_y, + border as _, + border as _, + ) + .to_gtk_edge(); + + // we ignore the `__Unknown` variant so the webview receives the click correctly if it is not on the edges. + match edge { + WindowEdge::__Unknown(_) => (), + _ => { + window.begin_resize_drag(edge, 1, root_x as i32, root_y as i32, event.time()) + } + } + } + } + } + } + + Propagation::Proceed + }, + ); + + webview.connect_touch_event( + move |webview: &webkit2gtk::WebView, event: >k::gdk::Event| { + // This one should be GtkBox + if let Some(window) = webview.parent().and_then(|w| w.parent()) { + // Safe to unwrap unless this is not from tao + let window: gtk::Window = window.downcast().unwrap(); + if !window.is_decorated() && window.is_resizable() && !window.is_maximized() { + if let Some(window) = window.window() { + if let Some((root_x, root_y)) = event.root_coords() { + if let Some(device) = event.device() { + let (window_x, window_y) = window.position(); + let (client_x, client_y) = (root_x - window_x as f64, root_y - window_y as f64); + let border = window.scale_factor() * BORDERLESS_RESIZE_INSET; + let edge = hit_test( + 0.0, + 0.0, + window.width() as f64, + window.height() as f64, + client_x, + client_y, + border as _, + border as _, + ) + .to_gtk_edge(); + + // we ignore the `__Unknown` variant so the window receives the click correctly if it is not on the edges. + match edge { + WindowEdge::__Unknown(_) => (), + _ => window.begin_resize_drag_for_device( + edge, + &device, + 0, + root_x as i32, + root_y as i32, + event.time(), + ), + } + } + } + } + } + } + + Propagation::Proceed + }, + ); + } +} diff --git a/vendor/tauri-runtime-wry/src/util.rs b/vendor/tauri-runtime-wry/src/util.rs new file mode 100644 index 0000000..e5369cf --- /dev/null +++ b/vendor/tauri-runtime-wry/src/util.rs @@ -0,0 +1,120 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg_attr(not(windows), allow(unused_imports))] +pub use imp::*; + +#[cfg(not(windows))] +mod imp {} + +#[cfg(windows)] +mod imp { + use std::{iter::once, os::windows::ffi::OsStrExt}; + + use once_cell::sync::Lazy; + use windows::{ + core::{HRESULT, PCSTR, PCWSTR}, + Win32::{ + Foundation::*, + Graphics::Gdi::*, + System::LibraryLoader::{GetProcAddress, LoadLibraryW}, + UI::{HiDpi::*, WindowsAndMessaging::*}, + }, + }; + + pub fn encode_wide(string: impl AsRef) -> Vec { + string.as_ref().encode_wide().chain(once(0)).collect() + } + + // Helper function to dynamically load function pointer. + // `library` and `function` must be zero-terminated. + pub(super) fn get_function_impl(library: &str, function: &str) -> FARPROC { + let library = encode_wide(library); + assert_eq!(function.chars().last(), Some('\0')); + + // Library names we will use are ASCII so we can use the A version to avoid string conversion. + let module = unsafe { LoadLibraryW(PCWSTR::from_raw(library.as_ptr())) }.unwrap_or_default(); + if module.is_invalid() { + return None; + } + + unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) } + } + + macro_rules! get_function { + ($lib:expr, $func:ident) => { + $crate::util::get_function_impl($lib, concat!(stringify!($func), '\0')) + .map(|f| unsafe { std::mem::transmute::<_, $func>(f) }) + }; + } + + type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; + type GetDpiForMonitor = unsafe extern "system" fn( + hmonitor: HMONITOR, + dpi_type: MONITOR_DPI_TYPE, + dpi_x: *mut u32, + dpi_y: *mut u32, + ) -> HRESULT; + type GetSystemMetricsForDpi = + unsafe extern "system" fn(nindex: SYSTEM_METRICS_INDEX, dpi: u32) -> i32; + + static GET_DPI_FOR_WINDOW: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); + static GET_DPI_FOR_MONITOR: Lazy> = + Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor)); + static GET_SYSTEM_METRICS_FOR_DPI: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetSystemMetricsForDpi)); + + #[allow(non_snake_case)] + pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { + if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { + // We are on Windows 10 Anniversary Update (1607) or later. + match GetDpiForWindow(hwnd) { + 0 => USER_DEFAULT_SCREEN_DPI, // 0 is returned if hwnd is invalid + dpi => dpi, + } + } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if monitor.is_invalid() { + return USER_DEFAULT_SCREEN_DPI; + } + + let mut dpi_x = 0; + let mut dpi_y = 0; + if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y).is_ok() { + dpi_x + } else { + USER_DEFAULT_SCREEN_DPI + } + } else { + // We are on Vista or later. + if IsProcessDPIAware().as_bool() { + let hdc = GetDC(Some(hwnd)); + if hdc.is_invalid() { + return USER_DEFAULT_SCREEN_DPI; + } + // If the process is DPI aware, then scaling must be handled by the application using + // this DPI value. + let dpi = GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32; + ReleaseDC(Some(hwnd), hdc); + dpi + } else { + // If the process is DPI unaware, then scaling is performed by the OS; we thus return + // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the + // application and the WM. + USER_DEFAULT_SCREEN_DPI + } + } + } + + #[allow(non_snake_case)] + pub unsafe fn get_system_metrics_for_dpi(nindex: SYSTEM_METRICS_INDEX, dpi: u32) -> i32 { + if let Some(GetSystemMetricsForDpi) = *GET_SYSTEM_METRICS_FOR_DPI { + GetSystemMetricsForDpi(nindex, dpi) + } else { + GetSystemMetrics(nindex) + } + } +} diff --git a/vendor/tauri-runtime-wry/src/webview.rs b/vendor/tauri-runtime-wry/src/webview.rs new file mode 100644 index 0000000..67b6073 --- /dev/null +++ b/vendor/tauri-runtime-wry/src/webview.rs @@ -0,0 +1,47 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod imp { + pub type Webview = webkit2gtk::WebView; +} + +#[cfg(target_vendor = "apple")] +mod imp { + use std::ffi::c_void; + + pub struct Webview { + pub webview: *mut c_void, + pub manager: *mut c_void, + #[cfg(target_os = "macos")] + pub ns_window: *mut c_void, + #[cfg(target_os = "ios")] + pub view_controller: *mut c_void, + } +} + +#[cfg(windows)] +mod imp { + use webview2_com::Microsoft::Web::WebView2::Win32::{ + ICoreWebView2Controller, ICoreWebView2Environment, + }; + pub struct Webview { + pub controller: ICoreWebView2Controller, + pub environment: ICoreWebView2Environment, + } +} + +#[cfg(target_os = "android")] +mod imp { + use wry::JniHandle; + pub type Webview = JniHandle; +} + +pub use imp::*; diff --git a/vendor/tauri-runtime-wry/src/window/linux.rs b/vendor/tauri-runtime-wry/src/window/linux.rs new file mode 100644 index 0000000..e7ee927 --- /dev/null +++ b/vendor/tauri-runtime-wry/src/window/linux.rs @@ -0,0 +1,31 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use gtk::prelude::*; +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use tao::platform::unix::WindowExtUnix; + +impl super::WindowExt for tao::window::Window { + fn set_enabled(&self, enabled: bool) { + self.gtk_window().set_sensitive(enabled); + } + + fn is_enabled(&self) -> bool { + self.gtk_window().is_sensitive() + } + + fn center(&self) { + if let Some(monitor) = self.current_monitor() { + let window_size = self.outer_size(); + let new_pos = super::calculate_window_center_position(window_size, monitor); + self.set_outer_position(new_pos); + } + } +} diff --git a/vendor/tauri-runtime-wry/src/window/macos.rs b/vendor/tauri-runtime-wry/src/window/macos.rs new file mode 100644 index 0000000..6bb16ce --- /dev/null +++ b/vendor/tauri-runtime-wry/src/window/macos.rs @@ -0,0 +1,43 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use objc2::MainThreadMarker; +use objc2_app_kit::{NSBackingStoreType, NSWindow, NSWindowStyleMask}; +use tao::platform::macos::WindowExtMacOS; + +impl super::WindowExt for tao::window::Window { + // based on electron implementation + // https://github.com/electron/electron/blob/15db63e26df3e3d59ce6281f030624f746518511/shell/browser/native_window_mac.mm#L474 + fn set_enabled(&self, enabled: bool) { + let ns_window: &NSWindow = unsafe { &*self.ns_window().cast() }; + if !enabled { + let frame = ns_window.frame(); + let mtm = MainThreadMarker::new() + .expect("`Window::set_enabled` can only be called on the main thread"); + let sheet = unsafe { + NSWindow::initWithContentRect_styleMask_backing_defer( + mtm.alloc(), + frame, + NSWindowStyleMask::Titled, + NSBackingStoreType::Buffered, + false, + ) + }; + unsafe { sheet.setAlphaValue(0.5) }; + unsafe { ns_window.beginSheet_completionHandler(&sheet, None) }; + } else if let Some(attached) = unsafe { ns_window.attachedSheet() } { + unsafe { ns_window.endSheet(&attached) }; + } + } + + fn is_enabled(&self) -> bool { + let ns_window: &NSWindow = unsafe { &*self.ns_window().cast() }; + unsafe { ns_window.attachedSheet() }.is_none() + } + + fn center(&self) { + let ns_window: &NSWindow = unsafe { &*self.ns_window().cast() }; + ns_window.center(); + } +} diff --git a/vendor/tauri-runtime-wry/src/window/mod.rs b/vendor/tauri-runtime-wry/src/window/mod.rs new file mode 100644 index 0000000..b18543e --- /dev/null +++ b/vendor/tauri-runtime-wry/src/window/mod.rs @@ -0,0 +1,72 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; + +use crate::monitor::MonitorExt; + +pub trait WindowExt { + /// Enable or disable the window + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Unsupported. + fn set_enabled(&self, enabled: bool); + + /// Whether the window is enabled or disabled. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Unsupported, always returns `true`. + fn is_enabled(&self) -> bool; + + /// Center the window + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Unsupported. + fn center(&self) {} + + /// Clears the window surface. i.e make it transparent. + #[cfg(windows)] + fn draw_surface( + &self, + surface: &mut softbuffer::Surface< + std::sync::Arc, + std::sync::Arc, + >, + background_color: Option, + ); +} + +#[cfg(mobile)] +impl WindowExt for tao::window::Window { + fn set_enabled(&self, _: bool) {} + fn is_enabled(&self) -> bool { + true + } +} + +pub fn calculate_window_center_position( + window_size: tao::dpi::PhysicalSize, + target_monitor: tao::monitor::MonitorHandle, +) -> tao::dpi::PhysicalPosition { + let work_area = target_monitor.work_area(); + + tao::dpi::PhysicalPosition::new( + (work_area.size.width as i32 - window_size.width as i32) / 2 + work_area.position.x, + (work_area.size.height as i32 - window_size.height as i32) / 2 + work_area.position.y, + ) +} diff --git a/vendor/tauri-runtime-wry/src/window/windows.rs b/vendor/tauri-runtime-wry/src/window/windows.rs new file mode 100644 index 0000000..6c032be --- /dev/null +++ b/vendor/tauri-runtime-wry/src/window/windows.rs @@ -0,0 +1,68 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use windows::Win32::{ + Foundation::{HWND, RECT}, + Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_EXTENDED_FRAME_BOUNDS}, + UI::Input::KeyboardAndMouse::{EnableWindow, IsWindowEnabled}, +}; + +use tao::platform::windows::WindowExtWindows; + +impl super::WindowExt for tao::window::Window { + fn set_enabled(&self, enabled: bool) { + let _ = unsafe { EnableWindow(HWND(self.hwnd() as _), enabled) }; + } + + fn is_enabled(&self) -> bool { + unsafe { IsWindowEnabled(HWND(self.hwnd() as _)) }.as_bool() + } + + fn center(&self) { + if let Some(monitor) = self.current_monitor() { + let mut window_size = self.outer_size(); + + if self.is_decorated() { + let mut rect = RECT::default(); + let result = unsafe { + DwmGetWindowAttribute( + HWND(self.hwnd() as _), + DWMWA_EXTENDED_FRAME_BOUNDS, + &mut rect as *mut _ as *mut _, + std::mem::size_of::() as u32, + ) + }; + if result.is_ok() { + window_size.height = (rect.bottom - rect.top) as u32; + } + } + + let new_pos = super::calculate_window_center_position(window_size, monitor); + self.set_outer_position(new_pos); + } + } + + fn draw_surface( + &self, + surface: &mut softbuffer::Surface< + std::sync::Arc, + std::sync::Arc, + >, + background_color: Option, + ) { + let size = self.inner_size(); + if let (Some(width), Some(height)) = ( + std::num::NonZeroU32::new(size.width), + std::num::NonZeroU32::new(size.height), + ) { + surface.resize(width, height).unwrap(); + let mut buffer = surface.buffer_mut().unwrap(); + let color = background_color + .map(|(r, g, b, _)| (b as u32) | ((g as u32) << 8) | ((r as u32) << 16)) + .unwrap_or(0); + buffer.fill(color); + let _ = buffer.present(); + } + } +}