Skip to content

Commit 7680aee

Browse files
committed
Add window management API and display tracking
Introduces window management support to the native API, including new bindings for window and window manager operations, Dart abstractions for windows, and event handling. The display example now tracks and visualizes the current window and cursor position. Context menu positioning is updated to use the active window's bounds. Exports for window-related modules are added to the public API.
1 parent 218669e commit 7680aee

8 files changed

Lines changed: 2148 additions & 15 deletions

File tree

examples/display_example/lib/main.dart

Lines changed: 218 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'package:flutter/material.dart';
23
import 'package:nativeapi/nativeapi.dart';
34

@@ -34,11 +35,86 @@ class _DisplayManagerPageState extends State<DisplayManagerPage> {
3435
Display? _selectedDisplay;
3536
bool _isLoading = true;
3637
String? _errorMessage;
38+
Window? _currentWindow;
39+
Offset _cursorPosition = Offset.zero;
40+
Timer? _updateTimer;
41+
List<int> _windowListenerIds = [];
3742

3843
@override
3944
void initState() {
4045
super.initState();
4146
_loadDisplays();
47+
_startTracking();
48+
}
49+
50+
@override
51+
void dispose() {
52+
_updateTimer?.cancel();
53+
final windowManager = WindowManager.instance;
54+
for (final listenerId in _windowListenerIds) {
55+
windowManager.removeListener(listenerId);
56+
}
57+
_windowListenerIds.clear();
58+
super.dispose();
59+
}
60+
61+
void _startTracking() {
62+
// Update cursor position and current window periodically
63+
_updateTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
64+
if (mounted) {
65+
_updateCursorAndWindow();
66+
}
67+
});
68+
69+
// Listen to window events to update current window
70+
final windowManager = WindowManager.instance;
71+
_windowListenerIds.add(
72+
windowManager.addCallbackListener<WindowFocusedEvent>((event) {
73+
if (mounted) {
74+
_updateCurrentWindow();
75+
}
76+
}),
77+
);
78+
_windowListenerIds.add(
79+
windowManager.addCallbackListener<WindowMovedEvent>((event) {
80+
if (mounted) {
81+
_updateCurrentWindow();
82+
}
83+
}),
84+
);
85+
_windowListenerIds.add(
86+
windowManager.addCallbackListener<WindowResizedEvent>((event) {
87+
if (mounted) {
88+
_updateCurrentWindow();
89+
}
90+
}),
91+
);
92+
}
93+
94+
void _updateCursorAndWindow() {
95+
final displayManager = DisplayManager.instance;
96+
final cursorPos = displayManager.getCursorPosition();
97+
98+
final windowManager = WindowManager.instance;
99+
final currentWindow = windowManager.getCurrent();
100+
101+
if (mounted) {
102+
setState(() {
103+
_cursorPosition = cursorPos;
104+
_currentWindow = currentWindow;
105+
});
106+
}
107+
}
108+
109+
void _updateCurrentWindow() {
110+
final windowManager = WindowManager.instance;
111+
final currentWindow = windowManager.getCurrent();
112+
113+
if (mounted) {
114+
setState(() {
115+
_currentWindow = currentWindow;
116+
});
117+
}
42118
}
43119

44120
Future<void> _loadDisplays() async {
@@ -196,6 +272,8 @@ class _DisplayManagerPageState extends State<DisplayManagerPage> {
196272
displays: _displays,
197273
selectedDisplay: _selectedDisplay,
198274
onDisplayTap: _selectDisplay,
275+
currentWindow: _currentWindow,
276+
cursorPosition: _cursorPosition,
199277
),
200278
),
201279
if (_selectedDisplay != null)
@@ -214,6 +292,8 @@ class _DisplayManagerPageState extends State<DisplayManagerPage> {
214292
displays: _displays,
215293
selectedDisplay: _selectedDisplay,
216294
onDisplayTap: _selectDisplay,
295+
currentWindow: _currentWindow,
296+
cursorPosition: _cursorPosition,
217297
),
218298
),
219299
if (_selectedDisplay != null)
@@ -233,12 +313,16 @@ class DisplayCanvas extends StatelessWidget {
233313
final List<Display> displays;
234314
final Display? selectedDisplay;
235315
final Function(Display) onDisplayTap;
316+
final Window? currentWindow;
317+
final Offset cursorPosition;
236318

237319
const DisplayCanvas({
238320
super.key,
239321
required this.displays,
240322
required this.selectedDisplay,
241323
required this.onDisplayTap,
324+
this.currentWindow,
325+
this.cursorPosition = Offset.zero,
242326
});
243327

244328
@override
@@ -281,9 +365,14 @@ class DisplayCanvas extends StatelessWidget {
281365
width: bounds.width * scale,
282366
height: bounds.height * scale,
283367
child: Stack(
284-
children: displays
285-
.map((display) => _buildDisplay(display, bounds, scale))
286-
.toList(),
368+
children: [
369+
...displays
370+
.map((display) => _buildDisplay(display, bounds, scale))
371+
.toList(),
372+
if (currentWindow != null)
373+
_buildWindow(currentWindow!, bounds, scale),
374+
_buildCursor(bounds, scale),
375+
],
287376
),
288377
),
289378
);
@@ -467,12 +556,6 @@ class DisplayCanvas extends StatelessWidget {
467556
return Colors.grey[200]!;
468557
}
469558

