use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::{AnyWindowHandle, SharedString, SystemWindowTab, WindowId};
#[derive(Default)]
pub struct TabManagerState {
groups: HashMap<String, Vec<AnyWindowHandle>>,
window_identifiers: HashMap<WindowId, String>,
window_titles: HashMap<WindowId, SharedString>,
}
#[derive(Clone)]
pub struct WindowTabManager {
state: Arc<Mutex<TabManagerState>>,
window_handle: AnyWindowHandle,
}
impl WindowTabManager {
pub fn new(window_handle: AnyWindowHandle, state: Arc<Mutex<TabManagerState>>) -> Self {
Self {
state,
window_handle,
}
}
pub fn shared_state() -> Arc<Mutex<TabManagerState>> {
Arc::new(Mutex::new(TabManagerState::default()))
}
pub fn set_tabbing_identifier(&self, identifier: Option<String>) {
let mut state = self.state.lock().unwrap();
let window_id = self.window_handle.window_id();
if let Some(old_id) = state.window_identifiers.remove(&window_id) {
if let Some(group) = state.groups.get_mut(&old_id) {
group.retain(|h| h.window_id() != window_id);
if group.is_empty() {
state.groups.remove(&old_id);
}
}
}
if let Some(id) = identifier {
if !id.is_empty() {
state
.groups
.entry(id.clone())
.or_default()
.push(self.window_handle);
state.window_identifiers.insert(window_id, id);
}
}
}
pub fn merge_all_windows(&self) {
let state = self.state.lock().unwrap();
let window_id = self.window_handle.window_id();
let _ = (state, window_id);
}
pub fn move_tab_to_new_window(&self) {
let mut state = self.state.lock().unwrap();
let window_id = self.window_handle.window_id();
if let Some(identifier) = state.window_identifiers.remove(&window_id) {
if let Some(group) = state.groups.get_mut(&identifier) {
group.retain(|h| h.window_id() != window_id);
if group.is_empty() {
state.groups.remove(&identifier);
}
}
}
}
pub fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
let state = self.state.lock().unwrap();
let window_id = self.window_handle.window_id();
let identifier = state.window_identifiers.get(&window_id)?;
let group = state.groups.get(identifier)?;
let tabs: Vec<SystemWindowTab> = group
.iter()
.map(|handle| {
let title = state
.window_titles
.get(&handle.window_id())
.cloned()
.unwrap_or_default();
SystemWindowTab::new(title, *handle)
})
.collect();
Some(tabs)
}
pub fn set_title(&self, title: SharedString) {
let mut state = self.state.lock().unwrap();
state
.window_titles
.insert(self.window_handle.window_id(), title);
}
pub fn remove_window(&self) {
let mut state = self.state.lock().unwrap();
let window_id = self.window_handle.window_id();
state.window_titles.remove(&window_id);
if let Some(identifier) = state.window_identifiers.remove(&window_id) {
if let Some(group) = state.groups.get_mut(&identifier) {
group.retain(|h| h.window_id() != window_id);
if group.is_empty() {
state.groups.remove(&identifier);
}
}
}
}
pub fn tabbing_identifier(&self) -> Option<String> {
let state = self.state.lock().unwrap();
state
.window_identifiers
.get(&self.window_handle.window_id())
.cloned()
}
pub fn tab_count(&self) -> usize {
let state = self.state.lock().unwrap();
let window_id = self.window_handle.window_id();
state
.window_identifiers
.get(&window_id)
.and_then(|id| state.groups.get(id))
.map(|g| g.len())
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{EmptyView, WindowHandle, WindowId};
fn make_handle(raw: u32) -> AnyWindowHandle {
let id: WindowId = slotmap::KeyData::from_ffi(raw as u64).into();
WindowHandle::<EmptyView>::new(id).into()
}
#[test]
fn test_set_tabbing_identifier_groups_windows() {
let state = WindowTabManager::shared_state();
let h1 = make_handle(1);
let h2 = make_handle(2);
let h3 = make_handle(3);
let m1 = WindowTabManager::new(h1, state.clone());
let m2 = WindowTabManager::new(h2, state.clone());
let m3 = WindowTabManager::new(h3, state.clone());
assert!(m1.tabbed_windows().is_none());
m1.set_tabbing_identifier(Some("group-a".into()));
m2.set_tabbing_identifier(Some("group-a".into()));
m3.set_tabbing_identifier(Some("group-b".into()));
let tabs = m1.tabbed_windows().unwrap();
assert_eq!(tabs.len(), 2);
assert!(tabs.iter().any(|t| t.handle == h1));
assert!(tabs.iter().any(|t| t.handle == h2));
let tabs_b = m3.tabbed_windows().unwrap();
assert_eq!(tabs_b.len(), 1);
assert!(tabs_b.iter().any(|t| t.handle == h3));
}
#[test]
fn test_move_tab_to_new_window() {
let state = WindowTabManager::shared_state();
let h1 = make_handle(10);
let h2 = make_handle(11);
let h3 = make_handle(12);
let m1 = WindowTabManager::new(h1, state.clone());
let m2 = WindowTabManager::new(h2, state.clone());
let m3 = WindowTabManager::new(h3, state.clone());
m1.set_tabbing_identifier(Some("grp".into()));
m2.set_tabbing_identifier(Some("grp".into()));
m3.set_tabbing_identifier(Some("grp".into()));
assert_eq!(m1.tab_count(), 3);
m2.move_tab_to_new_window();
assert_eq!(m1.tab_count(), 2);
assert!(m2.tabbed_windows().is_none());
assert_eq!(m2.tab_count(), 0);
}
#[test]
fn test_tabbed_windows_returns_titles() {
let state = WindowTabManager::shared_state();
let h1 = make_handle(20);
let h2 = make_handle(21);
let m1 = WindowTabManager::new(h1, state.clone());
let m2 = WindowTabManager::new(h2, state.clone());
m1.set_title("Window 1".into());
m2.set_title("Window 2".into());
m1.set_tabbing_identifier(Some("titled".into()));
m2.set_tabbing_identifier(Some("titled".into()));
let tabs = m1.tabbed_windows().unwrap();
assert_eq!(tabs.len(), 2);
let titles: Vec<&str> = tabs.iter().map(|t| t.title.as_ref()).collect();
assert!(titles.contains(&"Window 1"));
assert!(titles.contains(&"Window 2"));
}
#[test]
fn test_remove_window_cleans_up() {
let state = WindowTabManager::shared_state();
let h1 = make_handle(30);
let h2 = make_handle(31);
let m1 = WindowTabManager::new(h1, state.clone());
let m2 = WindowTabManager::new(h2, state.clone());
m1.set_tabbing_identifier(Some("cleanup".into()));
m2.set_tabbing_identifier(Some("cleanup".into()));
assert_eq!(m1.tab_count(), 2);
m1.remove_window();
assert_eq!(m2.tab_count(), 1);
assert!(m1.tabbed_windows().is_none());
}
#[test]
fn test_set_tabbing_identifier_none_removes_from_group() {
let state = WindowTabManager::shared_state();
let h1 = make_handle(40);
let h2 = make_handle(41);
let m1 = WindowTabManager::new(h1, state.clone());
let m2 = WindowTabManager::new(h2, state.clone());
m1.set_tabbing_identifier(Some("temp".into()));
m2.set_tabbing_identifier(Some("temp".into()));
assert_eq!(m1.tab_count(), 2);
m1.set_tabbing_identifier(None);
assert!(m1.tabbed_windows().is_none());
assert_eq!(m2.tab_count(), 1);
}
#[test]
fn test_set_tabbing_identifier_switches_group() {
let state = WindowTabManager::shared_state();
let h1 = make_handle(50);
let h2 = make_handle(51);
let h3 = make_handle(52);
let m1 = WindowTabManager::new(h1, state.clone());
let m2 = WindowTabManager::new(h2, state.clone());
let m3 = WindowTabManager::new(h3, state.clone());
m1.set_tabbing_identifier(Some("alpha".into()));
m2.set_tabbing_identifier(Some("alpha".into()));
m3.set_tabbing_identifier(Some("beta".into()));
assert_eq!(m1.tab_count(), 2);
assert_eq!(m3.tab_count(), 1);
m1.set_tabbing_identifier(Some("beta".into()));
assert_eq!(m2.tab_count(), 1); assert_eq!(m3.tab_count(), 2); assert_eq!(m1.tabbing_identifier(), Some("beta".into()));
}
}