Skip to content

Commit

Permalink
Playtime: Use trait object instead of enum for app integration distin…
Browse files Browse the repository at this point in the history
…ction
  • Loading branch information
helgoboss committed Nov 10, 2023
1 parent 489f651 commit bb525fc
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 116 deletions.
189 changes: 85 additions & 104 deletions main/src/infrastructure/ui/app/app_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,126 +9,107 @@ use playtime_clip_engine::proto::{
use prost::Message;
use reaper_low::raw;
use std::cell::RefCell;
use std::fmt::Debug;
use std::rc::Rc;
use std::time::Duration;
use swell_ui::{SharedView, View, ViewContext, Window};
use validator::HasLen;

pub type SharedAppInstance = Rc<RefCell<AppInstance>>;
pub type SharedAppInstance = Rc<RefCell<dyn AppInstance>>;

#[derive(Debug)]
pub struct AppInstance {
integration: AppIntegration,
}
pub trait AppInstance: Debug {
fn is_running(&self) -> bool;

#[derive(Debug)]
enum AppIntegration {
/// App will run within a SWELL window that is provided by ReaLearn (`AppPanel`).
///
/// This is currently only a good choice on Windows. On macOS, the app uses an NSViewController
/// that's supposed to be attached to the NSWindow in order to manage its content view (NSView).
/// But SWELL doesn't just provide the NSWindow, it also provides and manages the content view.
/// Letting the content view be managed by both the NSViewController and SWELL is not possible.
///
/// There's the possibility to use child windows on macOS but this means that if the app itself
/// tries to access and control its containing window, it's going to affect the *child* window
/// and not the window provided ReaLearn. It also needs more attention when it comes to
/// keyboard shortcut forwarding and comes with probably a whole bunch of other corner cases.
/// It's still an interesting possibility, especially when it comes to implementing docking.
Parented(SharedView<AppPanel>),
/// App will run in its own window.
///
/// This is possible on all OS.
Standalone(StandaloneApp),
fn start_or_show(&mut self, owning_window: Window) -> Result<()>;

fn stop(&mut self);

fn send(&self, reply: &Reply) -> Result<()>;

fn notify_app_is_ready(&mut self, callback: AppCallback);
}

impl AppInstance {
pub fn new(session: WeakSession) -> Self {
#[cfg(target_os = "windows")]
{
let app_panel = AppPanel::new(session);
Self {
integration: AppIntegration::Parented(SharedView::new(app_panel)),
}
pub fn create_app_instance(session: WeakSession) -> impl AppInstance {
#[cfg(target_os = "windows")]
{
let app_panel = AppPanel::new(session);
ParentedAppInstance {
panel: SharedView::new(app_panel),
}
#[cfg(target_os = "macos")]
{
let standalone_app = StandaloneApp::new(session);
Self {
integration: AppIntegration::Standalone(standalone_app),
}
}
#[cfg(target_os = "macos")]
{
StandaloneAppInstance {
session,
running_state: None,
}
}
}

pub fn is_open(&self) -> bool {
match &self.integration {
AppIntegration::Parented(p) => p.is_open(),
AppIntegration::Standalone(a) => a.is_open(),
}
/// App will run within a SWELL window that is provided by ReaLearn (`AppPanel`).
///
/// This is currently only a good choice on Windows. On macOS, the app uses an NSViewController
/// that's supposed to be attached to the NSWindow in order to manage its content view (NSView).
/// But SWELL doesn't just provide the NSWindow, it also provides and manages the content view.
/// Letting the content view be managed by both the NSViewController and SWELL is not possible.
///
/// There's the possibility to use child windows on macOS but this means that if the app itself
/// tries to access and control its containing window, it's going to affect the *child* window
/// and not the window provided ReaLearn. It also needs more attention when it comes to
/// keyboard shortcut forwarding and comes with probably a whole bunch of other corner cases.
/// It's still an interesting possibility, especially when it comes to implementing docking.
#[derive(Debug)]
pub struct ParentedAppInstance {
panel: SharedView<AppPanel>,
}

impl AppInstance for ParentedAppInstance {
fn is_running(&self) -> bool {
self.panel.is_open()
}

pub fn open(&mut self, owning_window: Window) -> Result<()> {
match &mut self.integration {
AppIntegration::Parented(p) => {
if let Some(window) = p.view_context().window() {
// If window already open (and maybe just hidden), simply show it.
window.show();
return Ok(());
}
// Fail fast if library not available
App::get_or_load_app_library()?;
// Then open. This actually only opens the SWELL window. The real stuff is done
// in the "opened" handler of the SWELL window.
p.clone().open(owning_window);
Ok(())
}
AppIntegration::Standalone(p) => p.open(),
fn start_or_show(&mut self, owning_window: Window) -> Result<()> {
if let Some(window) = self.panel.view_context().window() {
// If window already open (and maybe just hidden), simply show it.
window.show();
return Ok(());
}
// Fail fast if library not available
App::get_or_load_app_library()?;
// Then open. This actually only opens the SWELL window. The real stuff is done
// in the "opened" handler of the SWELL window.
self.panel.clone().open(owning_window);
Ok(())
}

pub fn close(&mut self) {
match &mut self.integration {
AppIntegration::Parented(p) => {
p.close();
}
AppIntegration::Standalone(p) => p.close(),
}
fn stop(&mut self) {
self.panel.close()
}

pub fn send_to_app(&self, reply: &Reply) -> Result<()> {
match &self.integration {
AppIntegration::Parented(p) => p.send_to_app(reply),
AppIntegration::Standalone(p) => p.send_to_app(reply),
}
fn send(&self, reply: &Reply) -> Result<()> {
self.panel.send_to_app(reply)
}

pub fn notify_app_is_ready(&mut self, callback: AppCallback) {
match &mut self.integration {
AppIntegration::Parented(p) => p.notify_app_is_ready(callback),
AppIntegration::Standalone(p) => p.notify_app_is_ready(callback),
}
fn notify_app_is_ready(&mut self, callback: AppCallback) {
self.panel.notify_app_is_ready(callback);
}
}

/// App will run in its own window.
///
/// This is possible on all OS.
#[derive(Debug)]
struct StandaloneApp {
struct StandaloneAppInstance {
session: WeakSession,
open_state: Option<OpenState>,
running_state: Option<RunningAppState>,
}

impl StandaloneApp {
pub fn new(session: WeakSession) -> Self {
Self {
session,
open_state: None,
}
}

pub fn is_open(&self) -> bool {
self.open_state.is_some()
impl AppInstance for StandaloneAppInstance {
fn is_running(&self) -> bool {
self.running_state.is_some()
}

pub fn open(&mut self) -> Result<()> {
fn start_or_show(&mut self, _owning_window: Window) -> Result<()> {
let app_library = App::get_or_load_app_library()?;
let session_id = self
.session
Expand All @@ -138,32 +119,32 @@ impl StandaloneApp {
.id()
.to_string();
let app_handle = app_library.run_in_parent(None, session_id)?;
let open_state = OpenState {
let running_state = RunningAppState {
app_handle,
app_callback: None,
event_receivers: Some(subscribe_to_events()),
};
self.open_state = Some(open_state);
self.running_state = Some(running_state);
Ok(())
}

pub fn close(&mut self) {
fn stop(&mut self) {
todo!()
}

pub fn send_to_app(&self, reply: &Reply) -> Result<()> {
self.open_state
fn send(&self, reply: &Reply) -> Result<()> {
self.running_state
.as_ref()
.context("app not open")?
.send_to_app(reply)
.send(reply)
}

pub fn notify_app_is_ready(&mut self, callback: AppCallback) {
let Some(open_state) = &mut self.open_state else {
fn notify_app_is_ready(&mut self, callback: AppCallback) {
let Some(running_state) = &mut self.running_state else {
return;
};
// Handshake finished! The app has the host callback and we have the app callback.
open_state.app_callback = Some(callback);
running_state.app_callback = Some(callback);
// Now we can start passing events to the app callback
// self.start_timer();
}
Expand All @@ -173,11 +154,11 @@ impl StandaloneApp {
pub struct AppPanel {
view: ViewContext,
session: WeakSession,
open_state: RefCell<Option<OpenState>>,
open_state: RefCell<Option<RunningAppState>>,
}

#[derive(Debug)]
struct OpenState {
struct RunningAppState {
app_handle: AppHandle,
app_callback: Option<AppCallback>,
// TODO-medium This is too specific.
Expand All @@ -198,7 +179,7 @@ impl AppPanel {
.borrow()
.as_ref()
.context("app not open")?
.send_to_app(reply)
.send(reply)
}

pub fn toggle_full_screen(&self) -> Result<()> {
Expand Down Expand Up @@ -242,7 +223,7 @@ impl AppPanel {
.id()
.to_string();
let app_handle = app_library.run_in_parent(Some(window), session_id)?;
let open_state = OpenState {
let open_state = RunningAppState {
app_handle,
app_callback: None,
event_receivers: Some(subscribe_to_events()),
Expand All @@ -261,8 +242,8 @@ impl AppPanel {
}
}

impl OpenState {
pub fn send_to_app(&self, reply: &Reply) -> Result<()> {
impl RunningAppState {
pub fn send(&self, reply: &Reply) -> Result<()> {
let app_callback = self.app_callback.context("app callback not known yet")?;
send_to_app(app_callback, reply);
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion main/src/infrastructure/ui/app/app_library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ fn send_to_app(session_id: &str, reply_value: reply::Value) -> Result<()> {
let reply = Reply {
value: Some(reply_value),
};
app_instance.borrow().send_to_app(&reply)?;
app_instance.borrow().send(&reply)?;
Ok(())
}

Expand Down
6 changes: 3 additions & 3 deletions main/src/infrastructure/ui/header_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ impl HeaderPanel {
let text_from_clipboard = Rc::new(get_text_from_clipboard().unwrap_or_default());
let text_from_clipboard_clone = text_from_clipboard.clone();
#[cfg(feature = "playtime")]
let app_is_open = self.panel_manager().borrow().app_panel_is_open();
let app_is_open = self.panel_manager().borrow().app_instance_is_running();
let data_object_from_clipboard = if text_from_clipboard.is_empty() {
None
} else {
Expand Down Expand Up @@ -2301,12 +2301,12 @@ impl HeaderPanel {

#[cfg(feature = "playtime")]
fn show_app(&self) {
self.panel_manager().borrow().show_app_panel();
self.panel_manager().borrow().start_or_show_app_instance();
}

#[cfg(feature = "playtime")]
fn close_app(&self) {
self.panel_manager().borrow().close_app_panel();
self.panel_manager().borrow().stop_app_instance();
}

fn open_preset_folder(&self) {
Expand Down
19 changes: 11 additions & 8 deletions main/src/infrastructure/ui/independent_panel_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl IndependentPanelManager {
mapping_panels: Default::default(),
#[cfg(feature = "playtime")]
app_instance: std::rc::Rc::new(std::cell::RefCell::new(
crate::infrastructure::ui::AppInstance::new(session.clone()),
crate::infrastructure::ui::create_app_instance(session.clone()),
)),
message_panel: SharedView::new(SessionMessagePanel::new(session)),
}
Expand Down Expand Up @@ -103,19 +103,22 @@ impl IndependentPanelManager {
}

#[cfg(feature = "playtime")]
pub fn show_app_panel(&self) {
let result = self.app_instance.borrow_mut().open(reaper_main_window());
pub fn start_or_show_app_instance(&self) {
let result = self
.app_instance
.borrow_mut()
.start_or_show(reaper_main_window());
crate::base::notification::notify_user_on_anyhow_error(result);
}

#[cfg(feature = "playtime")]
pub fn close_app_panel(&self) {
self.app_instance.borrow_mut().close();
pub fn stop_app_instance(&self) {
self.app_instance.borrow_mut().stop();
}

#[cfg(feature = "playtime")]
pub fn app_panel_is_open(&self) -> bool {
self.app_instance.borrow().is_open()
pub fn app_instance_is_running(&self) -> bool {
self.app_instance.borrow().is_running()
}

pub fn close_message_panel(&self) {
Expand Down Expand Up @@ -158,7 +161,7 @@ impl IndependentPanelManager {
}
self.mapping_panels.clear();
#[cfg(feature = "playtime")]
self.app_instance.borrow_mut().close();
self.app_instance.borrow_mut().stop();
}

fn request_panel(&mut self) -> SharedView<MappingPanel> {
Expand Down

0 comments on commit bb525fc

Please sign in to comment.