How ChatMCP Handles Window Management on Desktop Platforms Using `window_manager`
ChatMCP uses the window_manager Flutter plugin to initialize frameless windows with custom sizes, enables toolbar dragging via DragToMoveArea, and exposes minimize, maximize, and close controls through platform-guarded UI widgets.
ChatMCP is an open-source Flutter-based chat client that leverages the window_manager plugin to deliver native desktop window experiences on Windows, macOS, and Linux. When running on desktop platforms, the application hijacks the native title bar, implements custom draggable toolbars, and provides programmatic control over window states. This article examines the exact implementation details found in the repository's Dart source files.
Desktop Platform Detection and Initialization
Before invoking any desktop-specific APIs, ChatMCP determines the current platform using utility constants defined in lib/utils/platform.dart.
Checking for Desktop Environments
The application defines a boolean flag that evaluates to true only when running natively on Windows, macOS, or Linux:
final bool kIsDesktop = !kIsWeb && (kIsLinux || kIsWindows || kIsMacOS);
This kIsDesktop constant guards all desktop-specific logic throughout the UI, ensuring mobile and web builds never invoke unsupported native methods.
Early Initialization in main.dart
When kIsDesktop is true, lib/main.dart initializes the window manager before any widgets render:
if (kIsDesktop) {
await wm.windowManager.ensureInitialized();
final wm.WindowOptions windowOptions = wm.WindowOptions(
size: Size(1200, 800),
minimumSize: Size(400, 600),
center: true,
backgroundColor: Colors.transparent,
titleBarStyle: wm.TitleBarStyle.hidden,
windowButtonVisibility: true,
);
await wm.windowManager.waitUntilReadyToShow(windowOptions, () async {
try {
await wm.windowManager.show();
await wm.windowManager.focus();
} catch (e) {
Logger.root.warning('Window initialization error: $e');
}
});
}
The ensureInitialized() method loads the native bindings, while waitUntilReadyToShow delays the window presentation until the specified options are applied.
Configuring Frameless Window Behavior
ChatMCP opts for a custom title bar by hiding the native OS chrome and implementing its own controls.
WindowOptions and Hidden Title Bars
The WindowOptions constructor in main.dart configures several critical parameters:
size: Size(1200, 800)— Sets the default window dimensions on launch.minimumSize: Size(400, 600)— Prevents the user from resizing below usable thresholds.titleBarStyle: wm.TitleBarStyle.hidden— Removes the native title bar, allowing Flutter to draw a custom toolbar.backgroundColor: Colors.transparent— Enables seamless blending with custom frame designs.
Showing and Focusing the Window
Inside the waitUntilReadyToShow callback, the application explicitly calls wm.windowManager.show() to make the window visible and wm.windowManager.focus() to bring it to the foreground. A brief delay ensures the native window completes its setup before the UI appears.
Implementing Draggable Custom Chrome
Without a native title bar, users need an alternative way to move the window. ChatMCP solves this by making the top toolbar a draggable zone.
DragToMoveArea for Frameless Windows
In lib/page/layout/widgets/top_toolbar.dart, the toolbar conditionally wraps its content with DragToMoveArea:
child: kIsDesktop
? wm.DragToMoveArea(child: _buildToolbarContent(context))
: _buildToolbarContent(context),
The DragToMoveArea widget intercepts mouse drag events and forwards them to the native window manager, allowing users to reposition the frameless window by dragging the custom toolbar.
Double-Tap Maximization Gesture
The toolbar also supports a double-tap gesture to toggle between maximized and restored states:
onDoubleTap: () async {
if (kIsDesktop) {
bool isMaximized = await wm.windowManager.isMaximized();
if (isMaximized) {
await wm.windowManager.unmaximize();
} else {
await wm.windowManager.maximize();
}
}
},
This logic checks the current window state asynchronously before invoking the appropriate API.
Custom Window Control Buttons
Since the native title bar is hidden, ChatMCP provides its own minimize, maximize, and close buttons in lib/page/layout/widgets/window_controls.dart.
The WindowControls Widget Implementation
The widget defines three control buttons with distinct behaviors:
Minimize Button:
_WindowButton(
icon: Icons.remove,
onPressed: () async => await wm.windowManager.minimize(),
tooltip: l10n.minimize,
),
Maximize/Restore Toggle:
_WindowButton(
icon: Icons.crop_square,
onPressed: () async {
bool isMaximized = await wm.windowManager.isMaximized();
if (isMaximized) {
await wm.windowManager.unmaximize();
} else {
await wm.windowManager.maximize();
}
},
tooltip: l10n.maximize,
),
Close Button:
_WindowButton(
icon: Icons.close,
onPressed: () async => await wm.windowManager.close(),
tooltip: l10n.close,
isCloseButton: true,
),
State-Aware Maximize and Restore
The maximize button dynamically changes behavior based on the window's current state. When isMaximized() returns true, the button calls unmaximize() to restore the previous size; otherwise, it calls maximize() to fill the screen.
Additional Window Management Techniques
Beyond the core setup, window_manager enables runtime manipulation of window properties.
Runtime Window Resizing
Developers can adjust window dimensions programmatically after initialization:
import 'package:window_manager/window_manager.dart' as wm;
Future<void> resizeWindow() async {
await wm.windowManager.setSize(const Size(1600, 900));
}
Taskbar Visibility Control
To hide the window from the taskbar or dock:
await wm.windowManager.setSkipTaskbar(true);
Listening for Window Events
The plugin supports event listeners for state changes:
wm.windowManager.addListener(MyWindowListener());
class MyWindowListener extends wm.WindowListener {
@override
void onWindowFocus() {
debugPrint('Window gained focus');
}
@override
void onWindowBlur() {
debugPrint('Window lost focus');
}
}
Summary
- ChatMCP initializes
window_managerearly inlib/main.dartonly whenkIsDesktopis true, usingWindowOptionsto configure a 1200×800 frameless window centered on screen. - The
DragToMoveAreawidget wraps the toolbar inlib/page/layout/widgets/top_toolbar.dart, enabling users to drag the frameless window by its custom chrome. - Double-tapping the toolbar checks
isMaximized()to toggle asynchronously betweenmaximize()andunmaximize()states. - The
WindowControlswidget provides explicit minimize, maximize/restore, and close buttons that invoke correspondingwindow_managerAPIs. - Runtime window manipulation leverages
setSize(),setSkipTaskbar(), and event listeners for dynamic desktop window behavior.
Frequently Asked Questions
Why does ChatMCP hide the native title bar on desktop?
ChatMCP hides the native title bar using TitleBarStyle.hidden in WindowOptions to implement a custom frameless design that matches the Flutter UI theme. This allows the application to draw its own gradient backgrounds and custom toolbar widgets while maintaining native window controls through the window_manager API.
How does ChatMCP allow dragging a window without a title bar?
The application wraps the toolbar content with the DragToMoveArea widget from the window_manager package, as seen in lib/page/layout/widgets/top_toolbar.dart. This widget forwards mouse drag events directly to the native window manager, enabling standard window repositioning behavior despite the absence of native chrome.
Can I change the default window size in ChatMCP?
Yes, the default size is defined in lib/main.dart within the WindowOptions constructor as Size(1200, 800). You can modify these dimensions before building, or call wm.windowManager.setSize() at runtime to dynamically resize the window based on user preferences or screen constraints.
What happens if window_manager fails to initialize?
The initialization logic in lib/main.dart wraps the show() and focus() calls inside a try-catch block that logs warnings via Logger.root.warning. This defensive programming ensures that if the native window manager fails to bind, the application continues running rather than crashing, though the window may appear with default system styling.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →