Window
드래그 · 리사이즈 · 스냅을 지원하는 다중 창 매니저
목차
Window와 WindowNavigator가 함께 데스크톱형 창 매니저를 만듭니다.
타이틀 바로 드래그하고, 모든 변·모서리에서 크기를 바꾸며, 화면 가장자리에
스냅합니다. 최소화 / 최대화 / 항상 위 레이어도 지원합니다. Flutter와 Web의
API와 동작은 같습니다.
기본 사용법 (Basic Usage)#
WindowNavigator(
initialWindows: [
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 240, 180),
title: const Text('Window 1'),
content: const Text('Content'),
),
],
child: const Center(child: Text('Desktop')),
)
빠른 오버라이드 (Chain)#
이미 만든 Window 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class WindowChainExample extends StatefulWidget {
const WindowChainExample({super.key});
@override
State<WindowChainExample> createState() => _WindowChainExampleState();
}
class _WindowChainExampleState extends State<WindowChainExample> {
final GlobalKey<WindowNavigatorHandle> _navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
OutlinedContainer(
containerStyle: const CoreOutlinedContainerStyle(height: 600),
child: WindowNavigator(
key: _navigatorKey,
initialWindows: [
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 200, 200),
title: const Text('Window 1'),
content: const Center(child: Text('Window 1')),
).withStyle(
const CoreWindowStyle(
titleBarHeight: CoreSpace.space48,
unfocusedTitleStyle: CoreTextStyle(
color: CoreColor.token(CoreColors.outline),
),
panelStyle: CoreWindowPanelStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
borderColor: CoreColor.token(CoreColors.primary),
containerBorderWidth: CoreStrokeWidth.stroke2,
),
),
),
Window(
bounds: const CoreWindowRect.fromLTWH(200, 0, 200, 200),
title: const Text('Window 2'),
content: const Center(child: Text('Window 2')),
),
],
child: const Center(child: Text('Desktop')),
),
),
const Gap.space8(),
Button(
onPressed: () {
final nav = _navigatorKey.currentState;
if (nav == null) return;
final index = nav.windows.length + 1;
nav.pushWindow(
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 200, 200),
title: Text('Window $index'),
content: Center(child: Text('Window $index')),
),
);
},
child: const Text('Add Window'),
),
],
);
}
}
class WindowChainExample extends StatefulComponent {
const WindowChainExample({super.key});
@override
State<WindowChainExample> createState() => _WindowChainExampleState();
}
class _WindowChainExampleState extends State<WindowChainExample> {
final GlobalKey _navigatorKey = GlobalKey();
@override
Component build(BuildContext context) {
return div(
[
OutlinedContainer(
containerStyle: const CoreOutlinedContainerStyle(height: 600),
child: WindowNavigator(
key: _navigatorKey,
initialWindows: [
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 200, 200),
title: const Text('Window 1'),
content: div(
[const Text('Window 1')],
classes: 'flex items-center justify-center w-full h-full',
),
).withStyle(
const CoreWindowStyle(
titleBarHeight: CoreSpace.space48,
unfocusedTitleStyle: CoreTextStyle(
color: CoreColor.token(CoreColors.outline),
),
panelStyle: CoreWindowPanelStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
borderColor: CoreColor.token(CoreColors.primary),
containerBorderWidth: CoreStrokeWidth.stroke2,
),
),
),
Window(
bounds: const CoreWindowRect.fromLTWH(200, 0, 200, 200),
title: const Text('Window 2'),
content: div(
[const Text('Window 2')],
classes: 'flex items-center justify-center w-full h-full',
),
),
],
child: div(
[const Text('Desktop')],
classes: 'flex items-center justify-center w-full h-full',
),
),
),
Gap.space8(),
Button(
onPressed: () {
final nav = _navigatorKey.currentWindowNavigator;
if (nav == null) return;
final index = nav.windows.length + 1;
nav.pushWindow(
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 200, 200),
title: Text('Window $index'),
content: div(
[Text('Window $index')],
classes: 'flex items-center justify-center w-full h-full',
),
),
);
},
child: const Text('Add Window'),
),
],
classes: 'flex flex-col w-full',
);
}
}
Live Preview#
코드 보기
class WindowDefaultExample extends StatefulComponent {
const WindowDefaultExample({super.key});
@override
State<WindowDefaultExample> createState() => _WindowDefaultExampleState();
}
class _WindowDefaultExampleState extends State<WindowDefaultExample> {
final GlobalKey _navigatorKey = GlobalKey();
@override
Component build(BuildContext context) {
return div(
[
OutlinedContainer(
containerStyle: const CoreOutlinedContainerStyle(height: 600),
child: WindowNavigator(
key: _navigatorKey,
initialWindows: [
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 200, 200),
title: const Text('Window 1'),
content: div(
[const Text('Window 1')],
classes: 'flex items-center justify-center w-full h-full',
),
),
Window(
bounds: const CoreWindowRect.fromLTWH(200, 0, 200, 200),
title: const Text('Window 2'),
content: div(
[const Text('Window 2')],
classes: 'flex items-center justify-center w-full h-full',
),
),
],
child: div(
[const Text('Desktop')],
classes: 'flex items-center justify-center w-full h-full',
),
),
),
Gap.space8(),
Button(
onPressed: () {
final nav = _navigatorKey.currentWindowNavigator;
if (nav == null) return;
final index = nav.windows.length + 1;
nav.pushWindow(
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 200, 200),
title: Text('Window $index'),
content: div(
[Text('Window $index')],
classes: 'flex items-center justify-center w-full h-full',
),
),
);
},
child: const Text('Add Window'),
),
],
classes: 'flex flex-col w-full',
);
}
}
class WindowDefaultExample extends StatefulWidget {
const WindowDefaultExample({super.key});
@override
State<WindowDefaultExample> createState() => _WindowDefaultExampleState();
}
class _WindowDefaultExampleState extends State<WindowDefaultExample> {
final GlobalKey<WindowNavigatorHandle> _navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
OutlinedContainer(
containerStyle: const CoreOutlinedContainerStyle(height: 600),
child: WindowNavigator(
key: _navigatorKey,
initialWindows: [
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 200, 200),
title: const Text('Window 1'),
content: const Center(child: Text('Window 1')),
),
Window(
bounds: const CoreWindowRect.fromLTWH(200, 0, 200, 200),
title: const Text('Window 2'),
content: const Center(child: Text('Window 2')),
),
],
child: const Center(child: Text('Desktop')),
),
),
const Gap.space8(),
Button(
onPressed: () {
final nav = _navigatorKey.currentState;
if (nav == null) return;
final index = nav.windows.length + 1;
nav.pushWindow(
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 200, 200),
title: Text('Window $index'),
content: Center(child: Text('Window $index')),
),
);
},
child: const Text('Add Window'),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- IDE·대시보드·도구 팔레트처럼 여러 창을 동시에 띄우고 옮길 때
- 창마다 드래그 · 리사이즈 · 가장자리 스냅이 필요할 때
- 최소화 / 최대화 / 항상 위 레이어가 필요할 때
대신 다른 컴포넌트를 사용하세요:
Dialog: 한 번의 확인이 필요한 모달Drawer: 화면 가장자리에서 밀어 여는 패널WindowPanel: 창 매니저 없이 타이틀 바 chrome만 필요할 때
Import#
import 'package:coui_flutter/coui_flutter.dart';
import 'package:coui_web/coui_web.dart';
import 'package:jaspr/jaspr.dart';
Props / Parameters#
여기에는 위젯 생성자 파라미터만 적습니다. 프레임 chrome 은 windowStyle.panelStyle
중첩 슬롯으로 흐릅니다.
W는 Flutter의 Widget, Web의 Component를 뜻하는 합성 콘텐츠 타입입니다. bounds와 리사이즈 치수는 논리 px이며
maximized 사각형은 호스트 크기에 대한 0–1 비율입니다.
Window#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
title | W? | null | 타이틀 바 안의 제목 슬롯 |
actions |
W? |
null |
타이틀 바의 추가 액션 슬롯 |
content | W? | null | 본문 영역 |
controller |
CoreWindowController? |
null |
외부 반응형 상태 컨트롤러 |
bounds |
CoreWindowRect? |
필수 (Window.controlled는 없음 — 컨트롤러가 소유) |
초기 플로팅 영역 |
maximized |
CoreWindowRect? |
null |
상대 좌표(0..1) 최대화 영역 |
minimized |
bool? |
false |
처음에 최소화된 상태로 시작할지 |
alwaysOnTop |
bool? |
false |
항상 위 레이어에 고정 |
enableSnapping |
bool? |
true |
가장자리 드래그 스냅 |
resizable |
bool? |
true |
8방향 변/모서리 리사이즈 핸들 |
resizeThickness |
double? |
CoreWindowContract.defaultResizeThickness |
리사이즈 hit 영역의 두께 (논리 px) |
draggable |
bool? |
true |
타이틀 바 드래그 |
closable | bool? | true | 닫기 컨트롤 |
maximizable |
bool? |
true |
최대화 컨트롤 |
minimizable |
bool? |
true |
최소화 컨트롤 |
constraints |
CoreWindowConstraints? |
kCoreDefaultWindowConstraints (최소 200×200) |
리사이즈 중 적용되는 최소/최대 크기 |
windowStyle |
CoreWindowStyle? |
null |
chrome 단일 진입점 — 타이틀 바 높이, 리사이즈 두께, 스냅 바/미리보기 chrome, 창 전환, 중첩
panelStyle
(
CoreWindowPanelStyle
)
|
WindowNavigator#
창 스택을 소유하는 호스트입니다. 생성자 파라미터는 위 Window와 별개입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
initialWindows |
List<Window> |
필수 | 마운트 시 그릴 창 목록 |
child | W? | null | 배경(데스크톱) 콘텐츠 |
showTopSnapBar |
bool |
true |
프리셋 타일 스냅 바를 그릴지 |
프로그래밍 제어#
final controller = CoreWindowController(
bounds: CoreWindowRect.fromLTWH(0, 0, 320, 240),
);
controller.bounds = CoreWindowRect.fromLTWH(100, 100, 320, 240);
controller.maximized = const CoreWindowRect.fromLTWH(0, 0, 1, 1);
controller.minimized = true;
스타일 시스템 (Style System)#
windowStyle 슬롯#
모든 chrome / 치수 오버라이드는 단일 windowStyle 슬롯(프로젝트 전역은
CoreWindowTheme.style)으로 흐릅니다. 패널 프레임 자체는 합성된
WindowPanel이라, 그 chrome은 중첩 panelStyle로 갑니다.
CoreWindowStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
titleBarHeight |
double? |
Title bar height in logical pixels. |
snapPreviewBorderRadius |
CoreBorderRadius? |
Border radius for the floating snap-preview overlay (semi-transparent surface shown while dragging a window into a snap zone). Overrides [defaultSnapPreviewBorderRadius] when set. |
snapBarBorderRadius |
CoreBorderRadius? |
Border radius for the snap-bar banner container (the rounded strip that drops down from the navigator top while a window is being dragged). Overrides [defaultSnapBarBorderRadius] when set. |
snapBarTileSpacing |
double? |
Spacing between adjacent tiles inside the snap-bar grid in logical pixels. Flutter applies it as native
Row.spacing
; Web emits the matching CSS
gap
value on the flex container. Overrides [defaultSnapBarTileSpacing] when set.
|
snapBarPadding |
CoreEdgeInsets? |
Inner padding of the snap-bar banner / snap-preview container. Overrides [defaultSnapBarPadding] when set. |
snapPreviewBorderColor |
CoreColor? |
Outline colour of the snap-preview overlay border. Overrides [defaultSnapPreviewBorderColor] when set. |
snapPreviewBackgroundColor |
CoreColor? |
Semi-transparent surface fill of the snap-preview overlay. Overrides [defaultSnapPreviewBackgroundColor] when set. |
snapBarBorderColor |
CoreColor? |
Outline colour of the snap-bar banner container border. Overrides [defaultSnapBarBorderColor] when set. |
snapBarBackgroundColor |
CoreColor? |
Surface fill of the snap-bar banner container. Overrides [defaultSnapBarBackgroundColor] when set. |
snapBarBorderWidth |
double? |
Border width of the snap-bar banner container. Overrides [defaultSnapBarBorderWidth] when set. |
snapBarHeight |
double? |
Height of the snap-bar banner (logical/CSS px). Overrides [defaultSnapBarHeight] when set. |
snapBarTopOffset |
double? |
Top offset of the expanded snap-bar banner (logical/CSS px). Overrides [defaultSnapBarTopOffset] when set. |
snapBarHiddenOffset |
double? |
Resting "peek" pose of the hidden snap-bar as a fraction of its height (−1 = fully hidden, 0 = fully shown); also the start of the content fade-in. Overrides [defaultSnapBarHiddenOffset] when set. |
snapBarAnimation |
Duration? |
Snap-bar drop-down show/hide animation duration. Overrides [defaultSnapBarAnimation] when set. |
snapTileSpacing |
double? |
Inner gap between abutting snap-bar tiles (logical px). Overrides [defaultSnapTileSpacing] when set. |
windowTransition |
Duration? |
Window open / close / maximize transition duration. Overrides [defaultWindowTransition] when set. |
minimizedScale |
double? |
Scale of the window while minified during a drag-snap. Overrides [defaultMinimizedScale] when set. |
closeAnimationScaleMin |
double? |
Lower bound of the close-animation scale tween. Overrides [defaultCloseAnimationScaleMin] when set. |
closeAnimationScaleMax |
double? |
Upper bound of the close-animation scale tween. Overrides [defaultCloseAnimationScaleMax] when set. |
snapPreviewBlurSigma |
double? |
Gaussian σ of the snap-preview ghost's backdrop blur. Overrides [defaultSnapPreviewBlurSigma] when set. |
snapTileBorderColor |
CoreColor? |
Outline colour of each snap-bar tile border. Overrides [defaultSnapTileBorderColor] when set. |
snapTileBorderWidth |
double? |
Border width of each snap-bar tile in logical/CSS px. Overrides [defaultSnapTileBorderWidth] when set. |
snapTileBackgroundColor |
CoreColor? |
Fill of a snap-bar tile at rest. Overrides [defaultSnapTileBackgroundColor] when set. |
snapTileHoverBackgroundColor |
CoreColor? |
Fill of a snap-bar tile while hovered. Overrides [defaultSnapTileHoverBackgroundColor] when set. |
unfocusedTitleStyle |
CoreTextStyle? |
Title-text overlay merged onto the panel's
titleStyle
for every window EXCEPT the frontmost one — both platforms dim unfocused titles with it (reference behaviour: focused = onSurface, rest = muted). Overrides [defaultUnfocusedTitleStyle] when set.
|
panelStyle |
CoreWindowPanelStyle? |
Chrome of the composed
WindowPanel
— nested slot raw-forwarded into the panel (child-component-composition). Merged over [defaultPanelStyle]; a FULL maximize forces the corner radius to zero on top of the merged result.
|
테마 커스터마이징 (Theme Customization)#
기본값은 CoreWindowTheme.style로 덮어씁니다.
const CoreComponentTheme(
window: CoreWindowTheme(
style: CoreWindowStyle(
titleBarHeight: CoreSpace.space36,
),
),
);
리사이즈 hit 영역의 두께는 chrome 이 아니라 behaviour 입니다 (8 개 영역은 아무것도 그리지 않습니다) — Theme 이 아니라 위젯 파라미터로 조정합니다:
Window(
bounds: bounds,
resizeThickness: CoreSpace.space10,
);
동작 스펙 (Behavior)#
-
Window는 창 설정과 컨트롤러 연결을 나타내며 실제 창 스택은
WindowNavigator에 마운트합니다.initialWindows와pushWindow로 창을 구성합니다. -
draggable은 타이틀 바 이동,resizable은 가장자리·모서리 크기 조절을 허용합니다. 리사이즈는constraints의 최소/최대 크기에 제한됩니다. -
최소화는 콘텐츠를 숨기고, 최대화·스냅은 호스트에 대한 상대 사각형을 사용합니다.
enableSnapping과showTopSnapBar가 스냅 동작과 프리셋 표시를 정합니다. -
Window.controlled는 전달한CoreWindowController가 상태를 소유합니다. 콜백으로 단순 통보하는 프레임과 달리 컨트롤러의 bounds/maximized/minimized 변경이 실제 창에 반영됩니다.
사용 가이드라인 (Usage Guidelines)#
창 이동·리사이즈·스냅·스택 관리가 필요하면 Window/WindowNavigator를 사용합니다. 타이틀 바와 콘텐츠 프레임만 그리거나 외부에서 상태를 관리하려면 WindowPanel을 선택합니다. 문서 흐름의 패널은 Card, 단일 모달 확인은 Dialog가 더 직접적입니다.
✅ Do#
프레임 chrome 은 중첩 슬롯 panelStyle 로 조정
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 320, 240),
title: const Text('Editor'),
windowStyle: CoreWindowStyle(
panelStyle: CoreWindowPanelStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
titleBarColor: CoreColor.token(CoreColors.surfaceContainerHigh),
),
),
)
Window 의 프레임 자체는 합성된 WindowPanel 이라, 모서리 반경 · 타이틀 바 배경 같은 프레임 chrome 은 windowStyle 최상위가 아니라 중첩 슬롯 windowStyle.panelStyle 로 흐릅니다.
❌ Don't#
CoreWindowStyle 최상위에 프레임 반경 필드가 있다고 가정하지 않기
// ❌ CoreWindowStyle 에는 프레임용 borderRadius 필드가 없다
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 320, 240),
windowStyle: CoreWindowStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
),
)
snapPreviewBorderRadius / snapBarBorderRadius 는 각각 스냅 미리보기 고스트와 스냅 바 배너 전용입니다 — 실제 창 프레임 반경은 panelStyle 안에서만 조정됩니다.
✅ Do#
프로그래밍 방식 제어가 필요하면 Window.controlled 로 컨트롤러를 직접 소유
final controller = CoreWindowController(
bounds: const CoreWindowRect.fromLTWH(0, 0, 320, 240),
);
Window.controlled(
controller: controller,
title: const Text('Editor'),
content: const Text('Content'),
)
// 이후 controller.bounds = ... 로 위치/크기를 외부에서 갱신
창을 외부 상태(저장된 레이아웃 복원 등)로 옮기거나 크기를 바꿔야 한다면 Window.controlled 로 CoreWindowController 를 직접 소유시키세요. 초기 위치는 bounds: 파라미터가 아니라 컨트롤러 생성 시 넘깁니다.
❌ Don't#
기본 생성자에 bounds 와 controller 를 동시에 넘기지 않기
// ❌ controller 가 주어지면 bounds 는 조용히 무시된다
Window(
bounds: const CoreWindowRect.fromLTWH(0, 0, 320, 240),
controller: controller,
content: const Text('Content'),
)
기본 Window(...) 생성자에서도 controller 가 주어지면 bounds/closable/constraints 등 나머지 필드는 전부 무시되고 컨트롤러의 값만 쓰입니다 — 두 값을 동시에 주면 bounds 가 조용히 버려집니다.
접근성 (Accessibility)#
역할#
Window 와 WindowNavigator 자체는 아무 시맨틱도 내보내지 않습니다 — Flutter 쪽에
Semantics 호출이 하나도 없고, Web 쪽이 붙이는 속성은 data-co-window 류 데이터 속성뿐입니다.
윈도우의 접근성 정보는 합성된 WindowPanel에서 옵니다. Web은 dialog 역할, Flutter는 시맨틱 컨테이너로 노출하고 둘 다 현재 로케일의 windowLabel을 이름으로 사용합니다. 타이틀 바 컨트롤도 로케일화된 이름을 갖습니다. 스냅 바·스냅 타일·리사이즈 핸들·데스크톱 배경은 별도 역할이나 이름을 제공하지 않습니다.
키보드#
| 키 | 동작 |
|---|---|
Enter | 포커스된 타이틀 바 컨트롤(최소화 / 최대화 / 닫기) 활성화 |
Space | 포커스된 타이틀 바 컨트롤 활성화 |
window.dart 에는 양 플랫폼 모두 키 핸들러가 없습니다. 위 두 키는 타이틀 바의 Button 이 제공하는 것이고, 윈도우를 올리고 · 옮기고 · 크기를 바꾸고 · 스냅하는 조작은 전부 포인터 전용입니다. 8방향 리사이즈 핸들도 드래그로만 동작하며
Escape 는 아무 동작도 하지 않습니다.
포커스 관리#
입력 포커스를 관리하지 않습니다. focusWindow / unfocusWindow 는 이름과 달리 z-순서를 올리고 타이틀 스타일을 바꾸는 것이 전부이며,
키보드 포커스는 옮기지 않습니다. 포커스 트랩도, 닫힐 때의 복원도, 배경 윈도우에 대한 inert / BlockSemantics
처리도 없습니다.
윈도우 안에서 포커스를 받는 것은 타이틀 바 버튼과 호출자가 넣은 content 안의 요소들뿐입니다.
스크린 리더#
Web에서는 로케일화된 Window/창 이름의 dialog, Flutter에서는 같은 이름의 시맨틱 컨테이너로 읽힙니다. 이름은 활성 로케일에서 오지만 실제 title 슬롯을 접근성 이름으로 사용하지는 않습니다.
윈도우의 실제 title은 접근성 이름으로 쓰이지 않습니다 — 두 플랫폼 모두 로케일화된 일반 명사 Window/창으로 읽힙니다.
알려진 제약#
- 이동·크기 조절·스냅의 키보드 경로가 없습니다. 최대화·최소화·닫기는 포커스된 컨트롤 버튼으로 활성화할 수 있지만 Escape 닫기는 없습니다. 위치·크기를 키보드로 다뤄야 하는 제품은 CoreWindowController를 조작하는 별도 컨트롤을 제공해야 합니다.
-
여러 윈도우가 동시에 접근성 트리와 Tab 순서에 남습니다.
role="dialog"를 쓰면서도 배경 윈도우를 비활성화하지 않으므로, 뒤에 있는 윈도우의 버튼과 콘텐츠에 그대로 Tab 이 닿습니다. - 모든 윈도우가 같은 일반 이름으로 발표됩니다. 여러 개를 띄우면 리더에서 서로 구분되지 않습니다.
-
WindowActions로 직접 그린 액션 행에는 접근성 이름이 아예 없습니다. 이 위젯은 아이콘만 든Button세 개를 라벨 없이 만들기 때문에,WindowPanel이 그리는 동일한 컨트롤과 달리 리더가 이름을 읽을 수 없습니다.actions를 직접 구성한다면 각 버튼에 라벨을 직접 붙이세요.
컴포넌트와 무관하게 적용되는 축(동작 줄이기 · 고대비 · 색 강제 모드 · 최소 터치 타겟)은 전역 접근성 축에 정리되어 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 창 마운트 | WindowNavigator의 위젯 레이어 그룹 | WindowNavigator 루트 DOM의 positioned 창 |
| 타이틀 바 이동 | WindowPanel의 pan 훅과 위젯 좌표 | WindowPanel의 pointer/document 훅과 DOM 좌표 |
| 창 순서 | navigator 자식의 페인트 순서 | navigator 내부 z-index 순서 |
| 크기·상태 | CoreWindowController를 Flutter 트리에 연결 | 같은 CoreWindowController를 Jaspr 상태에 연결 |
| 프레임 표시 | 합성 WindowPanel | 합성 WindowPanel |
운영체제의 별도 창을 생성하지 않고 각 앱의 한 호스트 영역 안에서 창 UI를 그립니다. 플랫폼별 입력 포커스 관리와 창의 시각적 z-순서는 구분해야 합니다.