Skip to content

WindowGroup ​

The WindowGroup component provides SwiftUI-compatible window group management, allowing you to create templates for multiple windows of the same type. This is ideal for document-based applications where users can open multiple instances.

Basic Usage ​

typescript
import { WindowGroup } from '@tachui/core/viewport'

// Simple window group
const MyWindowGroup = WindowGroup({
  id: 'documents',
  title: 'Document',
  children: () => DocumentView()
})

Data-Driven Windows ​

WindowGroup excels at creating data-driven windows where each window represents a specific data object:

typescript
// Define your data type
class Document {
  id!: string
  title!: string
  content!: string
  lastModified!: Date
}

// Create window group for documents
const DocumentGroup = WindowGroup({
  id: 'document-editor',
  title: 'Document Editor',
  for: Document,
  children: (document: Document) => DocumentEditor({ document }),
  width: 900,
  height: 700,
  resizable: true
})

// Usage
const openDocument = async (doc: Document) => {
  await DocumentGroup.openForData(doc)
}

API Reference ​

Props ​

PropertyTypeRequiredDescription
idstringYesUnique identifier for the window group
titlestringNoDefault title for windows in this group
forclass constructorNoData type for data-driven windows
childrenFunctionYesComponent factory function
widthnumberNoDefault window width (default: 800)
heightnumberNoDefault window height (default: 600)
resizablebooleanNoWhether windows can be resized (default: true)
minimizablebooleanNoWhether windows can be minimized (default: true)
maximizablebooleanNoWhether windows can be maximized (default: true)
modalbooleanNoWhether windows are modal (default: false)

Version 1.2 Properties:

PropertyTypeRequiredDescription
groupingStrategy'tabs' | 'stack' | 'cascade' | 'tile'NoWindow arrangement strategy (default: 'stack')
tabConfigWindowTabConfigNoTab grouping configuration
poolConfigWindowPoolConfigNoWindow pooling configuration
stateSyncScope'none' | 'group' | 'global'NoState synchronization scope (default: 'none')
maxInstancesnumberNoMaximum concurrent windows (default: Infinity)

Methods ​

The WindowGroup component returns an object with these methods:

typescript
interface WindowGroupComponent<T> {
  // Basic window management
  open(data?: T): Promise<void>
  openForData(data: T): Promise<void>
  closeAll(): Promise<void>
  getWindows(): ViewportInstance[]
  
  // Version 1.2: Configuration methods
  configureGrouping(strategy: WindowGroupingStrategy): void
  configureTabs(config: WindowTabConfig): void  
  configurePooling(config: WindowPoolConfig): void
  enableStateSync(scope: StateSyncScope): void
  
  // Version 1.2: State management
  syncGroupState<S>(key: string, value: S): void
  getGroupState<S>(key: string): S | undefined
  onGroupStateChange<S>(key: string, callback: (value: S) => void): () => void
  
  // Version 1.2: Access to underlying WindowGroup
  group: WindowGroup // Access to full WindowGroup API
  
  // Render method for integration
  render(): ComponentInstance
}

Examples ​

Version 1.2: Advanced Window Group ​

This example demonstrates the advanced Version 1.2 features:

typescript
// Project-based document editor with advanced features
class ProjectDocument {
  constructor(
    public id: string,
    public title: string,
    public content: string = '',
    public projectId: string,
    public lastModified: Date = new Date()
  ) {}
}

// Advanced WindowGroup with Version 1.2 features
const ProjectDocuments = WindowGroup({
  id: 'project-documents',
  title: 'Project Document',
  for: ProjectDocument,
  children: (doc: ProjectDocument) => ProjectDocumentEditor({ document: doc }),
  
  // Version 1.2: Window grouping strategy
  groupingStrategy: 'tabs',
  
  // Version 1.2: Tab configuration
  tabConfig: {
    enabled: true,
    maxTabs: 8,
    tabPosition: 'top',
    allowDetach: true,
    allowReorder: true
  },
  
  // Version 1.2: Window pooling for performance
  poolConfig: {
    enabled: true,
    maxPoolSize: 5,
    reuseThreshold: 30000,  // 30 seconds
    keepAliveTime: 300000   // 5 minutes
  },
  
  // Version 1.2: Group-level state synchronization
  stateSyncScope: 'group',
  
  // Version 1.2: Limit concurrent windows
  maxInstances: 10
})

