Adding the SatelliteWindowController interface to `_window.dart` (#182903)

## What's new?
- Added the concept of a `SatelliteWindowController` and
`SatelliteWindow` widget to the `_window.dart` API
- Wrote tests for it
- Throwing "unsupported" for all existing `WindowingOwner`s when trying
to create it
- Stubbed it in the example application
- Implemented a "happy" implementation in the `flutter_test` package

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.
diff --git a/examples/api/lib/widgets/windows/satellite.0.dart b/examples/api/lib/widgets/windows/satellite.0.dart
new file mode 100644
index 0000000..6ed7262
--- /dev/null
+++ b/examples/api/lib/widgets/windows/satellite.0.dart
@@ -0,0 +1,125 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// TODO(mattkae): remove invalid_use_of_internal_member ignore comment when this API is stable.
+// See: https://github.com/flutter/flutter/issues/177586
+// ignore_for_file: invalid_use_of_internal_member
+// ignore_for_file: implementation_imports
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/_window.dart';
+import 'package:flutter/src/widgets/_window_positioner.dart';
+
+void main() {
+  try {
+    WidgetsFlutterBinding.ensureInitialized();
+    runWidget(
+      RegularWindow(
+        controller: RegularWindowController(
+          preferredSize: const Size(800, 600),
+          preferredConstraints: const BoxConstraints(
+            minWidth: 640,
+            minHeight: 480,
+          ),
+          title: 'Example Window',
+        ),
+        child: const MaterialApp(home: MyApp()),
+      ),
+    );
+  } on UnsupportedError catch (_) {
+    // TODO(mattkae): Remove this catch block when satellite windows are supported in tests.
+    // For now, we need to catch the error so that the API smoke tests pass.
+    runApp(
+      MaterialApp(
+        home: Scaffold(body: Center(child: Text('Unsupported'))),
+      ),
+    );
+  }
+}
+
+class MyApp extends StatefulWidget {
+  const MyApp({super.key});
+
+  @override
+  State<MyApp> createState() => _MyAppState();
+}
+
+class _CallbackSatelliteDelegate extends SatelliteWindowControllerDelegate {
+  _CallbackSatelliteDelegate({required this.onDestroyCallback});
+
+  final VoidCallback onDestroyCallback;
+
+  @override
+  void onWindowDestroyed() {
+    onDestroyCallback();
+  }
+}
+
+class _MyAppState extends State<MyApp> {
+  SatelliteWindowController? _satelliteController;
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> children = <Widget>[
+      ElevatedButton(
+        onPressed: () {
+          setState(() {
+            _satelliteController ??= SatelliteWindowController(
+              parent: WindowScope.of(context),
+              initialPositioner: const WindowPositioner(
+                parentAnchor: WindowPositionerAnchor.right,
+                childAnchor: WindowPositionerAnchor.left,
+              ),
+              preferredSize: const Size(300, 200),
+              title: 'Satellite Window',
+              delegate: _CallbackSatelliteDelegate(
+                onDestroyCallback: () {
+                  setState(() {
+                    _satelliteController = null;
+                  });
+                },
+              ),
+            );
+          });
+        },
+        child: const Text('Show Satellite'),
+      ),
+    ];
+
+    if (_satelliteController != null) {
+      children.add(
+        SatelliteWindow(
+          controller: _satelliteController!,
+          child: Container(
+            padding: const EdgeInsets.all(8),
+            color: Colors.black,
+            child: Column(
+              mainAxisSize: MainAxisSize.min,
+              children: <Widget>[
+                const Text(
+                  'This is a satellite window',
+                  style: TextStyle(color: Colors.white),
+                ),
+                const SizedBox(height: 8),
+                ElevatedButton(
+                  onPressed: () {
+                    setState(() {
+                      _satelliteController?.destroy();
+                    });
+                  },
+                  child: const Text('Close'),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+    }
+
+    return Scaffold(
+      body: Center(
+        child: Row(mainAxisSize: MainAxisSize.min, children: children),
+      ),
+    );
+  }
+}
diff --git a/examples/api/test/widgets/windows/satellite.0_test.dart b/examples/api/test/widgets/windows/satellite.0_test.dart
new file mode 100644
index 0000000..f2f3bef
--- /dev/null
+++ b/examples/api/test/widgets/windows/satellite.0_test.dart
@@ -0,0 +1,16 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_api_samples/widgets/windows/satellite.0.dart'
+    as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('Calling satellite main returns normally', (
+    WidgetTester tester,
+  ) async {
+    expect(() => example.main(), returnsNormally);
+    expect(find.text('Unsupported'), findsOneWidget);
+  });
+}
diff --git a/examples/multiple_windows/lib/app/main_window.dart b/examples/multiple_windows/lib/app/main_window.dart
index 35e636e..e83ce70 100644
--- a/examples/multiple_windows/lib/app/main_window.dart
+++ b/examples/multiple_windows/lib/app/main_window.dart
@@ -141,6 +141,7 @@
         controller: tooltip,
       ),
       PopupWindowController() => null,
+      SatelliteWindowController() => null,
     };
   }
 
@@ -150,6 +151,7 @@
       DialogWindowController() => 'Dialog',
       TooltipWindowController() => 'Tooltip',
       PopupWindowController() => 'Popup',
+      SatelliteWindowController() => 'Satellite',
     };
   }
 
