turion / rhine

Haskell Functional Reactive Programming framework with type-level clocks
http://hackage.haskell.org/package/rhine
117 stars 21 forks source link

dear-imgui bindings #250

Open turion opened 9 months ago

turion commented 9 months ago

It would be great to have bindings for https://hackage.haskell.org/package/dear-imgui, which is a GUI library.

Cajunvoodoo commented 8 months ago

I've actually been working on something like this for Dunai! It's in a very rough condition, but I haven't had tons of time (or motivation) to work on it recently (I really wanted it to compile to the browser, but I think the JS backend needs to mature). The biggest issue with it is sampling rate (my laptop manages to limit the framerate, but on my desktop it burns a core running at max fps). I managed to get user input, graphs, windows, and buttons working in what I think is a decently ergonomic way, but I'm very open to suggestions! The current version of the Dunai library can be found here (everything about it, including the name, is/was WIP and is mostly due to a change in project goals/scope).

I'd happily switch to Rhine, especially with the cleaner user experience, but I'm not entirely confident with the various Rhine datatypes (I get the concept, but I don't know how much the API has changed in terms of UX).

ners commented 8 months ago

@Cajunvoodoo I'd love to collaborate with you on this!

turion commented 8 months ago

That sounds great! Since rhine currently depends on dunai, a dunai-dear-imgui library could actually be reused in rhine. For example, FRP.Rhine.ClSF.Except is to a large portion a re-export of the corresponding dunai module, and the other half is just simple lifting functions applied to the dunai primitives.

If you have a conceptual idea, feel free to open a PR, I'll guide you along. If not, here are some ideas how to design this. I think the central questions have to be "what is the monad" and "what are the clocks".

Monads

For the monads, I could imagine something like abstracting the brackets into reader monads that, when run, take care of the brackets. Something like this pseudocode:

{-# LANGUAGE DataKinds #-}
data Backend = OpenGL | SDL
class BackendBracket (b :: Backend) where
  type Context b
  type Config b
  create :: Config b -> IO (Context b)
  destroy :: Context b -> IO ()

instance BackendBracket 'OpenGL where
  type Context 'OpenGL = OpenGLContext
  type Config b = OpenGLConfig
  create = createWindow
  destroy = destroyWindow

-- Using e.g. vinyl heterogeneous lists
newtype ManagedT (backends :: [Backend]) a = ManagedT (ReaderT (Rec Context backends) IO a)

-- It should be possible to implement such a context like Forall using type families
type family Forall (backends :: [Backend]) (Backend -> Constraint) :: Constraint where
  Forall '[] _ = ()
  Forall (backend ': backends) f = (f backend, Forall backends f)

runManagedT :: Forall backends BackendBracked => ManagedT backends a -> IO a
runManagedT = ... -- create all contexts and destroy them at the end

This is just a suggestion, possibly heterogeneous lists are overkill and it's better to have e.g. data Backend = OpenGL | SDL | OpenGLAndSDL | ... to have more fine grained initialisations. Or one does away with the BackendBracket type class completely and burdens initialisation on the user.

Clocks

I'm not sure what's the best idea for clocks.

  1. The obvious way to go forward would be to use an event clock for every widget. But the downside would be that adding, changing or removing a widget then changes the type signature. That would create some friction while developing.
  2. Another way would be to have a single event clock for all widgets. But then one needs to add routing for all events again, which is annoying.
  3. Or one could have no clocks for widget events, and instead wire the widgets the same way one does in dear-imgui. Only the renderings are ticks then.

I guess it's easiest to start with 3. and once this is done, experiment with 2. or 1. when a few example applications are ready.