Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Less frameworky way to run the application #313

Open
vorner opened this issue Apr 22, 2020 · 9 comments
Open

Less frameworky way to run the application #313

vorner opened this issue Apr 22, 2020 · 9 comments
Labels
developer experience feature New feature or request help wanted Extra attention is needed shell

Comments

@vorner
Copy link

vorner commented Apr 22, 2020

Hello

First, I believe Iced is actually the first good-looking GUI API out there, so thanks!

Nevertheless, I have some small suggestion/feature request. There are two extremes how to run the application. One of them is the Application::run method that just takes care of everything and insists on managing the lifetime of the whole application, including setting up of a futures executor. On the other hand, there's the very low-level and manual way shown in https://github.com/hecrj/iced/blob/master/examples/integration/src/main.rs.

I'd like to have something in between. Something less verbose and more high level than the integration example, but something that's still flexible enough to let me setup (or reuse) eg. tokio threadpool the way I want, or to start the application, let it run until someone closes the window and then run some code afterwards. I'm not sure how exactly it looks inside, but for example hyper went from high-level „we manage everything“ serve method in previous versions to manual set up of server and converting it into a future one can spawn on the tokio executor.

I've tried scanning through the issues here and didn't find one similar, but maybe I'm not looking well enough.

@hecrj hecrj added feature New feature or request help wanted Extra attention is needed labels Apr 22, 2020
@hecrj
Copy link
Member

hecrj commented Apr 22, 2020

I agree we need to offer further abstractions to ease the integration process.

The integration example showcases one approach. It is there to invite users to build their own custom event loop to satisfy their use cases. As I said when I wrote the example in #183:

This process, although very flexible, is currently not as straightforward as I would like it to be. However, I prefer to wait, see how folks implement it and gather more use cases before designing further abstractions.

I believe the building blocks are there and now it's just a matter of gathering use cases, exploring different approaches, and finding patterns.

I use the library through my own opinionated approach. Therefore, I don't believe I should be the one building or exploring these abstractions.

Overall, I think this should happen as a result of a bunch of community efforts and collaboration. I placed the low-level building blocks there for this reason!

On a related note, I'd like to remind anyone willing to help that, when it comes to collaboration, writing code is a very small part of the equation! It should be the last part of the process. In my experience, the best kind of collaboration happens when a user:

  • Socializes (gets in touch!)
  • Uses the library
  • Gets inspired
  • Explores an approach
  • Reports results (not just code!)

Code is the easy part. Before that, a considerable amount of communication, inspiration, and exploration is necessary. Please, keep this in mind if you want to take a shot at this!

@krooq
Copy link

krooq commented May 8, 2020

This would be a great way to sandbox ideas that may not fit into the current architecture/lifecycle.
I'd like to see how we can abstract out the lifecycle :D

@krooq
Copy link

krooq commented May 11, 2020

I'd like to get the conversation started on this topic. The earlier we discuss these things the less locked in the library will be to the provided framework.
The library is already in a great state to start thinking about how this could look. Everything is decoupled and has clear boundaries and responsibilities.

To get things going here are some things I'd like to be able to use the iced ecosystem for:

  • A simple cross platform rendering engine
  • A toolkit for UI widgets in any kind of graphical application

And some things I might want to implement myself or use another library/pattern for:

  • State management
  • Event loop management
  • Application lifecycle

In the integration example you mention these 5 steps

  1. Initialize data structures
  2. Map shell events to iced events
  3. Process iced events
  4. Update the user interface
  5. Draw the user interface

These give us some hints to what would be nice to have.
e.g. Having control over [5] lets you control framerate to some extent.

@azriel91
Copy link
Contributor

Heya, I've got a use case, and have halfway explored the possibility. Use case:

  1. I'm making a new library, nginee, which has its own EventLoop. The EventLoop runs a number of EventHandlers.
  2. Each event handler's logic is an async function.
  3. Mixing async and winit's synchronous EventLoop::run is tricky, but has been done.
  4. Since nginee is in control of the winit event loop, I'd like a way to tell iced_winit, "heya, please use this winit event loop" 🔁.
  5. Also to be able to tell iced_web expose the event handler to call whenever an event happens.