diff --git a/examples/multiple_windows/lib/app/window_content.dart b/examples/multiple_windows/lib/app/window_content.dart
index 5b1d7c1..dac69e1 100644
--- a/examples/multiple_windows/lib/app/window_content.dart
+++ b/examples/multiple_windows/lib/app/window_content.dart
@@ -49,6 +49,7 @@
         ),
       ),
       PopupWindowController() => throw UnimplementedError(),
+      SatelliteWindowController() => throw UnimplementedError(),
     };
   }
 }
diff --git a/packages/flutter/lib/src/widgets/_window.dart b/packages/flutter/lib/src/widgets/_window.dart
index a108d56..bfec2df 100644
--- a/packages/flutter/lib/src/widgets/_window.dart
+++ b/packages/flutter/lib/src/widgets/_window.dart
@@ -890,6 +890,248 @@
   void setConstraints(BoxConstraints constraints);
 }
 
+/// Delegate class for satellite window controller.
+///
+/// {@macro flutter.widgets.windowing.experimental}
+///
+/// See also:
+///
+///  * [SatelliteWindowController], the controller that creates and manages a satellite window.
+///  * [SatelliteWindow], the widget for a satellite window.
+@internal
+mixin class SatelliteWindowControllerDelegate {
+  /// Invoked when the user attempts to close the window.
+  ///
+  /// The default implementation destroys the window. Subclasses
+  /// can override the behavior to delay or prevent the window from closing.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  ///
+  /// See also:
+  ///
+  /// * [onWindowDestroyed], which is invoked after the window is closed.
+  @internal
+  void onWindowCloseRequested(SatelliteWindowController controller) {
+    if (!isWindowingEnabled) {
+      throw UnsupportedError(_kWindowingDisabledErrorMessage);
+    }
+
+    controller.destroy();
+  }
+
+  /// Invoked after the window is closed.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  ///
+  /// See also:
+  ///
+  /// * [onWindowCloseRequested], which is invoked when the user attempts to close the window.
+  @internal
+  void onWindowDestroyed() {
+    if (!isWindowingEnabled) {
+      throw UnsupportedError(_kWindowingDisabledErrorMessage);
+    }
+  }
+}
+
+/// A controller for a satellite window.
+///
+/// A satellite window is an auxiliary window to a regular or dialog window.
+/// Satellite windows are initiially placed using a [WindowPositioner]. Afterwards,
+/// the satellite window maintains its position relative to its parent. It is
+/// hidden if the application becomes fullscreen or maximized.
+///
+/// Satellite windows may be resized and moved by the user. After being moved by
+/// the user, the satellite window will retain its new position relative to its
+/// parent such that when its parent moves, the satellite will be moved by the same
+/// offset.
+///
+/// A satellite may be reparented. For example, if an application has two documents
+/// open, the satellite may choose to reparent to the active document such that
+/// closing one document will not cause the satellite to close. This behavior
+/// may be implemented at the library level, such as in the Material API.
+/// Reparenting a satellite will not change the current absolute position of the
+/// satellite.
+///
+/// Upon construction, the window is created for the platform with the provided
+/// properties.
+///
+/// This class does not interact with the widget tree. Instead, it is typically
+/// provided to the [SatelliteWindow] widget, who does the work of rendering the
+/// content inside of this window.
+///
+/// The user of this class is responsible for managing the lifecycle of the window.
+/// When the window is no longer needed, the user should call [destroy] on this
+/// controller to release the resources associated with the window.
+///
+/// If the parent window of the satellite is destroyed, then the satellite will
+/// be destroyed as well. The user does not need to explicitly call [destroy]
+/// in this case.
+///
+/// [SatelliteWindowControllerDelegate.onWindowDestroyed]
+/// will be called when the window is destroyed.
+///
+/// {@tool snippet}
+/// An example usage of [SatelliteWindowController] looks like:
+///
+/// ** See code in examples/api/lib/widgets/windows/satellite.0.dart **
+/// {@end-tool}
+///
+/// Children of a [SatelliteWindow] widget can access the [SatelliteWindowController]
+/// via the [WindowScope] inherited widget.
+///
+/// {@macro flutter.widgets.windowing.experimental}
+@internal
+abstract class SatelliteWindowController extends BaseWindowController {
+  /// Creates a [SatelliteWindowController] with the provided properties.
+  ///
+  /// Upon construction, the window is created by the platform.
+  ///
+  /// The [parent] argument specifies the parent window of this satellite.
+  ///
+  /// The [initialPositioner] argument specifies how the satellite should be positioned
+  /// relative to the [initialAnchorRect]. The positioner is only applied the first
+  /// time that the window is shown. Afterwards, the user may move and resize
+  /// the window to their preference. If the [parent] of the satellite moves, the
+  /// satellite is moved relative to its parent. The satellite will always retain
+  /// its current offset from its parent unless it is moved independently.
+  ///
+  /// The [initialAnchorRect] argument specifies the rectangle in the parent's coordinate
+  /// space to which the tooltip is anchored. If it is `null`, then the satellite
+  /// is position relative to the parent window, including its decorations.
+  ///
+  /// {@macro flutter.widgets.windowing.constraints}
+  ///
+  /// The [title] argument configures the window's title.
+  /// If omitted, some platforms might fall back to the app's name.
+  ///
+  /// The [delegate] argument can be used to listen to the window's
+  /// lifecycle. For example, it can be used to save state before
+  /// a window is closed.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  factory SatelliteWindowController({
+    required BaseWindowController parent,
+    required WindowPositioner initialPositioner,
+    Rect? initialAnchorRect,
+    Size? preferredSize,
+    BoxConstraints? preferredConstraints,
+    String? title,
+    SatelliteWindowControllerDelegate? delegate,
+  }) {
+    if (!isWindowingEnabled) {
+      throw UnsupportedError(_kWindowingDisabledErrorMessage);
+    }
+
+    if (preferredSize != null && preferredConstraints != null) {
+      assert(preferredConstraints.isSatisfiedBy(preferredSize));
+    }
+
+    final WindowingOwner owner = WidgetsBinding.instance.windowingOwner;
+    return owner.createSatelliteWindowController(
+      delegate: delegate ?? SatelliteWindowControllerDelegate(),
+      parent: parent,
+      initialAnchorRect: initialAnchorRect,
+      initialPositioner: initialPositioner,
+      preferredSize: preferredSize,
+      preferredConstraints: preferredConstraints,
+      title: title,
+    );
+  }
+
+  /// Creates an empty [SatelliteWindowController].
+  ///
+  /// This method is only intended to be used by subclasses of the
+  /// [SatelliteWindowController].
+  ///
+  /// Users who want to instantiate a new [SatelliteWindowController] should
+  /// always use the factory method to create a controller that is valid
+  /// for their particular platform.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  @protected
+  SatelliteWindowController.empty();
+
+  /// The parent controller of this satellite.
+  ///
+  /// The satellite will be destroyed if its parent is destroyed.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  BaseWindowController get parent;
+
+  /// The current title of the window.
+  ///
+  /// The title shown in the window is controlled by the platform and may differ
+  /// from the given `title`.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  String get title;
+
+  /// Whether the window is currently activated.
+  ///
+  /// If `true` this means that the window is currently focused and
+  /// can receive user input.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  bool get isActivated;
+
+  /// Request to change the parent of the window.
+  ///
+  /// The satellite will maintain its current position after being reparented.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  void setParent(BaseWindowController parent);
+
+  /// Request change to the content size of the window.
+  ///
+  /// The [size] describes the new requested window size. If the size disagrees
+  /// with the current constraints placed upon the window, the platform might
+  /// clamp the size within the constraints.
+  ///
+  /// The platform is free to ignore this request.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  void setSize(Size size);
+
+  /// Request change to the constraints of the window.
+  ///
+  /// The [constraints] describes the new constraints that the window should
+  /// satisfy. If the constraints disagree with the current size of the window,
+  /// the platform might resize the window to satisfy the new constraints.
+  ///
+  /// The platform is free to ignore this request.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  void setConstraints(BoxConstraints constraints);
+
+  /// Request change for the window title.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  void setTitle(String title);
+
+  /// Requests that the window be displayed in its current size and position.
+  ///
+  /// The platform may also give the window input focus and bring it to the
+  /// top of the window stack. However, this behavior is platform-dependent.
+  ///
+  /// If the window is minimized, the window returns to the size and position
+  /// that it had before that state was applied. The window will also be
+  /// brought to the top of the window stack.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  void activate();
+}
+
 /// [WindowingOwner] is responsible for creating and managing window controllers.
 ///
 /// A custom implementation can be provided by setting [WidgetsBinding.windowingOwner].
