DraggablePanel#
화면 전체를 덮는 child 위에 드래그 가능한 핸들을 띄우고, 핸들을 놓으면
가장 가까운 화면 가장자리에 스냅 도킹되는 플로팅 패널입니다. 핸들을 탭하면
호출자가 넘긴 panel 콘텐츠가 펼쳐집니다 — Apple PencilKit 의 떠 있는 도구
팔레트와 같은 동작이며, 하나의 팔레트로 여러 캔버스(예: 도서 뷰어 + 노트)를
제어하는 용도에 적합합니다.
CoUI 는 도메인 무관 generic chrome 만 제공합니다 — 펜/색/두께 같은 도메인
콘텐츠는 panel 슬롯에 호출자가 채웁니다.
Live Preview#
class DraggablePanelDefaultExample extends StatefulComponent {
const DraggablePanelDefaultExample({super.key});
@override
State<DraggablePanelDefaultExample> createState() =>
_DraggablePanelDefaultExampleState();
}
class _DraggablePanelDefaultExampleState
extends State<DraggablePanelDefaultExample> {
final CoreDraggablePanelController _controller =
CoreDraggablePanelController(initiallyOpen: true);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Component build(BuildContext context) {
return div(
[
DraggablePanel(
controller: _controller,
panel: div(
[
const Text('Pen').bodyMedium,
const Text('Eraser').bodyMedium,
const Text('Color').bodyMedium,
],
classes: 'flex flex-col gap-${CoreSpace.scale.space8}',
),
child: div(
[const Text('Canvas').bodyMedium],
classes:
'flex items-center justify-center w-full h-full bg-surface-container-low',
),
),
],
styles: Styles(
raw: const {'width': '${360 / 16}rem', 'height': '${360 / 16}rem'},
),
);
}
}
class DraggablePanelDefaultExample extends StatefulWidget {
const DraggablePanelDefaultExample({super.key});
@override
State<DraggablePanelDefaultExample> createState() =>
_DraggablePanelDefaultExampleState();
}
class _DraggablePanelDefaultExampleState
extends State<DraggablePanelDefaultExample> {
final CoreDraggablePanelController _controller =
CoreDraggablePanelController(initiallyOpen: true);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
width: 360,
height: 360,
child: DraggablePanel(
controller: _controller,
panel: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: CoreSpace.space8,
children: [
const Text('Pen').bodyMedium,
const Text('Eraser').bodyMedium,
const Text('Color').bodyMedium,
],
),
child: ColoredBox(
color: theme.colorScheme.surfaceContainerLow!.toValue(),
child: Center(child: const Text('Canvas').bodyMedium),
),
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 화면 어디서나 접근 가능해야 하는 떠 있는 도구 팔레트 / 액션 패널
- 사용자가 위치를 옮기고 가장자리에 붙여 두고 싶은 보조 컨트롤
- 본문(스크롤/줌되는 캔버스 등) 위에 항상 떠 있어야 하는 패널
대신 다른 컴포넌트를 사용하세요:
- 트리거에 고정 앵커된 팝업:
Popover/DropdownMenu - 모달:
Dialog/Drawer - 고정 위치 액션 버튼:
Fab - 창 외형(타이틀 바 + 컨트롤):
WindowPanel
기본 사용법#
final controller = CoreDraggablePanelController(initiallyOpen: true);
DraggablePanel(
controller: controller,
panel: const Column(
mainAxisSize: MainAxisSize.min,
children: [Text('Pen'), Text('Eraser'), Text('Color')],
),
child: const Center(child: Text('Canvas')),
)
final controller = CoreDraggablePanelController(initiallyOpen: true);
DraggablePanel(
controller: controller,
panel: div([
const Text('Pen').bodyMedium,
const Text('Eraser').bodyMedium,
const Text('Color').bodyMedium,
], classes: 'flex flex-col gap-\${CoreSpace.scale.space8}'),
child: div([const Text('Canvas').bodyMedium],
classes: 'flex items-center justify-center w-full h-full'),
)
Props / Parameters#
| Param | Type | Default | 설명 |
|---|---|---|---|
child |
Widget? / Component? |
null |
패널이 떠 있는 본문 (보통 화면 전체) |
panel |
Widget? / Component? |
null |
핸들을 열었을 때 펼쳐지는 패널 콘텐츠 |
handle |
Widget? / Component? |
null |
커스텀 핸들 콘텐츠 (null 이면 기본 드래그 글리프) |
dockType |
CoreDraggablePanelDockType |
.inside |
가장자리 도킹 방식 |
enabled | bool | true | 드래그 가능 여부 |
closeOnTapOutside |
bool |
true |
열린 패널 바깥 탭 시 닫힘 |
dockOffset |
double |
CoreSpace.space8 |
도킹 시 가장자리와의 간격 (logical px) |
controller |
CoreDraggablePanelController? |
null |
위치/열림 상태 프로그래매틱 제어 |
onPositionChanged |
void Function(double x, double y)? |
null |
핸들 위치 확정 시 콜백 (영속화용) |
onOpenChanged |
CoreValueChanged<bool>? |
null |
패널 열림 상태 변경 콜백 |
draggablePanelStyle |
CoreDraggablePanelStyle? |
null |
인스턴스별 chrome 스타일 (단일 진입점) |
스타일 시스템#
모든 시각 chrome 은 단일 draggablePanelStyle (CoreDraggablePanelStyle)
슬롯으로 흐릅니다. 평면 chrome prop 은 노출하지 않습니다.
Resolve chain#
CoreDraggablePanelStyle.defaultX
→ CoreDraggablePanelTheme.style // 프로젝트 공통
→ widget.draggablePanelStyle // 인스턴스별
Style 필드#
| 필드 | 기본값 |
|---|---|
panelBackgroundColor | surface |
panelBorderColor / panelBorderWidth |
outlineVariant / stroke1 |
panelBorderRadius | radius16 |
panelPadding | space8 |
panelWidth | size240 |
panelShadow | CoreShadow.lg |
handleSize | size48 |
handleBackgroundColor / handleForegroundColor |
surface / onSurfaceVariant |
handleBorderRadius | radius16 |
handleShadow | CoreShadow.md |
animationDuration | 200ms |
동작 스펙#
-
드래그: 핸들을 끌면 본문 위를 따라 이동합니다 (Flutter
GestureDetector, Web document 레벨mousemove리스너 — 핸들 밖으로 나가도 드래그 유지). -
스냅 도킹: 놓으면 가장 가까운 좌/우 가장자리에
dockOffset간격으로 스냅됩니다 (animationDuration으로 애니메이션). - 탭으로 열기/닫기: 드래그 없이 탭하면 패널이 토글됩니다.
- 방향 자동 선택: 패널은 핸들이 도킹된 반대쪽(공간이 있는 쪽)으로 펼쳐집니다.
크로스 플랫폼 차이점#
동작·API 는 양 플랫폼 동일합니다. 위치 좌표는 logical px 단위이며 Web 은
rem 으로 emit 됩니다. 핸들/패널의 런타임 위치(left/top)는 위젯이 드래그
상태에서 계산하고, chrome(색·radius·shadow·크기)은 resolver 가 제공합니다.
관련 컴포넌트#
WindowPanel— 타이틀 바 + 컨트롤이 있는 창 외형 패널Fab— 고정 위치 플로팅 액션 버튼 (드래그 없음)Popover— 트리거에 앵커된 팝업