디자인 시스템
픽셀이 아니라 역할
위젯 묶음은 디자인이 아닙니다. 디자인 시스템 일곱 개가 Column 과
Button 을 Mac, Android, Windows 앱처럼 보이는 것으로 바꿉니다. 위젯은 역할을
내보내고, 렌더러가 그것을 활성 시스템의 토큰으로 풉니다.
역할이 먼저, 리터럴은 탈출구
위젯은 역할을 내보냅니다. "이것은 제목", "이것은 표면 색", "이것은 중간 간격"입니다. 그 역할이 어떻게 보일지는 렌더러가 정합니다. 리터럴 값도 늘 쓸 수 있고, 그것은 언제나 "정확히 이것을 그려라"라는 뜻입니다.
이유는 프레임 예산입니다. Host는 렌더러의 UI 스레드에서 돌므로, Host가 하는 일은 모두 상호작용의 예산에서 빠집니다. Host가 토큰을 푼다면 다크 모드 전환이 영향받는 노드마다 속성을 다시 보내야 합니다. 렌더러가 풀면 같은 전환이 테마 레코드 하나이고, Compose가 필요한 곳만 무효화합니다.
이유가 하나 더 있습니다. 시스템 색 구성과 호스트 플랫폼은 렌더러가 이미 아는 사실입니다. Host가 그것을 풀려면 경계를 건너 물어야 하는데, 경계는 일부러 작게 유지합니다.
시스템 일곱 개, 위젯 트리 하나 구현
디자인 시스템은 한 쌍입니다. 토큰 표(색, 글꼴, 모양, 간격)와 컴포넌트 규칙(높이, 버튼 변형,
모션, 재질)입니다. DesignSystem 에는 일곱 개가 있습니다.
DesignSystem | 디자인 언어 |
|---|---|
Material3 | Google의 Material 3 |
Cupertino | macOS 26과 iOS 26 이전의 Apple Human Interface Guidelines |
LiquidGlass | macOS 26과 iOS 26이 스스로를 그리는 Apple의 현재 언어 |
Fluent | Microsoft의 WinUI와 Fluent 2 |
Gnome | GNOME의 Adwaita |
Breeze | KDE의 Breeze |
Deepin | Deepin의 DDE |
같은 트리를 다른 시스템으로 그리면 다르게 보이는 것이 의도입니다. 컴포넌트 규칙은
디자인에 중립적인 변형을 읽습니다. ButtonVariant 는 Filled,
Tonal, Outlined, Text 이고, 시스템마다 자기 방식으로
그립니다. Material 3는 큰 반지름과 물결 효과로, Apple의 시스템은 연속 곡률과 하이라이트로,
Fluent는 4 dp에 위쪽이 밝은 테두리로 그립니다.
디자인 시스템을 더해도 위젯, 속성 스키마, 와이어 포맷은 바뀌지 않습니다. Rust 열거형의 변형 하나, 토큰 표 하나, Kotlin의 규칙 묶음 하나입니다.
통일형과 적응형
애플리케이션이 시작할 때 고릅니다. Theme 과 DesignSystem 은 0.0.1에
있고, 아래 값은 지금 컴파일됩니다.
use compose_rust::{ColorScheme, DesignSystem, Theme};
// The same design system on every platform.
const UNIFIED: Theme = Theme::unified(DesignSystem::Material3);
// Follow the host platform. The fallback argument is required.
const ADAPTIVE: Theme = Theme::adaptive(DesignSystem::Material3);
// Light and dark are a separate axis. FollowSystem is the default.
const DARK: Theme = UNIFIED.with_color_scheme(ColorScheme::Dark);
트리를 만드는 층이 테마를 LaunchBuilder::with_theme 에 넘깁니다. 부르지 않으면
Theme::adaptive(DesignSystem::Material3) 입니다. 플랫폼 자신의 모양이고, 플랫폼에
그것이 없으면 Material 3입니다. 대체값이 필수 인자이므로 adaptive 가 고를 것이 없는
플랫폼은 없습니다.
| 플랫폼 | adaptive 가 고르는 것 |
|---|---|
| Android | Material 3 |
| macOS, iOS | Liquid Glass |
| Windows | Fluent |
| Linux, GNOME 세션 | GNOME |
| Linux, KDE 세션 | Breeze |
| Linux, 그 밖의 세션이나 알 수 없는 세션 | Deepin |
| Web | Fluent |
| 알아보지 못한 플랫폼 | 넘긴 대체값 |
Linux 줄은 시작할 때 XDG_CURRENT_DESKTOP 을 한 번 읽습니다. Web 줄은 사실이
아니라 선택입니다. 브라우저에는 자기 디자인 언어가 없으므로, 웹에서는 unified 를
직접 쓰는 편이 더 분명합니다.
ColorScheme 은 Light, Dark, FollowSystem
입니다. 시스템이 어두워지면 렌더러가 먼저 알고 스스로 적용합니다. Host는 관여하지 않습니다.
스크롤 위치와 포커스가 Kotlin에 머무는 것과 같은 이유입니다.
앱 색 구현
자기 브랜드가 있는 앱은 디자인 시스템 위에 Palette 를 얹습니다. 팔레트는 색 역할의
값만, 역할 하나와 구성 하나씩 바꿉니다. 모양, 글꼴, 간격, 높이, 모션, 컴포넌트 규칙은 시스템의
것으로 남습니다. 같은 주황색이 Material 3에서는 Material 3 버튼이 되고, Liquid Glass에서는 유리
캡슐이 됩니다.
use compose_rust::{Color, ColorRole, DesignSystem, Palette, Theme};
const EMBER: Palette = Palette::new()
.with(ColorRole::Primary, Color::rgb(0xE8590C), Color::rgb(0xFF8A4C))
.with(ColorRole::Background, Color::rgb(0xFFFBF8), Color::rgb(0x141110));
const THEME: Theme = Theme::adaptive(DesignSystem::Material3).with_palette(&EMBER);
with 는 밝은 값과 어두운 값을 함께 주고, with_light 와
with_dark 는 하나씩 줍니다. 두 구성이 함께 건너가므로 시스템을 따라 어두워질 때
Host에 묻지 않습니다. 주지 않은 역할은 시스템의 값을 그대로 씁니다.
Palette::check(design_system) 은 팔레트가 대비를 채우지 못한 짝을 나열합니다. 일곱
시스템 모두에 대해 자기 테스트에서 부르십시오. 플랫폼의 고대비 모드에서는 팔레트를 내려놓고
시스템 자신의 색을 그립니다.
경계를 건너는 것
SetTheme 레코드 하나입니다. 첫 배치의 첫 레코드로 보내고, 앱이 테마를 바꿀 때만
다시 보냅니다. 디자인 시스템, 대체값, 색 구성, 적응형 여부를 담습니다. 팔레트는 짧은 역할 값
목록으로 건너갑니다. 토큰 표는 건너가지 않습니다. Rust에서 데이터로 쓰고, 빌드할 때 렌더러에
생성해 넣고, 렌더러에서 풉니다.
렌더러가 토큰을 풀기 때문에, Rust 쪽 테스트는 어떤 역할을 보냈는지만 확인할 수 있습니다. 실제 색과 크기는 렌더러 테스트가 확인합니다.
Compose 포크로 옮기기 계획
디자인 시스템의 소스는 이 저장소를 떠나 thisisthepy의 Compose 포크로 옮겨 갔습니다.
org.thisisthepy.compose.* 아래의 평범한 Compose 라이브러리로 나누는 일은 아직 진행 중입니다
(#39). 그러면 compose-rust뿐
아니라 어느 Compose 앱이든 쓸 수 있습니다.
- 시스템마다 컴포넌트 라이브러리 하나:
org.thisisthepy.compose.material3,.cupertino,.fluent,.liquidglass등. - 그 위에서 기본으로 플랫폼 자신의 시스템을 따르는
org.thisisthepy.compose.adaptive. - 두 층이 함께 쓰는 계약인
org.thisisthepy.compose.designsystem.
역할, Theme 값, 와이어 레코드는 그대로입니다. 옮겨 가는 것은 그것을 그리는 Kotlin이
사는 곳입니다.