@@ -960,6 +1202,24 @@
     required WindowPositioner positioner,
     required BaseWindowController parent,
   });
+
+  /// Creates a [SatelliteWindowController] with the provided properties.
+  ///
+  /// Most app developers should use [SatelliteWindowController]'s constructor
+  /// instead of calling this method directly. This method allows platforms
+  /// to inject platform-specific logic.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  SatelliteWindowController createSatelliteWindowController({
+    required SatelliteWindowControllerDelegate delegate,
+    required BaseWindowController parent,
+    required WindowPositioner initialPositioner,
+    Rect? initialAnchorRect,
+    Size? preferredSize,
+    BoxConstraints? preferredConstraints,
+    String? title,
+  });
 }
 
 /// Creates default windowing owner for standard desktop embedders.
@@ -1028,6 +1288,19 @@
   }) {
     throw UnimplementedError(errorMessage);
   }
+
+  @override
+  SatelliteWindowController createSatelliteWindowController({
+    required SatelliteWindowControllerDelegate delegate,
+    required BaseWindowController parent,
+    required WindowPositioner initialPositioner,
+    Rect? initialAnchorRect,
+    Size? preferredSize,
+    BoxConstraints? preferredConstraints,
+    String? title,
+  }) {
+    throw UnimplementedError(errorMessage);
+  }
 }
 
 /// The [RegularWindow] widget provides a way to render a regular window in the