Currently:

  • iced_winit's run function does these things:
    • initializes application context
    • runs the winit event loop
    • handles winit events
  • iced_web::Application::run does these things:

As an experiment, I grabbed the initialization and event handling code from iced_winit and gotten it to run as an async EventHandler. Here's an example of an nginee event loop with two event handlers:

  • Handler 1: exit the application after 3 seconds.
  • Handler 2: runs iced_winit.

iced_integration

Code of interest:

  • iced_winit application context initialization iced_winit.rs#L49-L125

    The application context was semi tricky to set up because of type parameters and same-named traits, but compiler messages were helpful enough.

    Thought I could refer to the integration example's main.rs, but that one references wgpu's device, which is private (for good reason -- I only read ECOSYSTEM.md after this experiment).

  • iced_winit event handler: iced_winit.rs#L127-L265

    I think it's alright for this to take an owned winit Event, but within the context of my library, I'm not sure if the event handler should be setting control_flow.

  • Bridge between iced_winit and my EventHandler: iced_winit.rs#L273-L291

    This was actually really difficult to write -- needed to create a closure that returns a Future that needs to hold onto the IcedWinit context object, but the context must be returned after that Future is done.

    Ended up passing in an owned context object to the closure, and returning it at the end of the closure execution.

  • Small application that uses the IcedWinit event handler: iced_integration/src/lib.rs

    It's not elegant to instantiate, as one has to tell the context object what Executor and Compositor to use.

    This information is actually captured by iced::Application, but it needed to use the independent event handling code that was extracted, that's why the manual trait referencing. I exposed Instance in iced/src/application.rs to get the free trait impls, but with work on iced's API, we wouldn't have to.

@azriel91
Copy link
Contributor

azriel91 commented Jun 1, 2020

In the integration example you mention these 5 steps

  1. Initialize data structures
  2. Map shell events to iced events
  3. Process iced events
  4. Update the user interface
  5. Draw the user interface

Given these steps, I think a good approach is:

  1. Allow iced to return the data structures that represent the application context.
  2. Expose event handling functions for iced (for each backend) to respond to iced::Events (can bikeshed). These functions may take in the application context data structure(s) from 1..
  3. Expose default mapping functions from winit::Events (and other default backends) to iced::Events.
  4. iced::Application::run (and backends) call these functions.
  5. Users can also call the functions in 3., or 2. if they are using a different event loop / windowing system.

That way we can retain the simple Application::run interface, while users who want to use iced as a library may do so.