function ProjectDocumentEditor({ document }: { document: ProjectDocument }) {
  const [content, setContent] = createSignal(document.content)
  const [selectedTool, setSelectedTool] = createSignal('edit')
  const [projectTheme, setProjectTheme] = createSignal<'light' | 'dark'>('light')
  
  // Version 1.2: Listen for synchronized state changes
  createEffect(() => {
    // Listen for tool changes across all project windows
    const unsubscribeTool = ProjectDocuments.onGroupStateChange('selectedTool', (tool: string) => {
      setSelectedTool(tool)
    })
    
    // Listen for theme changes
    const unsubscribeTheme = ProjectDocuments.onGroupStateChange('theme', (theme: 'light' | 'dark') => {
      setProjectTheme(theme)
    })
    
    // Cleanup subscriptions
    onCleanup(() => {
      unsubscribeTool()
      unsubscribeTheme()
    })
  })
  
  const syncTool = (tool: string) => {
    setSelectedTool(tool)
    // Version 1.2: Sync tool selection across all project windows
    ProjectDocuments.syncGroupState('selectedTool', tool)
  }
  
  const handleSave = () => {
    document.content = content()
    document.lastModified = new Date()
    
    // Version 1.2: Notify other windows about the update
    ProjectDocuments.syncGroupState('documentUpdated', {
      documentId: document.id,
      timestamp: Date.now()
    })
  }
  
  return VStack({
    padding: 20,
    spacing: 16,
    backgroundColor: () => projectTheme() === 'dark' ? '#2a2a2a' : '#ffffff',
    children: [
      // Synchronized tool bar
      HStack({
        spacing: 8,
        children: [
          Button({
            title: 'Edit',
            action: () => syncTool('edit'),
            variant: () => selectedTool() === 'edit' ? 'primary' : 'secondary'
          }),
          Button({
            title: 'Review',
            action: () => syncTool('review'),
            variant: () => selectedTool() === 'review' ? 'primary' : 'secondary'
          }),
          Button({
            title: 'Comment',
            action: () => syncTool('comment'),
            variant: () => selectedTool() === 'comment' ? 'primary' : 'secondary'
          }),
          Spacer(),
          Text({
            content: () => `Tool: ${selectedTool()} (synced)`,
            fontSize: 12,
            opacity: 0.7
          })
        ]
      }),
      
      // Document editor
      TextField({
        value: content,
        onChange: setContent,
        multiline: true,
        minHeight: 400
      }),
      
      Button({
        title: 'Save',
        action: handleSave,
        variant: 'primary'
      })
    ]
  })
}

// Usage example
function ProjectApp() {
  const openProjectDocument = async (doc: ProjectDocument) => {
    await ProjectDocuments.openForData(doc)
  }
  
  const configureWindowGroup = () => {
    // Version 1.2: Dynamic configuration
    ProjectDocuments.configureGrouping('cascade')  // Change to cascade layout
    ProjectDocuments.syncGroupState('theme', 'dark') // Set dark theme for all windows
  }
  
  // Version 1.2: Listen for group events
  createEffect(() => {
    // Listen for window lifecycle events
    const unsubscribeCreated = ProjectDocuments.group.onWindowCreated((window) => {
      console.log(`Document window created: ${window.id}`)
    })
    
    const unsubscribeEmpty = ProjectDocuments.group.onGroupEmpty(() => {
      console.log('All project documents closed')
    })
    
    const unsubscribeFull = ProjectDocuments.group.onGroupFull(() => {
      console.log('Maximum project documents reached')
    })
    
    onCleanup(() => {
      unsubscribeCreated()
      unsubscribeEmpty()
      unsubscribeFull()
    })
  })
  
  return VStack({
    children: [
      Button({
        title: 'Configure Windows',
        action: configureWindowGroup
      }),
      Button({
        title: 'Open Document',
        action: () => openProjectDocument(new ProjectDocument('1', 'Doc1', '', 'project1'))
      })
    ]
  })
}

Basic Document Editor ​

typescript
import { WindowGroup, useOpenWindow } from '@tachui/core/viewport'
import { VStack, Button, Text } from '@tachui/core'

// Document data type
class TextDocument {
  constructor(
    public id: string,
    public title: string,
    public content: string = '',
    public saved: boolean = false
  ) {}
}

// Document editor component
function DocumentEditor({ document }: { document: TextDocument }) {
  const [content, setContent] = createSignal(document.content)
  const [saved, setSaved] = createSignal(document.saved)
  
  const handleSave = () => {
    document.content = content()
    document.saved = true
    setSaved(true)
  }
  
  return VStack({
    children: [
      Text({ content: () => document.title + (saved() ? '' : ' *') }),
      TextField({
        value: content,
        onChange: (value) => {
          setContent(value)
          setSaved(false)
        },
        multiline: true
      }),
      Button({
        title: 'Save',
        action: handleSave,
        disabled: () => saved()
      })
    ]
  })
}

// Window group for documents
const DocumentWindows = WindowGroup({
  id: 'text-documents',
  title: 'Text Document',
  for: TextDocument,
  children: (document: TextDocument) => DocumentEditor({ document }),
  width: 800,
  height: 600,
  resizable: true
})

// Main app component
function DocumentApp() {
  const documents = createSignal<TextDocument[]>([])
  
  const createNewDocument = async () => {
    const doc = new TextDocument(
      `doc-${Date.now()}`,
      `Untitled-${documents()[0].length + 1}`
    )
    documents()[1]([...documents()[0], doc])
    await DocumentWindows.openForData(doc)
  }
  
  const openDocument = async (doc: TextDocument) => {
    await DocumentWindows.openForData(doc)
  }
  
  return VStack({
    children: [
      Button({
        title: 'New Document',
        action: createNewDocument
      }),
      ...documents()[0].map(doc => 
        Button({
          title: doc.title,
          action: () => openDocument(doc)
        })
      )
    ]
  })
}

