keep track of state per window

This allows multiple application windows to be opened and managed.
This commit is contained in:
Sebastian Thiel 2024-07-10 19:32:20 +02:00
parent ccd6a6fe0d
commit c310942b12
No known key found for this signature in database
GPG Key ID: 9CB5EE7895E8268B
3 changed files with 61 additions and 32 deletions

View File

@ -211,14 +211,19 @@ fn main() {
.menu(menu::build(tauri_context.package_info())) .menu(menu::build(tauri_context.package_info()))
.on_menu_event(|event|menu::handle_event(&event)) .on_menu_event(|event|menu::handle_event(&event))
.on_window_event(|event| { .on_window_event(|event| {
if let tauri::WindowEvent::Focused(focused) = event.event() { let window = event.window();
if *focused { match event.event() {
tokio::task::spawn(async move { tauri::WindowEvent::Destroyed => {
let _ = event.window().app_handle() window.app_handle()
.state::<WindowState>() .state::<WindowState>()
.flush().await; .remove(window.label());
});
} }
tauri::WindowEvent::Focused(focused) if *focused => {
window.app_handle()
.state::<WindowState>()
.flush(window.label()).ok();
},
_ => {}
} }
}) })
.build(tauri_context) .build(tauri_context)

View File

@ -4,7 +4,7 @@ pub mod commands {
use gitbutler_project::ProjectId; use gitbutler_project::ProjectId;
use gitbutler_project::{self as projects, Controller}; use gitbutler_project::{self as projects, Controller};
use tauri::State; use tauri::{State, Window};
use tracing::instrument; use tracing::instrument;
use crate::error::Error; use crate::error::Error;
@ -49,14 +49,15 @@ pub mod commands {
/// ///
/// We use it to start watching for filesystem events. /// We use it to start watching for filesystem events.
#[tauri::command(async)] #[tauri::command(async)]
#[instrument(skip(controller, watchers), err(Debug))] #[instrument(skip(controller, watchers, window), err(Debug))]
pub async fn set_project_active( pub async fn set_project_active(
controller: State<'_, Controller>, controller: State<'_, Controller>,
watchers: State<'_, WindowState>, watchers: State<'_, WindowState>,
window: Window,
id: ProjectId, id: ProjectId,
) -> Result<(), Error> { ) -> Result<(), Error> {
let project = controller.get(id).context("project not found")?; let project = controller.get(id).context("project not found")?;
Ok(watchers.set_project_to_window(&project)?) Ok(watchers.set_project_to_window(window.label(), &project)?)
} }
#[tauri::command(async)] #[tauri::command(async)]

View File

@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@ -14,7 +15,7 @@ mod event {
use gitbutler_watcher::Change; use gitbutler_watcher::Change;
use tauri::Manager; use tauri::Manager;
/// An change we want to inform the frontend about. /// A change we want to inform the frontend about.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ChangeForFrontend { pub(super) struct ChangeForFrontend {
name: String, name: String,
@ -85,18 +86,21 @@ impl Drop for State {
} }
} }
type WindowLabel = String;
type WindowLabelRef = str;
/// State associated to windows /// State associated to windows
/// Note that this type is managed in Tauri and thus needs to be send and sync. /// Note that this type is managed in Tauri and thus needs to be `Send` and `Sync`.
#[derive(Clone)] #[derive(Clone)]
pub struct WindowState { pub struct WindowState {
/// NOTE: This handle is required for this type to be self-contained as it's used by `core` through a trait. /// NOTE: This handle is required for this type to be self-contained as it's used by `core` through a trait.
app_handle: AppHandle, app_handle: AppHandle,
/// The state for the main window. /// The state for every open application window.
/// NOTE: This is a `tokio` mutex as this needs to lock the inner option from within async. /// NOTE: This is a `tokio` mutex as this needs to lock the inner option from within async.
state: Arc<tokio::sync::Mutex<Option<State>>>, state: Arc<tokio::sync::Mutex<BTreeMap<WindowLabel, State>>>,
} }
fn handler_from_app(app: &AppHandle) -> anyhow::Result<gitbutler_watcher::Handler> { fn handler_from_app(app: &AppHandle) -> Result<gitbutler_watcher::Handler> {
let projects = app.state::<projects::Controller>().inner().clone(); let projects = app.state::<projects::Controller>().inner().clone();
let users = app.state::<users::Controller>().inner().clone(); let users = app.state::<users::Controller>().inner().clone();
let vbranches = gitbutler_branch_actions::VirtualBranchActions::default(); let vbranches = gitbutler_branch_actions::VirtualBranchActions::default();
@ -120,9 +124,16 @@ impl WindowState {
} }
} }
/// Watch the project and assure no other instance can access it. /// Watch the `project`, assure no other instance can access it, and associate it with the window
/// uniquely identified by `window`.
///
/// Previous state will be removed and its resources cleaned up.
#[instrument(skip(self, project), err(Debug))] #[instrument(skip(self, project), err(Debug))]
pub fn set_project_to_window(&self, project: &projects::Project) -> Result<()> { pub fn set_project_to_window(
&self,
window: &WindowLabelRef,
project: &projects::Project,
) -> Result<()> {
let mut lock_file = let mut lock_file =
fslock::LockFile::open(project.gb_dir().join(WINDOW_LOCK_FILE).as_os_str())?; fslock::LockFile::open(project.gb_dir().join(WINDOW_LOCK_FILE).as_os_str())?;
lock_file lock_file
@ -133,20 +144,25 @@ impl WindowState {
let worktree_dir = project.path.clone(); let worktree_dir = project.path.clone();
let project_id = project.id; let project_id = project.id;
let watcher = gitbutler_watcher::watch_in_background(handler, worktree_dir, project_id)?; let watcher = gitbutler_watcher::watch_in_background(handler, worktree_dir, project_id)?;
block_on(self.state.lock()).replace(State { let mut state_by_label = block_on(self.state.lock());
project_id, state_by_label.insert(
watcher, window.to_owned(),
exclusive_access: lock_file, State {
}); project_id,
watcher,
exclusive_access: lock_file,
},
);
tracing::debug!("Maintaining {} Windows", state_by_label.len());
Ok(()) Ok(())
} }
pub async fn post(&self, action: gitbutler_watcher::Action) -> Result<()> { pub async fn post(&self, action: gitbutler_watcher::Action) -> Result<()> {
let state = self.state.lock().await; let mut state_by_label = self.state.lock().await;
if let Some(state) = state let state = state_by_label
.as_ref() .values_mut()
.filter(|state| state.project_id == action.project_id()) .find(|state| state.project_id == action.project_id());
{ if let Some(state) = state {
state state
.watcher .watcher
.post(action) .post(action)
@ -154,19 +170,26 @@ impl WindowState {
.context("failed to post event") .context("failed to post event")
} else { } else {
Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"matching watcher to post event not found, wanted {wanted}, got {actual:?}", "matching watcher to post event not found, wanted {wanted}",
wanted = action.project_id(), wanted = action.project_id(),
actual = state.as_ref().map(|s| s.project_id)
)) ))
} }
} }
pub async fn flush(&self) -> Result<()> { /// Flush file-monitor watcher events once the windows regains focus for it to respond instantly
let state = self.state.lock().await; /// instead of according to the tick-rate.
if let Some(state) = state.as_ref() { pub fn flush(&self, window: &WindowLabelRef) -> Result<()> {
let state_by_label = block_on(self.state.lock());
if let Some(state) = state_by_label.get(window) {
state.watcher.flush()?; state.watcher.flush()?;
} }
Ok(()) Ok(())
} }
/// Remove the state associated with `window`, typically upon its destruction.
pub fn remove(&self, window: &WindowLabelRef) {
let mut state_by_label = block_on(self.state.lock());
state_by_label.remove(window);
}
} }