Best way to get remote imgui for python? #416
Replies: 3 comments 5 replies
-
|
Hum, remoting is a complex subject. It only works in C++ at the moment, and its support is experimental within Hello ImGui, even in C++. |
Beta Was this translation helpful? Give feedback.
-
|
Well done! I had explored remoting inside helloimgui, prior to having a pyodide version. I had worked on providing a way to use imgui-ws from hello imgui. See However I am interested in your use case and in seeing your progress, since it might be useful when heavy competition is required on the server side. Transfering the font texture needs to be done at the backend level I’d say and it should probably follow the new texture API introduced in ImGui V 1.92 Can you tell me more about the stack you are using?
Thanks |
Beta Was this translation helpful? Give feedback.
-
|
it's pretty basic at the moment: server.py =execute with=> uv run server.py#!/usr/bin/env uv run
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "aiohttp",
# "imgui_bundle",
# "msgspec",
# "janus"
# ]
# ///
from serialize import PyImDrawData,replay_draw_data
import asyncio
import janus
from aiohttp import web
from imgui_bundle import imgui, immapp, hello_imgui
dQ = janus.Queue()
# ---------- WebSocket handler ----------
async def websocket_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
while True:
mybytes = await dQ.async_q.get()
await ws.send_bytes(mybytes,compress=None)
#await asyncio.sleep(0.1) # send every second
return ws
# ---------- HTTP server serving index.html ----------
async def index(request):
return web.FileResponse("index.html")
# ---------- Build and run app ----------
app = web.Application()
app.router.add_get("/", index)
app.router.add_get("/ws", websocket_handler)
# Serve index.html and allow file access
app.router.add_static("/", ".")
app.router.add_resource("/serialize.py")
# ---------- Gui stuff ------------
from imgui_bundle import implot, imgui, immapp, hello_imgui, icons_fontawesome_4
import time
import numpy as np
# Fill x and y whose plot is a heart
vals = np.arange(0, np.pi * 2, 0.01)
x = np.power(np.sin(vals), 3) * 16
y = 13 * np.cos(vals) - 5 * np.cos(2 * vals) - 2 * np.cos(3 * vals) - np.cos(4 * vals)
# Heart pulse rate and time tracking
phase = 0.0
t0 = time.time() + 0.2
heart_pulse_rate = 80
def gui():
global heart_pulse_rate, phase, t0, x, y
imgui.text("Made with " + icons_fontawesome_4.ICON_FA_HEART + " using Dear ImGui and ImPlot")
imgui.text(f"Running at {hello_imgui.frame_rate():.1f} FPS")
t = time.time()
phase += (t - t0) * heart_pulse_rate / (np.pi * 2)
k = 0.8 + 0.1 * np.cos(phase)
t0 = t
implot.begin_plot("Heart", immapp.em_to_vec2(21, 21))
implot.plot_line("", x * k, y * k)
implot.end_plot()
imgui.set_next_item_width(hello_imgui.em_size(10))
_, heart_pulse_rate = imgui.slider_float("Pulse", heart_pulse_rate, 30, 180)
def pre_imgui_callback():
dd = imgui.get_draw_data()
if(dQ.sync_q.empty()):
data = PyImDrawData.from_imgui(dd).to_bytes()
print(f"FILLED {len(data)} bytes")
dQ.sync_q.put_nowait(data)
callbacks = hello_imgui.RunnerCallbacks(
show_gui = gui,
after_swap = pre_imgui_callback,
)
runner = immapp.RunnerParams(
callbacks=callbacks,
)
addons = immapp.AddOnsParams(
with_implot=True,
)
# ---------- Run the stuff -------------
taskA = asyncio.to_thread(
immapp.run,
runner_params=runner,
add_ons_params = addons,
)
taskB = asyncio.to_thread(
web.run_app,app, host="0.0.0.0", port=8000
)
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(taskA)
# tg.create_task(taskB)
if __name__ == '__main__':
asyncio.run(main())Index.html:And a poorly slubbed together glue:serialize.py from __future__ import annotations
from typing import List, Tuple
import msgspec
from imgui_bundle import imgui
# ------------------------------------------------------------------
# The One True Class — clean, fast, websocket-ready
# ------------------------------------------------------------------
class PyImDrawData(msgspec.Struct, array_like=True):
total_vtx_count: int
total_idx_count: int
cmd_lists_count: int
# Nested structs — msgspec handles deep encoding automatically
class Vert(msgspec.Struct, array_like=True):
pos_x: float
pos_y: float
uv_x: float
uv_y: float
col: int # ARGB u32
class Cmd(msgspec.Struct, array_like=True):
elem_count: int
clip_x1: float
clip_y1: float
clip_x2: float
clip_y2: float
texture_id: int # u32
class imList(msgspec.Struct, array_like=True):
vertices: List[PyImDrawData.Vert]
indices: List[int] # auto 16/32-bit in real renderer, but we store as int
commands: List[PyImDrawData.Cmd]
lists: List[imList]
# ------------------------------------------------------------------
# Classmethods — clean API
# ------------------------------------------------------------------
@classmethod
def from_imgui(cls, dd: imgui.ImDrawData) -> "PyImDrawData":
"""Convert real ImDrawData → pure Python msgspec-ready structure"""
lists = []
for draw_list in dd.cmd_lists:
# Vertices
verts = []
for v in draw_list.vtx_buffer:
pos = v.pos
uv = v.uv
verts.append(cls.Vert(
pos_x=float(pos.x),
pos_y=float(pos.y),
uv_x=float(uv.x),
uv_y=float(uv.y),
col=int(v.col) & 0xFFFFFFFF
))
# Indices
indices = [int(i) for i in draw_list.idx_buffer]
# Commands
cmds = []
for c in draw_list.cmd_buffer:
clip = c.clip_rect
cmds.append(cls.Cmd(
elem_count=int(c.elem_count),
clip_x1=float(clip.x),
clip_y1=float(clip.y),
clip_x2=float(clip.z),
clip_y2=float(clip.w),
texture_id=int(c.get_tex_id())
))
lists.append(cls.imList(verts, indices, cmds))
return cls(
total_vtx_count=getattr(dd, "total_vtx_count", dd.total_vtx_count),
total_idx_count=getattr(dd, "total_idx_count", dd.total_idx_count),
cmd_lists_count=len(dd.cmd_lists),
lists=lists
)
def to_bytes(self) -> bytes:
"""Ultra-fast compact binary (better than msgpack in practice due to array_like)"""
return msgspec.msgpack.encode(self)
@classmethod
def from_bytes(cls, data: bytes) -> "PyImDrawData":
"""Deserialize from wire"""
return msgspec.msgpack.decode(data, type=cls)
# Optional: extract all used texture IDs
def used_texture_ids(self) -> set[int]:
ids = set()
for lst in self.lists:
for cmd in lst.commands:
if cmd.texture_id:
ids.add(cmd.texture_id)
return ids
def replay_draw_data(pyd: PyImDrawData, texture_map: dict[int, int] | None = None):
if texture_map is None:
texture_map = {}
draw_list = imgui.get_window_draw_list()
for lst in pyd.lists:
verts = lst.vertices
idxs = lst.indices
idx_offset = 0
for cmd in lst.commands:
# THIS IS THE IMPORTANT PART — bind the correct texture!
tex_id = texture_map.get(cmd.texture_id, imgui.ImTextureRef(imgui.get_io().fonts.python_get_texture_id()))
draw_list.push_texture(tex_id) # ← This sets the active texture
# Clip rect
draw_list.push_clip_rect((cmd.clip_x1, cmd.clip_y1), (cmd.clip_x2, cmd.clip_y2),True)
# Draw all triangles in this command
tri_count = cmd.elem_count // 3
for _ in range(tri_count):
if idx_offset + 2 >= len(idxs):
break
i0, i1, i2 = idxs[idx_offset:idx_offset + 3]
idx_offset += 3
v0 = verts[i0]
v1 = verts[i1]
v2 = verts[i2]
draw_list.add_triangle_filled( #probably wrong
(v0.pos_x, v0.pos_y),
(v1.pos_x, v1.pos_y),
(v2.pos_x, v2.pos_y),
v0.col
)
draw_list.pop_clip_rect()
draw_list.pop_texture() # restore previous texture?What I like about it is how little total code there is here. It's not even close to correct, I need to figure out how to get the cmd stuff to work properly. But it is pretty fun. |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
So, I have a imgui_bundle python application. And I would like to be able to run it remotely. in my use case I just have one instance that i need to worry about, so shared input would be fine. I looked over the imgui_bundle and saw some things that looked closeish, but not quite there. eg the stuff here:
imgui_bundle/bindings/imgui_bundle/hello_imgui.pyi
Line 2415 in f1f7827
But I'm not sure how it is supposed to be activated.
There is also the pyodide backend that can now run on the web which makes me wonder if it would be trivial to have a websocket share the drawlist/UserInput using the same version of imgui for max compatibility.
If there is a way to get this working in a stable manner it would be nice to know. I have reviewed some of the repos for remoting, but they seem pretty tied to the c++ implementation, and perhaps there is a way to jump that hurdle, but I'm not sure what it is.
Beta Was this translation helpful? Give feedback.
All reactions