Sound good? (I'll begin experimenting soon)

@hecrj
Copy link
Member

hecrj commented Jun 2, 2020

I just wanted to mention that the integration example has changed considerably since #354 landed. It now uses the new Program trait alongside program::State which abstracts a lot of the event, caching, and redrawing logic.

I think these new abstractions may help address this issue partially. Let me know!

@azriel91 I haven't had a chance to review your use cases or look at your code yet. Have you taken a look at the new abstractions? They seem to align with the ideas in your last comment.

@azriel91
Copy link
Contributor

azriel91 commented Jun 2, 2020

Yeaps ✌️! The first spike is based off that. program::State does abstract the underlying iced logic away (yay!), though there's still quite a lot of detail to glue the renderer and shell together.

I'm hoping for abstractions that are able to simply take in "this event happened, and here is the current application context", so something like this:

#[cfg(all(feature = "wgpu", feature = "winit"))]
fn main() {
    let my_iced_app = MyIcedApp::new();
    let mut iced_context = IcedContext::<Wgpu>::new(my_iced_app);
    let event_loop = winit::event_loop::EventLoop::new();

    use iced::winit::EventHandlerExt;
    event_loop.run(|event, _, control_flow| {
        iced_context.handle_winit_event(event, control_flow);
    });
}

#[cfg(all(feature = "wgpu", not(feature = "winit")))]
fn main() {
    let my_iced_app = MyIcedApp::new();
    let mut iced_context = IcedContext::<Wgpu>::new(my_iced_app);
    let custom_event_loop = otherloop::EventLoop::new();

    custom_event_loop.run(|event| {
        let iced_event = IcedEvent::from(event); // to be implemented by user.
        iced_context.handle_event(event);
    });
}

I'm still at the envisioning stage, so while it looks rather simplistic, ideally that's the goal.

@hecrj
Copy link
Member

hecrj commented Jun 3, 2020

@azriel91 I had something similar in mind when I was working on #354. Here are some thoughts:

  • I believe using Context as a name tends to be a code smell. It can mean anything and, as a consequence, it has absolutely no scope. Anything can be added to it without changing its meaning, which makes it a design hazard. I think a better name for it would be simply Window, as it is wired to an event loop, has a title, a mode, etc.

  • But if we name it Window, then some questions arise:

    • How will this type work once we tackle Multi-window support #27? Should the Compositor and the Renderer live outside of it so we can share resources? How will this abstraction be different than a program::State then? It's okay if we don't for now, but it would be great to have some answers to these questions.
    • Does Window::handle_event make sense? Maybe we need a better name.
  • As far as I understand, this abstraction is meant to let users subscribe to runtime events and decide when to feed them to the underlying application:

    • What are some particular use cases for this? You mention you are building a library. What does it do? Why does it need to control the event loop?
    • Does handle_event need to always be called on a RedrawRequested or MainEventsCleared? What is the contract of this method? Are there any use cases where it won't always be called?
    • Maybe instead of a Window we should create our own EventLoop wrapper with a run method that would handle_event automatically, but still allow for some amount of control with a closure.

I think once we discuss some of these, we will be able to understand the problem better.

@azriel91
Copy link
Contributor

azriel91 commented Jun 3, 2020

Heya, thank you for taking the time to look at the spike 🙇‍♂️.

I believe using Context as a name tends to be a code smell

nod, it tends towards the "God object" interface; am happy to name it something better ✌️. Storing the data hierarchically may feel better, and potentially help with sharing state (re: #27), but realistically it's still a "container for everything iced".

I think exploring the third main point should clarify my thoughts, so here we go:

What are some particular use cases for this? You mention you are building a library. What does it do?

I'm beginning another game engine (nginee), and am trying to keep things modular -- a game should be able to run headlessly, and may ignore loading textures if it's not rendering, may render to a terminal instead of graphically -- that sort of thing. So, displaying the game graphically may simply be turning on the "window" feature.


Why does it need to control the event loop?

This is a very good question -- I'm not sure it strictly needs to. Turning on "window" could mean take the provided user logic and put it inside an iced::Application.

What already exists in nginee is, I've gotten unit tests running and exiting winit event loops without terminating the test process1, which doesn't work when using winit "normally". This makes automated testing of a windowed game possible; normal usage (which is correct) doesn't allow for that2.


Does handle_event need to always be called on a RedrawRequested or MainEventsCleared? What is the contract of this method? Are there any use cases where it won't always be called?

Hm, good question. I haven't used nginee in a windowed application yet (was going to try with iced, hence this activity). As a first go I simply decided to forward the winit event through to the windowing library, because all my other event handlers are just functions that run (asynchronously, though that may not be important).


Maybe instead of a Window we should create our own EventLoop wrapper with a run method that would handle_event automatically, but still allow for some amount of control with a closure.

Haha, my mind's a bit done for today. May have to imagine what that looks like tomorrow.


1 Requires using EventLoop::new_any_thread, and using EventLoopExtDesktop::run_return
2 EventLoop::run(_) -> ! aborts the process even if you put it in a thread, which breaks the test execution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
developer experience feature New feature or request help wanted Extra attention is needed shell
Projects
None yet
Development

No branches or pull requests

4 participants