Image Viewer ​

typescript
class ImageFile {
  constructor(
    public id: string,
    public name: string,
    public url: string,
    public size: { width: number; height: number }
  ) {}
}

function ImageViewer({ image }: { image: ImageFile }) {
  return VStack({
    children: [
      Text({ content: image.name }),
      Image({
        src: image.url,
        alt: image.name,
        width: image.size.width,
        height: image.size.height
      })
    ]
  })
}

const ImageWindows = WindowGroup({
  id: 'image-viewer',
  title: 'Image Viewer',
  for: ImageFile,
  children: (image: ImageFile) => ImageViewer({ image }),
  width: Math.min(image.size.width + 100, 1200),
  height: Math.min(image.size.height + 100, 800),
  resizable: true
})

Settings Group ​

For non-data-driven windows, you can create groups without the for parameter:

typescript
const SettingsGroup = WindowGroup({
  id: 'settings',
  title: 'Settings',
  children: () => SettingsPanel(),
  width: 600,
  height: 400,
  resizable: false,
  modal: true
})

// Open settings window
await SettingsGroup.open()

Integration with App ​

WindowGroup components are typically used within an App component:

typescript
import { App } from '@tachui/core/viewport'

const MyApp = App({
  children: [
    // Document windows
    WindowGroup({
      id: 'documents',
      title: 'Document',
      for: Document,
      children: (doc) => DocumentEditor({ document: doc })
    }),
    
    // Settings window
    Window({
      id: 'settings',
      title: 'Settings',
      children: () => SettingsView()
    })
  ]
})

Window Management ​

Opening Windows ​

typescript
// From within a component
const openWindow = useOpenWindow()

// Open a new window in the group
await openWindow('document-new', DocumentEditor({ document: newDoc }))

// Or use the group directly
await DocumentWindows.openForData(newDoc)

Managing Multiple Instances ​

typescript
function DocumentManager() {
  const [openDocuments, setOpenDocuments] = createSignal<TextDocument[]>([])
  
  const trackDocument = (doc: TextDocument) => {
    setOpenDocuments(prev => [...prev, doc])
    DocumentWindows.openForData(doc)
  }
  
  const closeAllDocuments = async () => {
    await DocumentWindows.closeAll()
    setOpenDocuments([])
  }
  
  return VStack({
    children: [
      Button({
        title: `Close All (${openDocuments().length} open)`,
        action: closeAllDocuments,
        disabled: () => openDocuments().length === 0
      })
    ]
  })
}

Platform Adaptations ​

WindowGroup automatically adapts to different platforms:

Desktop (Electron) ​

  • Creates native BrowserWindow instances
  • Full window management (minimize, maximize, close)
  • Multi-monitor support

Web Browser ​

  • Uses popup windows when supported
  • Falls back to modal overlays
  • Portal rendering for embedded content

Mobile ​

  • Stack-based navigation
  • Sheet presentations
  • Gesture-based dismissal

Best Practices ​

1. Use Meaningful IDs ​

typescript
// Good: descriptive, unique IDs
WindowGroup({ id: 'markdown-editor', ... })
WindowGroup({ id: 'image-gallery', ... })

// Bad: generic IDs
WindowGroup({ id: 'group1', ... })
WindowGroup({ id: 'windows', ... })

2. Handle Data Changes ​

typescript
function DocumentEditor({ document }: { document: Document }) {
  // Listen for external changes
  createEffect(() => {
    if (document.isDeleted) {
      // Close this window
      useDismissWindow()('current-window-id')
    }
  })
  
  return DocumentView({ document })
}

3. Optimize for Large Numbers of Windows ​

typescript
// Limit concurrent windows
const MAX_WINDOWS = 10

const openDocument = async (doc: Document) => {
  const openWindows = DocumentWindows.getWindows()
  
  if (openWindows.length >= MAX_WINDOWS) {
    // Close oldest window
    const oldest = openWindows[0]
    await oldest.close()
  }
  
  await DocumentWindows.openForData(doc)
}

4. Handle Unsaved Changes ​

typescript
function DocumentEditor({ document }: { document: Document }) {
  const [hasChanges, setHasChanges] = createSignal(false)
  
  // Prevent closing with unsaved changes
  useEffect(() => {
    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
      if (hasChanges()) {
        e.preventDefault()
        e.returnValue = 'You have unsaved changes. Are you sure you want to close?'
      }
    }
    
    window.addEventListener('beforeunload', handleBeforeUnload)
    return () => window.removeEventListener('beforeunload', handleBeforeUnload)
  })
  
  return DocumentEditor({ 
    document, 
    onChange: () => setHasChanges(true),
    onSave: () => setHasChanges(false)
  })
}

Released under the MPL-2.0 License.