Skip to content

Plans for v1.0.0 #1

@Nekidev

Description

@Nekidev

ratapp is already quite useful in its current state, yet I want to add some more things for v1.0.0.

My main conflict with the current state is the duplicate ::rerender() methods, one in Screen and one in Navigator. They both do the same in different ways and in the end, background tasks feel like a second-class citizen when the reason I made the framework async in the first place was to have background tasks.

My plans for v1.0.0 start with unifying the re-rendering API under Navigator::rerender() and dropping Screen::rerender(), unlike my first plans. This is in favor of a new Screen::task() method whose signature would look something like follows:

impl Screen<ScreenID> for MyScreen {
    async fn task(&mut self, navigator: &Navigator<ScreenID>);
}

The App event loop will then look something like this (pseudocode-ish):

let mut task = Box::pin(screen.task());

loop {
    // Only one of these futures runs at the same time, so `Screen::task()` can take `&mut self` as an argument.
    tokio::select! {
        _ = &mut task => {},
        event = on_event() => screen.on_event().await,
        _ = rerenders.recv() => terminal.draw(),
        action => navigation.recv() => { ... }
    }
}

That way, we avoid locks and make background tasks first-class citizens, which is perfect.

I also want to make re-renders explicit. I.e. no more automatic re-rendering on events, only after a Navigator::rerender() call or a Navigator::goto() call (or the other future Navigator methods I'll describe below).

Also, I want to add a few web-like navigation features to ratapp. As it is right now, Navigator::goto() drops the current screen's state and initializes a new one. There's no way to keep that state (without using the global app state, which is not meant to be used to persist individual screen state) cross-navigation, so I want to address that.

Currently, the App definition looks something like this:

struct App<S, T> where S: ScreenState<T> {
    screen: S
}

When the application navigates, the App::screen property changes and the previous state is lost. I want to bring a History API (or just get it integrated into Navigator) which looks more like a VecDeque<S>. The Navigator would then implement a few methods:

  • Navigator::push(ScreenID) - Like the current Navigator::goto(), but now pushes a new screen to the navigation stack.
  • Navigator::back() - Navigates to the previous screen. Its state will be kept since it's in the VecDeque<S> history stack.
  • Navigator::forward() - Like Navigator::back(), but goes forward in the history.
  • Navigator::has_back() - Are there screens to navigate back to?
  • Navigator::has_forward() - Are there screens to navigate forward to?
  • Navigator::replace(ScreenID) - Instead of Navigator::push(), this method replaces the current screen (and its state) with the ScreenID's screen.
  • Navigator::clear() - Removes everything in the history stack but the current screen.
  • Navigator::clear_back() - Removes everything in the stack before the current screen.
  • Navigator::clear_forward() - Removes everything in the stack after the current screen.
  • Navigator::restart() - Sets the application and history back to the initial state. Everything back to default.

I have also thought about things like Navigator::back_many(usize) but I'm unsure how much that is needed yet. It may enable people to write messier code.

Last but not least, improvements to ScreenID. To start with, make it replicate the visibility of the Screens-derived struct. Right now it's always pub, but if the Screens-derived struct is pub(crate), for example, then things don't match. They should.

The second, now big, improvement I want to make to ScreenID is the ability to pass values in its enum variants as if they were URL routes with dynamic path or query parameters. For example, ScreenID::Profile(usize) to navigate to a dynamic profile's screen. Any types must be allowed. I'm planning to implement it doing something like

#[derive(Screens)]
struct MyScreens {
    Home(HomeScreen),

    #[params(usize)]
    Profile(ProfileScreen),
}

Then, the resulting ScreenID would look something like

enum ScreenID {
    Home,
    Profile(usize)
}

You can then just navigate to profile screens using Navigator.push(ScreenID::Profile(1234)), and screens would be initialized using Screen::with_params(params: T) instead of Screen::default(). When implementing a screen, it would look something like this

struct ProfileScreen {
    profile_id: usize,
}

impl Screen<ScreenID> for ProfileScreen {
    type Params = usize;

    fn with_params(profile_id: Params) -> Self {
        Self {
            profile_id,
        }
    }
}

Tuple-type and struct-type parameters would be able to be pattern matched (?) against for a more ergonomic API.

Suggestions are more than welcome!

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions