@@ -369,7 +369,7 @@ class _WindowManagerPageState extends State<WindowManagerPage> {
369369 }
370370}
371371
372- class WindowCanvas extends StatelessWidget {
372+ class WindowCanvas extends StatefulWidget {
373373 final List <Window > windows;
374374 final List <Display > displays;
375375 final Window ? selectedWindow;
@@ -383,9 +383,60 @@ class WindowCanvas extends StatelessWidget {
383383 required this .onWindowTap,
384384 });
385385
386+ @override
387+ State <WindowCanvas > createState () => _WindowCanvasState ();
388+ }
389+
390+ class _WindowCanvasState extends State <WindowCanvas > {
391+ final TransformationController _transformationController =
392+ TransformationController ();
393+ double _baseScale = 1.0 ;
394+
395+ @override
396+ void initState () {
397+ super .initState ();
398+ _transformationController.addListener (_onTransformationChanged);
399+ }
400+
401+ @override
402+ void dispose () {
403+ _transformationController.removeListener (_onTransformationChanged);
404+ _transformationController.dispose ();
405+ super .dispose ();
406+ }
407+
408+ void _onTransformationChanged () {
409+ final currentScale = _transformationController.value.getMaxScaleOnAxis ();
410+ if (currentScale != _baseScale) {
411+ setState (() {
412+ _baseScale = currentScale;
413+ });
414+ }
415+ }
416+
417+ void _zoomIn () {
418+ final currentScale = _transformationController.value.getMaxScaleOnAxis ();
419+ final newScale = (currentScale * 1.2 ).clamp (0.5 , 5.0 );
420+ _transformationController.value = Matrix4 .identity ().scaled (newScale);
421+ }
422+
423+ void _zoomOut () {
424+ final currentScale = _transformationController.value.getMaxScaleOnAxis ();
425+ final newScale = (currentScale / 1.2 ).clamp (0.5 , 5.0 );
426+ _transformationController.value = Matrix4 .identity ().scaled (newScale);
427+ }
428+
429+ void _resetZoom () {
430+ _transformationController.value = Matrix4 .identity ();
431+ }
432+
433+ void _fitToScreen () {
434+ _resetZoom ();
435+ }
436+
386437 @override
387438 Widget build (BuildContext context) {
388- if (windows.isEmpty && displays.isEmpty) {
439+ if (widget. windows.isEmpty && widget. displays.isEmpty) {
389440 return const Center (child: Text ('No windows or displays available' ));
390441 }
391442
@@ -401,9 +452,87 @@ class WindowCanvas extends StatelessWidget {
401452 ),
402453 borderRadius: BorderRadius .circular (12 ),
403454 ),
404- padding: const EdgeInsets .all (20 ),
405- child: LayoutBuilder (
406- builder: (context, constraints) => _buildWindowLayout (constraints),
455+ child: Stack (
456+ children: [
457+ // Main canvas with zoom support
458+ Padding (
459+ padding: const EdgeInsets .all (20 ),
460+ child: LayoutBuilder (
461+ builder: (context, constraints) => InteractiveViewer (
462+ transformationController: _transformationController,
463+ minScale: 0.5 ,
464+ maxScale: 5.0 ,
465+ boundaryMargin: const EdgeInsets .all (20 ),
466+ panEnabled: true ,
467+ scaleEnabled: true ,
468+ child: _buildWindowLayout (constraints),
469+ ),
470+ ),
471+ ),
472+ // Zoom controls
473+ Positioned (
474+ top: 8 ,
475+ right: 8 ,
476+ child: Container (
477+ decoration: BoxDecoration (
478+ color: Colors .white.withOpacity (0.9 ),
479+ borderRadius: BorderRadius .circular (8 ),
480+ boxShadow: [
481+ BoxShadow (
482+ color: Colors .black.withOpacity (0.1 ),
483+ blurRadius: 4 ,
484+ offset: const Offset (0 , 2 ),
485+ ),
486+ ],
487+ ),
488+ child: Column (
489+ mainAxisSize: MainAxisSize .min,
490+ children: [
491+ IconButton (
492+ icon: const Icon (Icons .add),
493+ onPressed: _zoomIn,
494+ tooltip: 'Zoom In' ,
495+ iconSize: 20 ,
496+ ),
497+ const Divider (height: 1 ),
498+ IconButton (
499+ icon: const Icon (Icons .remove),
500+ onPressed: _zoomOut,
501+ tooltip: 'Zoom Out' ,
502+ iconSize: 20 ,
503+ ),
504+ const Divider (height: 1 ),
505+ IconButton (
506+ icon: const Icon (Icons .fit_screen),
507+ onPressed: _fitToScreen,
508+ tooltip: 'Fit to Screen' ,
509+ iconSize: 20 ,
510+ ),
511+ ],
512+ ),
513+ ),
514+ ),
515+ // Scale indicator
516+ Positioned (
517+ bottom: 8 ,
518+ left: 8 ,
519+ child: Container (
520+ padding: const EdgeInsets .symmetric (horizontal: 8 , vertical: 4 ),
521+ decoration: BoxDecoration (
522+ color: Colors .black.withOpacity (0.6 ),
523+ borderRadius: BorderRadius .circular (4 ),
524+ ),
525+ child: Text (
526+ '${(_baseScale * 100 ).toStringAsFixed (0 )}%' ,
527+ style: const TextStyle (
528+ color: Colors .white,
529+ fontSize: 12 ,
530+ fontWeight: FontWeight .bold,
531+ ),
532+ ),
533+ ),
534+ ),
535+ ],
407536 ),
408537 ),
409538 );
@@ -422,19 +551,21 @@ class WindowCanvas extends StatelessWidget {
422551 final scaleY = constraints.maxHeight / bounds.height;
423552 final scale = (scaleX < scaleY ? scaleX : scaleY) * 0.85 ;
424553
425- return Center (
426- child: SizedBox (
427- width: bounds.width * scale,
428- height: bounds.height * scale,
429- child: Stack (
430- children: [
431- // Draw displays first as background
432- if (displays.isNotEmpty)
433- ...displays.map ((display) => _buildDisplay (display, bounds, scale)).toList (),
434- // Draw windows on top
435- ...windows.map ((window) => _buildWindow (window, bounds, scale)).toList (),
436- ],
437- ),
554+ return SizedBox (
555+ width: bounds.width * scale,
556+ height: bounds.height * scale,
557+ child: Stack (
558+ children: [
559+ // Draw displays first as background
560+ if (widget.displays.isNotEmpty)
561+ ...widget.displays
562+ .map ((display) => _buildDisplay (display, bounds, scale))
563+ .toList (),
564+ // Draw windows on top
565+ ...widget.windows
566+ .map ((window) => _buildWindow (window, bounds, scale))
567+ .toList (),
568+ ],
438569 ),
439570 );
440571 }
@@ -446,8 +577,8 @@ class WindowCanvas extends StatelessWidget {
446577 double maxY = double .negativeInfinity;
447578
448579 // First, include all displays if available
449- if (displays.isNotEmpty) {
450- for (final display in displays) {
580+ if (widget. displays.isNotEmpty) {
581+ for (final display in widget. displays) {
451582 final position = display.position;
452583 final size = display.size;
453584 minX = minX < position.dx ? minX : position.dx;
@@ -462,7 +593,7 @@ class WindowCanvas extends StatelessWidget {
462593 }
463594
464595 // Then, include all windows
465- for (final window in windows) {
596+ for (final window in widget. windows) {
466597 try {
467598 final windowBounds = window.bounds;
468599 minX = minX < windowBounds.left ? minX : windowBounds.left;
@@ -585,7 +716,7 @@ class WindowCanvas extends StatelessWidget {
585716 final contentWidth = contentBounds.width * scale;
586717 final contentHeight = contentBounds.height * scale;
587718
588- final isSelected = selectedWindow? .id == window.id;
719+ final isSelected = widget. selectedWindow? .id == window.id;
589720
590721 // Only draw if window is visible within bounds
591722 if (windowLeft + windowWidth < 0 ||
@@ -599,7 +730,7 @@ class WindowCanvas extends StatelessWidget {
599730 left: windowLeft,
600731 top: windowTop,
601732 child: GestureDetector (
602- onTap: () => onWindowTap (window),
733+ onTap: () => widget. onWindowTap (window),
603734 child: AnimatedContainer (
604735 duration: const Duration (milliseconds: 200 ),
605736 width: windowWidth,
0 commit comments