470-
Color _getDisplayBorderColor(bool isSelected, bool isPrimary) {
471-
if (isSelected) return Colors.blue[600]!;
472-
if (isPrimary) return Colors.green[600]!;
473-
return Colors.grey[400]!;
474-
}
475-
476559
List<Color> _getWorkAreaGradient(bool isSelected, bool isPrimary) {
477560
if (isSelected) {
478561
return [Colors.blue[400]!, Colors.blue[600]!];
@@ -482,6 +565,132 @@ class DisplayCanvas extends StatelessWidget {
482565
}
483566
return [Colors.grey[300]!, Colors.grey[400]!];
484567
}
568+
569+
Widget _buildWindow(Window window, Rect bounds, double scale) {
570+
try {
571+
final windowBounds = window.bounds;
572+
final windowLeft = (windowBounds.left - bounds.left) * scale;
573+
final windowTop = (windowBounds.top - bounds.top) * scale;
574+
final windowWidth = windowBounds.width * scale;
575+
final windowHeight = windowBounds.height * scale;
576+
577+
// Only draw if window is visible within bounds
578+
if (windowLeft + windowWidth < 0 ||
579+
windowTop + windowHeight < 0 ||
580+
windowLeft > bounds.width * scale ||
581+
windowTop > bounds.height * scale) {
582+
return const SizedBox.shrink();
583+
}
584+
585+
return Positioned(
586+
left: windowLeft,
587+
top: windowTop,
588+
child: Container(
589+
width: windowWidth,
590+
height: windowHeight,
591+
decoration: BoxDecoration(
592+
border: Border.all(
593+
color: Colors.orange,
594+
width: 2,
595+
),
596+
boxShadow: [
597+
BoxShadow(
598+
color: Colors.orange.withOpacity(0.3),
599+
blurRadius: 4,
600+
offset: const Offset(0, 2),
601+
),
602+
],
603+
),
604+
child: Stack(
605+
children: [
606+
// Window background (semi-transparent)
607+
Container(
608+
color: Colors.orange.withOpacity(0.1),
609+
),
610+
// Window title bar indicator
611+
Container(
612+
height: (20 * scale).clamp(8.0, 20.0),
613+
decoration: BoxDecoration(
614+
color: Colors.orange.withOpacity(0.3),
615+
border: const Border(
616+
bottom: BorderSide(color: Colors.orange, width: 1),
617+
),
618+
),
619+
padding: EdgeInsets.symmetric(
620+
horizontal: (8 * scale).clamp(4.0, 8.0),
621+
),
622+
child: Row(
623+
children: [
624+
Icon(
625+
Icons.window,
626+
size: (12 * scale).clamp(8.0, 12.0),
627+
color: Colors.orange[900],
628+
),
629+
SizedBox(width: (4 * scale).clamp(2.0, 4.0)),
630+
Expanded(
631+
child: Text(
632+
window.title.isNotEmpty ? window.title : 'Window',
633+
style: TextStyle(
634+
fontSize: (10 * scale).clamp(6.0, 10.0),
635+
color: Colors.orange[900],
636+
fontWeight: FontWeight.bold,
637+
overflow: TextOverflow.ellipsis,
638+
),
639+
),
640+
),
641+
],
642+
),
643+
),
644+
],
645+
),
646+
),
647+
);
648+
} catch (e) {
649+
// Window might have been destroyed, return empty widget
650+
return const SizedBox.shrink();
651+
}
652+
}
653+
654+
Widget _buildCursor(Rect bounds, double scale) {
655+
final cursorLeft = (cursorPosition.dx - bounds.left) * scale;
656+
final cursorTop = (cursorPosition.dy - bounds.top) * scale;
657+
658+
// Only draw if cursor is within bounds
659+
if (cursorLeft < 0 ||
660+
cursorTop < 0 ||
661+
cursorLeft > bounds.width * scale ||
662+
cursorTop > bounds.height * scale) {
663+
return const SizedBox.shrink();
664+
}
665+
666+
return Positioned(
667+
left: cursorLeft - 8,
668+
top: cursorTop - 8,
669+
child: Container(
670+
width: 16,
671+
height: 16,
672+
decoration: BoxDecoration(
673+
color: Colors.red.withOpacity(0.8),
674+
shape: BoxShape.circle,
675+
border: Border.all(color: Colors.white, width: 2),
676+
boxShadow: [
677+
BoxShadow(
678+
color: Colors.red.withOpacity(0.5),
679+
blurRadius: 4,
680+
spreadRadius: 2,
681+
),
682+
],
683+
),
684+
child: const Center(
685+
child: Icon(
686+
Icons.mouse,
687+
size: 8,
688+
color: Colors.white,
689+
),
690+
),
691+
),
692+
);
693+
}
485694
}
486695

487696
class DisplayDetails extends StatelessWidget {

packages/cnativeapi/ffigen.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ headers:
2020
- "src/libnativeapi/src/capi/string_utils_c.h"
2121
- "src/libnativeapi/src/capi/tray_icon_c.h"
2222
- "src/libnativeapi/src/capi/tray_manager_c.h"
23+
- "src/libnativeapi/src/capi/window_c.h"
24+
- "src/libnativeapi/src/capi/window_manager_c.h"
2325

2426
include-directives:
2527
- "src/libnativeapi/src/capi/accessibility_manager_c.h"
@@ -35,6 +37,8 @@ headers:
3537
- "src/libnativeapi/src/capi/string_utils_c.h"
3638
- "src/libnativeapi/src/capi/tray_icon_c.h"
3739
- "src/libnativeapi/src/capi/tray_manager_c.h"
40+
- "src/libnativeapi/src/capi/window_c.h"
41+
- "src/libnativeapi/src/capi/window_manager_c.h"
3842

3943
preamble: |
4044
// ignore_for_file: always_specify_types

0 commit comments

Comments
 (0)