EG/core/imgui_webview.py

226 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
imgui_webview.py
后台 playwright 无头浏览器 + 截图 → Panda3D 纹理,供 ImGui 面板显示。
"""
from __future__ import annotations
import threading
import io
import time
class ImGuiWebView:
"""
后台线程运行 playwright Chromium定期截图并转换为 Panda3D 纹理。
ImGui 直接用 imgui.image() 显示纹理,鼠标/滚轮事件转发给浏览器。
"""
def __init__(self, width: int = 1280, height: int = 720):
self.view_width = width
self.view_height = height
# 截图数据bytes
self._screenshot: bytes | None = None
self._screenshot_lock = threading.Lock()
self.tex_dirty = False # 有新截图待上传 GPU
# 状态
self.current_url = ""
self.title = ""
self.is_loading = False
self.error: str | None = None
# 待处理指令(由 ImGui 线程写,浏览器线程读)
self._cmd_navigate: str | None = None
self._cmd_click: tuple | None = None # (x_ratio, y_ratio)
self._cmd_scroll: float | None = None # pixels
self._cmd_back = False
self._cmd_forward = False
self._cmd_reload = False
self._lock = threading.Lock()
self._running = False
self._thread: threading.Thread | None = None
# ------------------------------------------------------------------ #
# 公开控制 API由 ImGui 线程调用,线程安全)
# ------------------------------------------------------------------ #
def start(self, url: str):
if self._running:
return
self._running = True
self._cmd_navigate = url
self._thread = threading.Thread(target=self._run, daemon=True,
name="imgui-webview")
self._thread.start()
def stop(self):
self._running = False
def navigate(self, url: str):
if not url.startswith(('http://', 'https://', 'file://')):
url = 'https://' + url
with self._lock:
self._cmd_navigate = url
self.is_loading = True
def click(self, x_ratio: float, y_ratio: float):
with self._lock:
self._cmd_click = (x_ratio, y_ratio)
def scroll(self, delta_px: float):
with self._lock:
self._cmd_scroll = delta_px
def go_back(self):
with self._lock:
self._cmd_back = True
def go_forward(self):
with self._lock:
self._cmd_forward = True
def reload(self):
with self._lock:
self._cmd_reload = True
def get_screenshot_bytes(self) -> bytes | None:
with self._screenshot_lock:
return self._screenshot
# ------------------------------------------------------------------ #
# 内部线程
# ------------------------------------------------------------------ #
def _run(self):
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(self._async_run())
except Exception as exc:
self.error = f"WebView 线程异常: {exc}"
import traceback; traceback.print_exc()
finally:
loop.close()
async def _async_run(self):
try:
from playwright.async_api import async_playwright
except ImportError:
self.error = (
"playwright 未安装。\n"
"请运行: pip install playwright\n"
"然后运行: playwright install chromium"
)
self._running = False
return
try:
async with async_playwright() as pw:
browser = await pw.chromium.launch(headless=True)
ctx = await browser.new_context(
viewport={"width": self.view_width,
"height": self.view_height},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/121.0.0.0 Safari/537.36"
),
)
page = await ctx.new_page()
# 初次导航
start_url = self._cmd_navigate or "about:blank"
self._cmd_navigate = None
await self._goto(page, start_url)
await self._snap(page)
# 主循环
import asyncio
while self._running:
await asyncio.sleep(0.05)
with self._lock:
nav_url = self._cmd_navigate; self._cmd_navigate = None
clk = self._cmd_click; self._cmd_click = None
scr = self._cmd_scroll; self._cmd_scroll = None
do_back = self._cmd_back; self._cmd_back = False
do_fwd = self._cmd_forward; self._cmd_forward = False
do_reload = self._cmd_reload; self._cmd_reload = False
changed = False
if nav_url:
self.is_loading = True
await self._goto(page, nav_url)
changed = True
self.is_loading = False
if do_back:
await page.go_back()
await asyncio.sleep(0.5)
changed = True
if do_fwd:
await page.go_forward()
await asyncio.sleep(0.5)
changed = True
if do_reload:
await page.reload(wait_until="domcontentloaded")
changed = True
if clk is not None:
xr, yr = clk
x = int(xr * self.view_width)
y = int(yr * self.view_height)
await page.mouse.click(x, y)
await asyncio.sleep(0.4)
changed = True
if scr is not None:
await page.evaluate(
f"window.scrollBy(0, {int(scr)})"
)
await asyncio.sleep(0.15)
changed = True
if changed:
self.current_url = page.url
try:
self.title = await page.title()
except Exception:
pass
await self._snap(page)
await browser.close()
except Exception as exc:
self.error = str(exc)
import traceback; traceback.print_exc()
finally:
self._running = False
async def _goto(self, page, url: str):
import asyncio
try:
await page.goto(url, wait_until="domcontentloaded", timeout=20_000)
self.current_url = page.url
try:
self.title = await page.title()
except Exception:
pass
except Exception as exc:
print(f"[WebView] 导航失败 {url}: {exc}")
async def _snap(self, page):
"""截图并更新 self._screenshot"""
try:
data = await page.screenshot(type="png", full_page=False)
with self._screenshot_lock:
self._screenshot = data
self.tex_dirty = True
except Exception as exc:
print(f"[WebView] 截图失败: {exc}")