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!
ratappis already quite useful in its current state, yet I want to add some more things forv1.0.0.My main conflict with the current state is the duplicate
::rerender()methods, one inScreenand one inNavigator. 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.0start with unifying the re-rendering API underNavigator::rerender()and droppingScreen::rerender(), unlike my first plans. This is in favor of a newScreen::task()method whose signature would look something like follows:The
Appevent loop will then look something like this (pseudocode-ish):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 aNavigator::goto()call (or the other futureNavigatormethods 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
Appdefinition looks something like this:When the application navigates, the
App::screenproperty changes and the previous state is lost. I want to bring aHistoryAPI (or just get it integrated intoNavigator) which looks more like aVecDeque<S>. TheNavigatorwould then implement a few methods:Navigator::push(ScreenID)- Like the currentNavigator::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 theVecDeque<S>history stack.Navigator::forward()- LikeNavigator::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 ofNavigator::push(), this method replaces the current screen (and its state) with theScreenID'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 theScreens-derived struct. Right now it's alwayspub, but if theScreens-derived struct ispub(crate), for example, then things don't match. They should.The second, now big, improvement I want to make to
ScreenIDis 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 likeThen, the resulting
ScreenIDwould look something likeYou can then just navigate to profile screens using
Navigator.push(ScreenID::Profile(1234)), and screens would be initialized usingScreen::with_params(params: T)instead ofScreen::default(). When implementing a screen, it would look something like thisTuple-type and struct-type parameters would be able to be pattern matched (?) against for a more ergonomic API.
Suggestions are more than welcome!