@@ -1320,6 +1593,68 @@
   }
 }
 
+/// The [SatelliteWindow] widget provides a way to render a satellite window in the
+/// widget tree.
+///
+/// The provided [controller] creates the native window that backs
+/// the widget. The [child] widget is rendered into this newly created window.
+///
+/// When a [SatelliteWindow] widget is removed from the tree, the window that was created
+/// by the [controller] remains valid until the caller destroys it by calling
+/// [SatelliteWindowController.destroy].
+///
+/// Widgets in the same tree as the [child] widget will have access to the
+/// [SatelliteWindowController] via the [WindowScope] widget.
+///
+/// {@macro flutter.widgets.windowing.experimental}
+///
+/// See also:
+///
+/// * [SatelliteWindowController], the controller that creates and manages satellite windows.
+@internal
+class SatelliteWindow extends StatelessWidget {
+  /// Creates a satellite window widget.
+  ///
+  /// The [controller] creates the native backing window into which the
+  /// [child] widget is rendered.
+  ///
+  /// It is up to the caller to destroy the window by calling
+  /// [SatelliteWindowController.destroy] when the window is no longer needed.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  SatelliteWindow({super.key, required this.controller, required this.child}) {
+    if (!isWindowingEnabled) {
+      throw UnsupportedError(_kWindowingDisabledErrorMessage);
+    }
+  }
+
+  /// Controller for this widget.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  final SatelliteWindowController controller;
+
+  /// The content rendered into this window.
+  ///
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  final Widget child;
+
+  /// {@macro flutter.widgets.windowing.experimental}
+  @internal
+  @override
+  Widget build(BuildContext context) {
+    return ListenableBuilder(
+      listenable: controller,
+      builder: (BuildContext context, Widget? widget) => WindowScope(
+        controller: controller,
+        child: View(view: controller.rootView, child: child),
+      ),
+    );
+  }
+}
+
 enum _WindowControllerAspect { contentSize, title, activated, maximized, minimized, fullscreen }
 
 /// Provides descendants with access to the [BaseWindowController] associated with
@@ -1453,6 +1788,7 @@
       DialogWindowController() => controller.title,
       TooltipWindowController() => '',
       PopupWindowController() => '',
+      SatelliteWindowController() => controller.title,
     };
   }
 
@@ -1476,6 +1812,7 @@
       DialogWindowController() => controller.title,
       TooltipWindowController() => '',
       PopupWindowController() => '',
+      SatelliteWindowController() => controller.title,
     };
   }
 
@@ -1500,6 +1837,7 @@
       DialogWindowController() => controller.isActivated,
       TooltipWindowController() => false,
       PopupWindowController() => controller.isActivated,
+      SatelliteWindowController() => controller.isActivated,
     };
   }
 
@@ -1524,6 +1862,7 @@
       DialogWindowController() => controller.isActivated,
       TooltipWindowController() => false,
       PopupWindowController() => controller.isActivated,
+      SatelliteWindowController() => controller.isActivated,
     };
   }
 
@@ -1548,6 +1887,7 @@
       DialogWindowController() => controller.isMinimized,
       TooltipWindowController() => false,
       PopupWindowController() => false,
+      SatelliteWindowController() => false,
     };
   }
 
@@ -1572,6 +1912,7 @@
       DialogWindowController() => controller.isMinimized,
       TooltipWindowController() => false,
       PopupWindowController() => false,
+      SatelliteWindowController() => false,
     };
   }
 
@@ -1596,6 +1937,7 @@
       DialogWindowController() => false,
       TooltipWindowController() => false,
       PopupWindowController() => false,
