Window#
Window와 WindowNavigator가 함께 데스크톱형 창 매니저를 만듭니다.
타이틀 바로 드래그하고, 모든 변·모서리에서 크기를 바꾸며, 화면 가장자리에
스냅합니다. 최소화 / 최대화 / 항상 위 레이어도 지원합니다. Flutter와 Web의
API와 동작은 같습니다.
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'),
),
],
);
}
}
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,
resizeThickness: CoreSpace.space8,
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',
);
}
}
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,
resizeThickness: CoreSpace.space8,
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'),
),
],
);
}
}
사용 시기 (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';
기본 사용법 (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,
resizeThickness: CoreSpace.space8,
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,
resizeThickness: CoreSpace.space8,
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',
);
}
}
Props / Parameters#
여기에는 위젯 생성자 파라미터만 적습니다. 프레임 chrome 은 windowStyle.panelStyle
중첩 슬롯으로 흐릅니다.
Window#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
title |
Widget? / Component? |
null |
타이틀 바 안의 제목 슬롯 |
actions |
Widget? / Component? |
기본 액션 | 최소화/최대화/닫기 행을 덮어씀 |
content |
Widget? / Component? |
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방향 변/모서리 리사이즈 핸들 |
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 |
Widget? / Component? |
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;
스타일 시스템 — windowStyle#
모든 chrome / 치수 오버라이드는 단일 windowStyle 슬롯(프로젝트 전역은
CoreWindowTheme.style)으로 흐릅니다. 패널 프레임 자체는 합성된
WindowPanel이라, 그 chrome은 중첩 panelStyle로 갑니다.
CoreWindowStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
titleBarHeight |
double? |
Title bar height in logical pixels. |
resizeThickness |
double? |
Thickness of each of the 8 resize hit regions 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.
|
테마#
기본값은 CoreWindowTheme.style로 덮어씁니다.
const CoreComponentTheme(
window: CoreWindowTheme(
style: CoreWindowStyle(
titleBarHeight: CoreSpace.space36,
),
),
);
리사이즈 hit 영역의 두께는 chrome 이 아니라 behaviour 입니다 (8 개 영역은 아무것도 그리지 않습니다) — Theme 이 아니라 위젯 파라미터로 조정합니다:
Window(
bounds: bounds,
resizeThickness: CoreSpace.space10,
);
사용 가이드라인 (Usage Guidelines)#
✅ 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)#
역할 / Semantics#
Window 와 WindowNavigator 자체는 아무 시맨틱도 내보내지 않습니다 — Flutter 쪽에
Semantics 호출이 하나도 없고, Web 쪽이 붙이는 속성은 data-co-window 류 데이터 속성뿐입니다.
윈도우의 접근성 정보는 전부 내부에 합성된 WindowPanel 에서 옵니다 — Web 은 role="dialog" + aria-label="window", Flutter 는
Semantics(container: true, label: 'window'), 그리고 타이틀 바의 컨트롤 버튼 세 개가 각각 라벨을 갖습니다. 스냅 바 · 스냅 타일 · 리사이즈 핸들 · 데스크탑 배경은 역할도 라벨도 없습니다.
키보드#
| 키 | 동작 |
|---|---|
Enter | 포커스된 타이틀 바 컨트롤(최소화 / 최대화 / 닫기) 활성화 |
Space | 포커스된 타이틀 바 컨트롤 활성화 |
window.dart 에는 양 플랫폼 모두 키 핸들러가 없습니다. 위 두 키는 타이틀 바의 Button 이 제공하는 것이고, 윈도우를 올리고 · 옮기고 · 크기를 바꾸고 · 스냅하는 조작은 전부 포인터 전용입니다. 8방향 리사이즈 핸들도 드래그로만 동작하며
Escape 는 아무 동작도 하지 않습니다.
포커스#
입력 포커스를 관리하지 않습니다. focusWindow / unfocusWindow 는 이름과 달리 z-순서를 올리고 타이틀 스타일을 바꾸는 것이 전부이며,
키보드 포커스는 옮기지 않습니다. 포커스 트랩도, 닫힐 때의 복원도, 배경 윈도우에 대한 inert / BlockSemantics
처리도 없습니다.
윈도우 안에서 포커스를 받는 것은 타이틀 바 버튼과 호출자가 넣은 content 안의 요소들뿐입니다.
스크린 리더#
Web 에서는 각 윈도우가 "window" 라는 이름의 dialog 로, 그 안에 최소화 / 최대화 / 닫기 세 버튼을 가진 것으로 발표됩니다. Flutter 에서는 "window" 라벨이 붙은 시맨틱 컨테이너로 읽히고, 이 문자열은 영어 리터럴이라 번역되지 않습니다(버튼 라벨은 로케일을 따르는 것과 다릅니다).
윈도우의 실제 title 은 접근성 이름으로 쓰이지 않습니다 — 두 플랫폼 모두 "window" 로만 읽힙니다.
알려진 제약#
-
윈도우를 키보드로 조작할 방법이 없습니다. 이동 · 크기 조절 · 스냅 · 최대화 · 최소화 · 닫기 어느 것도 키 경로가 없고
Escape도 동작하지 않습니다. 키보드로 창을 다룰 수 있어야 하는 제품이라면CoreWindowController를 직접 조작하는 별도 컨트롤을 호출자가 제공해야 합니다. -
여러 윈도우가 동시에 접근성 트리와 Tab 순서에 남습니다.
role="dialog"를 쓰면서도 배경 윈도우를 비활성화하지 않으므로, 뒤에 있는 윈도우의 버튼과 콘텐츠에 그대로 Tab 이 닿습니다. - 모든 윈도우가 같은 이름 "window" 로 발표됩니다. 여러 개를 띄우면 리더에서 서로 구분되지 않습니다.
-
WindowActions로 직접 그린 액션 행에는 접근성 이름이 아예 없습니다. 이 위젯은 아이콘만 든Button세 개를 라벨 없이 만들기 때문에,WindowPanel이 그리는 동일한 컨트롤과 달리 리더가 이름을 읽을 수 없습니다.actions를 직접 구성한다면 각 버튼에 라벨을 직접 붙이세요.
컴포넌트와 무관하게 적용되는 축(동작 줄이기 · 고대비 · 색 강제 모드 · 최소 터치 타겟)은 전역 접근성 축에 정리되어 있습니다.