diff --git a/src/dialogs/login_dialog.rs b/src/dialogs/login_dialog.rs index f7763f0..44d00fb 100644 --- a/src/dialogs/login_dialog.rs +++ b/src/dialogs/login_dialog.rs @@ -1,6 +1,7 @@ use super::RegisterDialog; +use crate::push; use crate::backend::{Backend, LoginData}; -use crate::widgets::{Navigator, NavigatorScreen}; +use crate::widgets::new_navigator::{NavigationHandle, Screen, Widget}; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; @@ -9,18 +10,15 @@ use std::rc::Rc; /// A dialog for entering login credentials. pub struct LoginDialog { - backend: Rc, + handle: NavigationHandle, widget: gtk::Stack, info_bar: gtk::InfoBar, username_entry: gtk::Entry, password_entry: gtk::Entry, - selected_cb: RefCell ()>>>, - navigator: RefCell>>, } -impl LoginDialog { - /// Create a new login dialog. - pub fn new(backend: Rc) -> Rc { +impl Screen<(), LoginData> for LoginDialog { + fn new(_: (), handle: NavigationHandle) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/login_dialog.ui"); @@ -33,22 +31,17 @@ impl LoginDialog { get_widget!(builder, gtk::Button, register_button); let this = Rc::new(Self { - backend, + handle, widget, info_bar, username_entry, password_entry, - selected_cb: RefCell::new(None), - navigator: RefCell::new(None), }); // Connect signals and callbacks cancel_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } + this.handle.pop(None); })); login_button.connect_clicked(clone!(@strong this => move |_| { @@ -62,16 +55,9 @@ impl LoginDialog { let c = glib::MainContext::default(); let clone = this.clone(); c.spawn_local(async move { - clone.backend.set_login_data(data.clone()).await.unwrap(); - if clone.backend.login().await.unwrap() { - if let Some(cb) = &*clone.selected_cb.borrow() { - cb(data); - } - - let navigator = clone.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } + clone.handle.backend.set_login_data(data.clone()).await.unwrap(); + if clone.handle.backend.login().await.unwrap() { + clone.handle.pop(Some(data)); } else { clone.widget.set_visible_child_name("content"); clone.info_bar.set_revealed(true); @@ -80,44 +66,21 @@ impl LoginDialog { })); register_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let dialog = RegisterDialog::new(this.backend.clone()); - - dialog.set_selected_cb(clone!(@strong this => move |data| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(data); - } - - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - navigator.push(dialog); - } + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + if let Some(data) = push!(clone.handle, RegisterDialog).await { + clone.handle.pop(Some(data)); + } + }); })); this } - - /// The closure to call when the login succeded. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } } -impl NavigatorScreen for LoginDialog { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - +impl Widget for LoginDialog { fn get_widget(&self) -> gtk::Widget { self.widget.clone().upcast() } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } } diff --git a/src/dialogs/preferences.rs b/src/dialogs/preferences.rs index 001c9b4..c9b7ac5 100644 --- a/src/dialogs/preferences.rs +++ b/src/dialogs/preferences.rs @@ -1,6 +1,6 @@ use super::{LoginDialog, ServerDialog}; use crate::backend::Backend; -use crate::widgets::NavigatorWindow; +use crate::widgets::new_navigator_window::NavigatorWindow; use gettextrs::gettext; use glib::clone; use gtk::prelude::*; @@ -85,16 +85,17 @@ impl Preferences { })); login_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = LoginDialog::new(this.backend.clone()); - - dialog.set_selected_cb(clone!(@strong this => move |data| { - this.login_row.set_subtitle(Some(&data.username)); - })); - - - let window = NavigatorWindow::new(dialog); + let window = NavigatorWindow::new(this.backend.clone()); window.set_transient_for(&this.window); window.show(); + + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + if let Some(data) = window.navigator.replace::<_, _, LoginDialog>(()).await { + clone.login_row.set_subtitle(Some(&data.username)); + } + }); })); // Initialize diff --git a/src/dialogs/register.rs b/src/dialogs/register.rs index 727713d..cd0fb7e 100644 --- a/src/dialogs/register.rs +++ b/src/dialogs/register.rs @@ -1,5 +1,5 @@ use crate::backend::{Backend, LoginData, UserRegistration}; -use crate::widgets::{Navigator, NavigatorScreen}; +use crate::widgets::new_navigator::{NavigationHandle, Screen, Widget}; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; @@ -9,7 +9,7 @@ use std::rc::Rc; /// A dialog for creating a new user account. pub struct RegisterDialog { - backend: Rc, + handle: NavigationHandle, widget: gtk::Stack, username_entry: gtk::Entry, email_entry: gtk::Entry, @@ -18,13 +18,11 @@ pub struct RegisterDialog { captcha_row: libadwaita::ActionRow, captcha_entry: gtk::Entry, captcha_id: RefCell>, - selected_cb: RefCell>>, - navigator: RefCell>>, } -impl RegisterDialog { +impl Screen<(), LoginData> for RegisterDialog { /// Create a new register dialog. - pub fn new(backend: Rc) -> Rc { + fn new(_: (), handle: NavigationHandle) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/register_dialog.ui"); @@ -39,7 +37,7 @@ impl RegisterDialog { get_widget!(builder, gtk::Entry, captcha_entry); let this = Rc::new(Self { - backend, + handle, widget, username_entry, email_entry, @@ -48,17 +46,12 @@ impl RegisterDialog { captcha_row, captcha_entry, captcha_id: RefCell::new(None), - selected_cb: RefCell::new(None), - navigator: RefCell::new(None), }); // Connect signals and callbacks cancel_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } + this.handle.pop(None); })); register_button.connect_clicked(clone!(@strong this => move |_| { @@ -93,20 +86,13 @@ impl RegisterDialog { }; // TODO: Handle errors. - if clone.backend.register(registration).await.unwrap() { - if let Some(cb) = &*clone.selected_cb.borrow() { - let data = LoginData { - username, - password, - }; + if clone.handle.backend.register(registration).await.unwrap() { + let data = LoginData { + username, + password, + }; - cb(data); - } - - let navigator = clone.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } + clone.handle.pop(Some(data)); } else { clone.widget.set_visible_child_name("content"); } @@ -119,7 +105,7 @@ impl RegisterDialog { let context = glib::MainContext::default(); let clone = this.clone(); context.spawn_local(async move { - let captcha = clone.backend.get_captcha().await.unwrap(); + let captcha = clone.handle.backend.get_captcha().await.unwrap(); clone.captcha_row.set_title(Some(&captcha.question)); clone.captcha_id.replace(Some(captcha.id)); clone.widget.set_visible_child_name("content"); @@ -127,23 +113,10 @@ impl RegisterDialog { this } - - /// The closure to call when the login succeded. - pub fn set_selected_cb(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } } -impl NavigatorScreen for RegisterDialog { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - +impl Widget for RegisterDialog { fn get_widget(&self) -> gtk::Widget { self.widget.clone().upcast() } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } } diff --git a/src/meson.build b/src/meson.build index 88aa8da..6667975 100644 --- a/src/meson.build +++ b/src/meson.build @@ -98,6 +98,8 @@ sources = files( 'widgets/mod.rs', 'widgets/navigator.rs', 'widgets/navigator_window.rs', + 'widgets/new_navigator.rs', + 'widgets/new_navigator_window.rs', 'widgets/player_bar.rs', 'widgets/poe_list.rs', 'widgets/screen.rs', diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index bb85c93..d8018eb 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -10,9 +10,13 @@ pub use list::*; pub mod navigator; pub use navigator::*; +pub mod new_navigator; + pub mod navigator_window; pub use navigator_window::*; +pub mod new_navigator_window; + pub mod player_bar; pub use player_bar::*; diff --git a/src/widgets/navigator.rs b/src/widgets/navigator.rs index 7e4a98b..f9a4a9f 100644 --- a/src/widgets/navigator.rs +++ b/src/widgets/navigator.rs @@ -148,3 +148,4 @@ impl Navigator { self.old_screens.borrow_mut().clear(); } } + diff --git a/src/widgets/new_navigator.rs b/src/widgets/new_navigator.rs new file mode 100644 index 0000000..33b360f --- /dev/null +++ b/src/widgets/new_navigator.rs @@ -0,0 +1,242 @@ +use crate::backend::Backend; +use futures_channel::oneshot; +use futures_channel::oneshot::{Receiver, Sender}; +use glib::clone; +use gtk::prelude::*; +use std::cell::{Cell, RefCell}; +use std::rc::{Rc, Weak}; + +/// Simplification for pushing new screens. +/// +/// This macro can be invoked in two forms. +/// +/// 1. To push screens without an input value: +/// +/// ``` +/// let result = push!(handle, ScreenType).await; +/// ``` +/// +/// 2. To push screens with an input value: +/// +/// ``` +/// let result = push!(handle, ScreenType, input).await; +/// ``` +#[macro_export] +macro_rules! push { + ($handle:expr, $screen:ty) => { + $handle.push::<_, _, $screen>(()) + }; + ($handle:expr, $screen:ty, $input:ident) => { + $handle.push::<_, _, $screen>($input) + }; +} + +/// A widget that represents a logical unit of transient user interaction and +/// that optionally resolves to a specific return value. +pub trait Screen: Widget { + /// Create a new screen and initialize it with the provided input value. + fn new(input: I, navigation_handle: NavigationHandle) -> Rc where Self: Sized; +} + +/// Something that can be represented as a GTK widget. +pub trait Widget { + /// Get the widget. + fn get_widget(&self) -> gtk::Widget; +} + +/// An accessor to navigation functionality for screens. +pub struct NavigationHandle { + /// The backend, in case the screen needs it. + pub backend: Rc, + + /// The toplevel window, in case the screen needs it. + pub window: gtk::Window, + + /// The navigator that created this navigation handle. + navigator: Weak, + + /// The sender through which the result should be sent. + sender: Cell>>>, +} + +impl NavigationHandle { + /// Switch to another screen and wait for that screen's result. + pub async fn push + 'static>(&self, input: I) -> Option { + let navigator = self.unwrap_navigator(); + let receiver = navigator.push::(input); + receiver.await.expect("The sender to send the result of a screen was dropped.") + } + + /// Go back to the previous screen optionally returning something. + pub fn pop(&self, output: Option) { + let sender = self.sender.take() + .expect("Tried to send result from screen through a dropped sender."); + + if sender.send(output).is_err() { + panic!("Tried to send result from screen to non-existing previous screen."); + } + + self.unwrap_navigator().pop(); + } + + /// Get the navigator and panic if it doesn't exist. + fn unwrap_navigator(&self) -> Rc { + Weak::upgrade(&self.navigator) + .expect("Tried to access non-existing navigator from a screen.") + } +} + +/// A toplevel widget for managing screens. +pub struct Navigator { + /// The underlying GTK widget. + pub widget: gtk::Stack, + + /// The backend, in case screens need it. + backend: Rc, + + /// The toplevel window of the navigator, in case screens need it. + window: gtk::Window, + + /// The currently active screens. The last screen in this vector is the one + /// that is currently visible. + screens: RefCell>>, + + /// A vector holding the widgets of the old screens that are waiting to be + /// removed after the animation has finished. + old_widgets: RefCell>, + + /// A closure that will be called when the last screen is popped. + back_cb: RefCell>>, +} + +impl Navigator { + /// Create a new navigator which will display the provided widget + /// initially. + pub fn new(backend: Rc, window: &W, empty_screen: &E) -> Rc + where + W: IsA, + E: IsA, + { + let widget = gtk::StackBuilder::new() + .hhomogeneous(false) + .vhomogeneous(false) + .interpolate_size(true) + .transition_type(gtk::StackTransitionType::Crossfade) + .hexpand(true) + .vexpand(true) + .build(); + + widget.add_child(empty_screen); + + let this = Rc::new(Self { + widget, + backend, + window: window.to_owned().upcast(), + screens: RefCell::new(Vec::new()), + old_widgets: RefCell::new(Vec::new()), + back_cb: RefCell::new(None), + }); + + this.widget.connect_property_transition_running_notify(clone!(@strong this => move |_| { + if !this.widget.get_transition_running() { + this.clear_old_widgets(); + } + })); + + this + } + + /// Set the closure to be called when the last screen is popped so that + /// the navigator shows its empty state. + pub fn set_back_cb(&self, cb: F) { + self.back_cb.replace(Some(Box::new(cb))); + } + + /// Drop all screens and show the provided screen instead. + pub async fn replace + 'static>(self: &Rc, input: I) -> Option { + for screen in self.screens.replace(Vec::new()) { + self.old_widgets.borrow_mut().push(screen.get_widget()); + } + + let receiver = self.push::(input); + + if !self.widget.get_transition_running() { + self.clear_old_widgets(); + } + + receiver.await.expect("The sender to send the result of a screen was dropped.") + } + + + /// Drop all screens and go back to the initial screen. The back callback + /// will not be called. + pub fn reset(&self) { + self.widget.set_visible_child_name("empty_screen"); + + for screen in self.screens.replace(Vec::new()) { + self.old_widgets.borrow_mut().push(screen.get_widget()); + } + + if !self.widget.get_transition_running() { + self.clear_old_widgets(); + } + } + + /// Show a screen with the provided input. This should only be called from + /// within a navigation handle. + fn push + 'static>(self: &Rc, input: I) -> Receiver> { + let (sender, receiver) = oneshot::channel(); + + let handle = NavigationHandle { + backend: Rc::clone(&self.backend), + window: self.window.clone(), + navigator: Rc::downgrade(self), + sender: Cell::new(Some(sender)), + }; + + let screen = S::new(input, handle); + + let widget = screen.get_widget(); + self.widget.add_child(&widget); + self.widget.set_visible_child(&widget); + + self.screens.borrow_mut().push(screen); + + receiver + } + + /// Pop the last screen from the list of screens. + fn pop(&self) { + let popped = if let Some(screen) = self.screens.borrow_mut().pop() { + let widget = screen.get_widget(); + self.old_widgets.borrow_mut().push(widget); + true + } else { + false + }; + + if popped { + if let Some(screen) = self.screens.borrow().last() { + let widget = screen.get_widget(); + self.widget.set_visible_child(&widget); + } else { + if let Some(cb) = &*self.back_cb.borrow() { + cb() + } + } + + if !self.widget.get_transition_running() { + self.clear_old_widgets(); + } + } + } + + /// Drop the old widgets. + fn clear_old_widgets(&self) { + for widget in self.old_widgets.borrow().iter() { + self.widget.remove(widget); + } + + self.old_widgets.borrow_mut().clear(); + } +} diff --git a/src/widgets/new_navigator_window.rs b/src/widgets/new_navigator_window.rs new file mode 100644 index 0000000..e35e66a --- /dev/null +++ b/src/widgets/new_navigator_window.rs @@ -0,0 +1,41 @@ +use crate::backend::Backend; +use super::new_navigator::{Navigator, Screen}; +use glib::clone; +use gtk::prelude::*; +use std::rc::Rc; + +/// A window hosting a navigator. +pub struct NavigatorWindow { + pub navigator: Rc, + window: libadwaita::Window, +} + +impl NavigatorWindow { + /// Create a new navigator window. + pub fn new(backend: Rc) -> Rc { + let window = libadwaita::Window::new(); + window.set_default_size(600, 424); + let placeholder = gtk::Label::new(None); + let navigator = Navigator::new(backend, &window, &placeholder); + libadwaita::WindowExt::set_child(&window, Some(&navigator.widget)); + + let this = Rc::new(Self { navigator, window }); + + this.navigator.set_back_cb(clone!(@strong this => move || { + this.window.close(); + })); + + this + } + + /// Make the wrapped window transient. This will make the window modal. + pub fn set_transient_for>(&self, window: &W) { + self.window.set_modal(true); + self.window.set_transient_for(Some(window)); + } + + /// Show the navigator window. + pub fn show(&self) { + self.window.show(); + } +}