+      SatelliteWindowController() => false,
     };
   }
 
@@ -1620,6 +1962,7 @@
       DialogWindowController() => false,
       TooltipWindowController() => false,
       PopupWindowController() => false,
+      SatelliteWindowController() => false,
     };
   }
 
@@ -1645,6 +1988,7 @@
       DialogWindowController() => false,
       TooltipWindowController() => false,
       PopupWindowController() => false,
+      SatelliteWindowController() => false,
     };
   }
 
@@ -1669,6 +2013,7 @@
       DialogWindowController() => false,
       TooltipWindowController() => false,
       PopupWindowController() => false,
+      SatelliteWindowController() => false,
     };
   }
 
@@ -1734,6 +2079,8 @@
                 dialog.title != (oldWidget.controller as DialogWindowController).title,
               TooltipWindowController() => false,
               PopupWindowController() => false,
+              final SatelliteWindowController satellite =>
+                satellite.title != (oldWidget.controller as SatelliteWindowController).title,
             },
             _WindowControllerAspect.activated => switch (controller) {
               final RegularWindowController regular =>
@@ -1744,6 +2091,9 @@
               TooltipWindowController() => false,
               final PopupWindowController popup =>
                 popup.isActivated != (oldWidget.controller as PopupWindowController).isActivated,
+              final SatelliteWindowController satellite =>
+                satellite.isActivated !=
+                    (oldWidget.controller as SatelliteWindowController).isActivated,
             },
             _WindowControllerAspect.maximized => switch (controller) {
               final RegularWindowController regular =>
@@ -1752,6 +2102,7 @@
               DialogWindowController() => false,
               TooltipWindowController() => false,
               PopupWindowController() => false,
+              SatelliteWindowController() => false,
             },
             _WindowControllerAspect.minimized => switch (controller) {
               final RegularWindowController regular =>
@@ -1761,6 +2112,7 @@
                 dialog.isMinimized != (oldWidget.controller as DialogWindowController).isMinimized,
               TooltipWindowController() => false,
               PopupWindowController() => false,
+              SatelliteWindowController() => false,
             },
             _WindowControllerAspect.fullscreen => switch (controller) {
               final RegularWindowController regular =>
@@ -1769,6 +2121,7 @@
               DialogWindowController() => false,
               TooltipWindowController() => false,
               PopupWindowController() => false,
+              SatelliteWindowController() => false,
             },
           },
     );
@@ -1988,6 +2341,10 @@
                 controller: popup,
                 child: entry.builder(context),
               ),
+              final SatelliteWindowController satellite => SatelliteWindow(
+                controller: satellite,
+                child: entry.builder(context),
+              ),
             };
           }).toList();
 
diff --git a/packages/flutter/lib/src/widgets/_window_linux.dart b/packages/flutter/lib/src/widgets/_window_linux.dart
index ca9fccd..9f8a99b 100644
--- a/packages/flutter/lib/src/widgets/_window_linux.dart
+++ b/packages/flutter/lib/src/widgets/_window_linux.dart
@@ -148,6 +148,20 @@
   }) {
     throw UnimplementedError('Popup windows are not yet implemented on Linux.');
   }
+
+  @internal
+  @override
+  SatelliteWindowController createSatelliteWindowController({
+    required SatelliteWindowControllerDelegate delegate,
+    required BaseWindowController parent,
+    required WindowPositioner initialPositioner,
+    Rect? initialAnchorRect,
+    Size? preferredSize,
+    BoxConstraints? preferredConstraints,
+    String? title,
+  }) {
+    throw UnimplementedError('Satellite windows are not yet implemented on Linux.');
+  }
 }
 
 /// Implementation of [RegularWindowController] for the Linux platform.
diff --git a/packages/flutter/lib/src/widgets/_window_macos.dart b/packages/flutter/lib/src/widgets/_window_macos.dart
index a4c71f2..ee5d947 100644
--- a/packages/flutter/lib/src/widgets/_window_macos.dart
+++ b/packages/flutter/lib/src/widgets/_window_macos.dart
@@ -156,6 +156,20 @@
       view.viewId,
     );
   }
+
+  @internal
+  @override
+  SatelliteWindowController createSatelliteWindowController({
+    required SatelliteWindowControllerDelegate delegate,
+    required BaseWindowController parent,
+    required WindowPositioner initialPositioner,
+    Rect? initialAnchorRect,
+    Size? preferredSize,
+    BoxConstraints? preferredConstraints,
+    String? title,
+  }) {
+    throw UnimplementedError('Satellite windows are not yet implemented on macOS.');
+  }
 }
 
 mixin _WindowControllerMixin {
diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart
index 1b2684c..aff7e52 100644
--- a/packages/flutter/lib/src/widgets/_window_win32.dart
+++ b/packages/flutter/lib/src/widgets/_window_win32.dart
@@ -206,6 +206,20 @@
     throw UnimplementedError('Popup windows are not yet implemented on Windows.');
   }
 
+  @internal
+  @override
+  SatelliteWindowController createSatelliteWindowController({
+    required SatelliteWindowControllerDelegate delegate,
+    required BaseWindowController parent,
+    required WindowPositioner initialPositioner,
+    Rect? initialAnchorRect,
+    Size? preferredSize,
+    BoxConstraints? preferredConstraints,
+    String? title,
+  }) {
+    throw UnimplementedError('Satellite windows are not yet implemented on Windows.');
+  }
+
   /// Register a new [WindowsMessageHandler].
   ///
   /// The handler will be triggered for unhandled messages for all top level
diff --git a/packages/flutter/test/widgets/windowing_test.dart b/packages/flutter/test/widgets/windowing_test.dart
index 5e7d425..fe47210 100644
--- a/packages/flutter/test/widgets/windowing_test.dart
+++ b/packages/flutter/test/widgets/windowing_test.dart
@@ -15,6 +15,8 @@
         RegularWindow,
         RegularWindowController,
         RegularWindowControllerDelegate,
+        SatelliteWindow,
+        SatelliteWindowController,
         TooltipWindow,
         TooltipWindowController,
         WindowScope,
@@ -162,6 +164,44 @@
   void destroy() {}
 }
 
+class _StubSatelliteWindowController extends SatelliteWindowController {
+  _StubSatelliteWindowController({required this.tester}) : super.empty() {
+    rootView = FakeView(tester.view);
+  }
+
+  final WidgetTester tester;
+
+  @override
+  BaseWindowController get parent => _StubRegularWindowController(tester);
+
+  @override
+  Size get contentSize => Size.zero;
+
+  @override
+  String get title => 'Stub Satellite Window';
+
+  @override
+  bool get isActivated => true;
+
+  @override
+  void setParent(BaseWindowController parent) {}
+
+  @override
+  void setSize(Size size) {}
+
+  @override
+  void setConstraints(BoxConstraints constraints) {}
+
+  @override
+  void setTitle(String title) {}
+
+  @override
+  void activate() {}
+
+  @override
+  void destroy() {}
+}
+
 void main() {
   group('Windowing', () {
     group('isWindowingEnabled is false', () {
@@ -220,6 +260,16 @@
         );
       });
 
+      testWidgets('SatelliteWindow throws UnsupportedError', (WidgetTester tester) async {
+        expect(
+          () => SatelliteWindow(
+            controller: _StubSatelliteWindowController(tester: tester),
+            child: const Text('Test'),
+          ),
+          throwsUnsupportedError,
+        );
+      });
+
       testWidgets('Accessing WindowScope.of throws UnsupportedError', (WidgetTester tester) async {
         await tester.pumpWidget(LookupBoundary(child: Container()));
         final BuildContext context = tester.element(find.byType(Container));
@@ -331,6 +381,26 @@
         expect(scope, isA<PopupWindowController>());
       });
 
+      testWidgets('Can access WindowScope.of for satellite windows', (WidgetTester tester) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        BaseWindowController? scope;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                scope = WindowScope.of(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(scope, isA<SatelliteWindowController>());
+      });
+
       testWidgets('Can access WindowScope.maybeOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -415,6 +485,28 @@
         expect(scope, isA<PopupWindowController>());
       });
 
+      testWidgets('Can access WindowScope.maybeOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        BaseWindowController? scope;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                scope = WindowScope.maybeOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(scope, isA<SatelliteWindowController>());
+      });
+
       testWidgets('Can access WindowScope.contentSizeOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -503,6 +595,28 @@
         expect(size, equals(Size.zero));
       });
 
+      testWidgets('Can access WindowScope.contentSizeOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        Size? size;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                size = WindowScope.contentSizeOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(size, equals(Size.zero));
+      });
+
       testWidgets('Can access WindowScope.maybeContentSizeOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -591,6 +705,28 @@
         expect(size, equals(Size.zero));
       });
 
+      testWidgets('Can access WindowScope.maybeContentSizeOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        Size? size;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                size = WindowScope.maybeContentSizeOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(size, equals(Size.zero));
+      });
+
       testWidgets('Can access WindowScope.titleOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -675,6 +811,28 @@
         expect(title, equals(''));
       });
 
+      testWidgets('Can access WindowScope.titleOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        String? title;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                title = WindowScope.titleOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(title, equals('Stub Satellite Window'));
+      });
+
       testWidgets('Can access WindowScope.maybeTitleOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -763,6 +921,28 @@
         expect(title, equals(''));
       });
 
+      testWidgets('Can access WindowScope.maybeTitleOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        String? title;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                title = WindowScope.maybeTitleOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(title, equals('Stub Satellite Window'));
+      });
+
       testWidgets('Can access WindowScope.isActivatedOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -851,6 +1031,28 @@
         expect(isActivated, equals(true));
       });
 
+      testWidgets('Can access WindowScope.isActivatedOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        bool? isActivated;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                isActivated = WindowScope.isActivatedOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(isActivated, equals(true));
+      });
+
       testWidgets('Can access WindowScope.maybeIsActivatedOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -939,6 +1141,28 @@
         expect(isActivated, equals(true));
       });
 
+      testWidgets('Can access WindowScope.maybeIsActivatedOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        bool? isActivated;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                isActivated = WindowScope.maybeIsActivatedOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(isActivated, equals(true));
+      });
+
       testWidgets('Can access WindowScope.isMinimizedOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -1203,6 +1427,28 @@
         expect(isMaximized, equals(false));
       });
 
+      testWidgets('Can access WindowScope.isMaximizedOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        bool? isMaximized;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                isMaximized = WindowScope.isMaximizedOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(isMaximized, equals(false));
+      });
+
       testWidgets('Can access WindowScope.maybeIsMaximizedOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -1291,6 +1537,28 @@
         expect(isMaximized, equals(false));
       });
 
+      testWidgets('Can access WindowScope.maybeIsMaximizedOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        bool? isMaximized;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                isMaximized = WindowScope.maybeIsMaximizedOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(isMaximized, equals(false));
+      });
+
       testWidgets('Can access WindowScope.isFullscreenOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -1379,6 +1647,28 @@
         expect(isFullscreen, equals(false));
       });
 
+      testWidgets('Can access WindowScope.isFullscreenOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        bool? isFullscreen;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                isFullscreen = WindowScope.isFullscreenOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(isFullscreen, equals(false));
+      });
+
       testWidgets('Can access WindowScope.maybeIsFullscreenOf for regular windows', (
         WidgetTester tester,
       ) async {
@@ -1466,6 +1756,37 @@
 
         expect(isFullscreen, equals(false));
       });
+
+      testWidgets('Can access WindowScope.maybeIsFullscreenOf for satellite windows', (
+        WidgetTester tester,
+      ) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        bool? isFullscreen;
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(
+            controller: controller,
+            child: Builder(
+              builder: (BuildContext context) {
+                isFullscreen = WindowScope.maybeIsFullscreenOf(context);
+                return const SizedBox.shrink();
+              },
+            ),
+          ),
+        );
+
+        expect(isFullscreen, equals(false));
+      });
+
+      testWidgets('SatelliteWindow does not throw', (WidgetTester tester) async {
+        final controller = _StubSatelliteWindowController(tester: tester);
+        addTearDown(controller.dispose);
+        await tester.pumpWidget(
+          wrapWithView: false,
+          SatelliteWindow(controller: controller, child: Container()),
+        );
+      });
     });
   });
 }
diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart
index 8ec5372..0ffc907 100644
--- a/packages/flutter_test/lib/src/binding.dart
+++ b/packages/flutter_test/lib/src/binding.dart
@@ -291,6 +291,13 @@
           }
           activateable = (popupChild as _TestPopupWindowController).getFirstActivatableChild();
           foundPopup = true;
+        case final SatelliteWindowController satelliteChild:
+          if (foundPopup) {
+            // Already found a popup, skip anything else.
+            break;
+          }
+          activateable = (satelliteChild as _TestSatelliteWindowController)
+              .getFirstActivatableChild();
       }
     }
 
@@ -427,6 +434,8 @@
         (testParent as _TestRegularWindowController).addChild(child);
       case final PopupWindowController testParent:
         (testParent as _TestPopupWindowController).addChild(child);
+      case final SatelliteWindowController testParent:
+        (testParent as _TestSatelliteWindowController).addChild(child);
       case TooltipWindowController _:
         fail('TooltipWindowController cannot be a parent of another window controller.');
     }
@@ -442,6 +451,8 @@
         (testParent as _TestRegularWindowController).removeChild(child);
       case final PopupWindowController testParent:
         (testParent as _TestPopupWindowController).removeChild(child);
+      case final SatelliteWindowController testParent:
+        (testParent as _TestSatelliteWindowController).removeChild(child);
       case TooltipWindowController _:
         fail('TooltipWindowController cannot be a parent of another window controller.');
     }
@@ -687,6 +698,112 @@
   }
 }
 
+class _TestSatelliteWindowController extends SatelliteWindowController
+    with _ChildWindowHierarchyMixin {
+  _TestSatelliteWindowController({
+    required SatelliteWindowControllerDelegate delegate,
+    required TestPlatformDispatcher platformDispatcher,
+    required this.windowingOwner,
+    required BaseWindowController parent,
+    ui.Rect? anchorRect,
+    required WindowPositioner positioner,
+    Size? preferredSize,
+    BoxConstraints? preferredConstraints,
+    String? title,
+  }) : _delegate = delegate,
+       _parent = parent,
+       // ignore: unused_field
+       _anchorRect = anchorRect,
+       // ignore: unused_field
+       _positioner = positioner,
+       _size = preferredSize ?? const Size(800, 600),
+       _constraints = preferredConstraints ?? BoxConstraints.loose(const Size(1920, 1080)),
+       _title = title ?? 'Test Window',
+       super.empty() {
+    _constrainToBounds();
+    rootView = _TestFlutterView(
+      controller: this,
+      platformDispatcher: platformDispatcher,
+      constraints: _constraints,
+    );
+    _addChildToParent(parent, this);
+
+    // Automatically activate the window when created.
+    activate();
+  }
+
+  final SatelliteWindowControllerDelegate _delegate;
+  BaseWindowController _parent;
+  final _TestWindowingOwner windowingOwner;
+  // ignore: unused_field
+  final ui.Rect? _anchorRect;
+  // ignore: unused_field
+  final WindowPositioner _positioner;
+  Size _size;
+  BoxConstraints _constraints;
+  String _title;
+
+  @override
+  Size get contentSize => _size;
+
+  @override
+  BaseWindowController get parent => _parent;
+
+  @override
+  String get title => _title;
+
+  @override
+  bool get isActivated => windowingOwner.isWindowControllerActive(this);
+
+  @override
+  void setParent(BaseWindowController parent) {
+    _removeChildFromParent(_parent, this);
+    _parent = parent;
+    _addChildToParent(parent, this);
+    notifyListeners();
+  }
+
+  @override
+  void setSize(Size size) {
+    _size = size;
+    _constrainToBounds();
+    notifyListeners();
+  }
+
+  @override
+  void setConstraints(BoxConstraints constraints) {
+    _constraints = constraints;
+    _constrainToBounds();
+    notifyListeners();
+  }
+
+  @override
+  void setTitle(String title) {
+    _title = title;
+    notifyListeners();
+  }
+
+  @override
+  void activate() {
+    final BaseWindowController activated = windowingOwner.activateWindowController(this);
+    activated.notifyListeners();
+  }
+
+  void _constrainToBounds() {
+    final double width = _constraints.constrainWidth(_size.width);
+    final double height = _constraints.constrainHeight(_size.height);
+    _size = Size(width, height);
+  }
+
+  @override
+  void destroy() {
+    _delegate.onWindowDestroyed();
+    removeAllChildren();
+    windowingOwner.deactivateWindowController(this);
+    _removeChildFromParent(_parent, this);
+  }
+}
+
 /// A [WindowingOwner] used for tests.
 ///
 /// This windowing owner will behave as a perfect windowing system, with no
@@ -731,6 +848,11 @@
             .getFirstActivatableChild();
         _activeWindowController = leaf;
         return _activeWindowController!;
+      case final SatelliteWindowController _:
+        final BaseWindowController leaf = (controller as _TestSatelliteWindowController)
+            .getFirstActivatableChild();
+        _activeWindowController = leaf;
+        return _activeWindowController!;
     }
   }
 
@@ -748,6 +870,8 @@
         fail('TooltipWindowController cannot be a parent of DialogWindowController.');
       case final PopupWindowController popupParent:
         popupParent.activate();
+      case final SatelliteWindowController satelliteParent:
+        satelliteParent.activate();
     }
 
     return true;
@@ -783,6 +907,10 @@
         if (!_tryActivateParent(popupController.parent)) {
           _activeWindowController = null;
         }
+      case final SatelliteWindowController satelliteController:
+        if (!_tryActivateParent(satelliteController.parent)) {
+          _activeWindowController = null;
+        }
     }
   }
 
@@ -868,6 +996,30 @@
       parent: parent,
     );
   }
+
+  @internal
+  @override
+  SatelliteWindowController createSatelliteWindowController({
+    required SatelliteWindowControllerDelegate delegate,
+    required BaseWindowController parent,
+    required WindowPositioner initialPositioner,
+    Rect? initialAnchorRect,
+    Size? preferredSize,
+    BoxConstraints? preferredConstraints,
+    String? title,
+  }) {
+    return _TestSatelliteWindowController(
+      delegate: delegate,
+      platformDispatcher: _platformDispatcher,
+      windowingOwner: this,
+      parent: parent,
+      anchorRect: initialAnchorRect,
+      positioner: initialPositioner,
+      preferredSize: preferredSize,
+      preferredConstraints: preferredConstraints,
+      title: title,
+    );
+  }
 }
 
 // Examples can assume: