Compose 위젯
rsx! 의 위젯
같은 크레이트는 rsx! 안에서 Compose 의 위젯 이름도 받습니다. Column,
Text, Button 과 서른 개 남짓입니다. 이 경로는
구현 되어 있고 지금 렌더러가 그립니다. CSS 대신 Compose 자체의
위젯과 디자인 시스템을 쓰고 싶을 때 씁니다.
앱의 모양
앱은 Element 를 돌려주는 함수 하나와 launch 호출 하나입니다. 크레이트
루트의 prelude 가 위젯, 훅, 매크로를 들여옵니다.
use dioxus_compose::prelude::*;
fn app() -> Element {
let mut count = use_signal(|| 0i32);
rsx! {
Column {
fill_max_width: true,
Text { text: format!("count: {}", count()) }
Button {
text: "increment",
on_click: move |_| count += 1,
}
}
}
}
fn main() {
dioxus_compose::launch(app);
}
launch 는 LaunchBuilder::new().launch(app) 의 줄임입니다. 빌더는 테마와 창
설명도 받습니다. 데스크톱과 iOS 에서는 Rust 의 main 이 렌더러를 돌립니다. Android 와
브라우저에서는 플랫폼이 루프를 가지므로 android_main! 과 web_main! 으로
진입점을 선언합니다.
LaunchBuilder::new()
.with_theme(Theme::unified(DesignSystem::Material3))
.launch(app);
위젯 목록
목록은 일부러 닫혀 있습니다. 렌더러는 정확히 이것들만 해석하고, 모르는 종류는 충돌 대신
ProtocolError 이벤트가 됩니다. 컴포저블마다의 속성과 기본값은
컴포저블 레퍼런스에 있습니다.
| 묶음 | 컴포저블 |
|---|---|
| 레이아웃 | Column, Row, Box, ScrollColumn, ScrollRow, Spacer |
| 텍스트와 입력 | Text, TextField, SelectionContainer |
| 컨트롤 | Button, Checkbox, RadioButton, Switch, Slider, Dropdown, DatePicker, TimePicker, ProgressIndicator, Chip, FloatingAction |
| 컨테이너 | Card, Surface, Scaffold, TopAppBar, Divider, Separator, Tooltip, Badge |
| 목록 | LazyColumn, LazyRow, LazyGrid |
| 내비게이션 | Navigation, NavigationItem, Tabs |
| 오버레이 | Dialog, Menu, Sheet, 그리고 트리에 속하지 않는 show_message |
| 그림 | Image, Icon, Canvas |
Box 가 아니라 dioxus_compose::Box
dioxus-core 0.7 의 rsx! 전개는 한정하지 않은 Box<T> 를 씁니다.
glob prelude 에 Compose 의 Box 가 있으면 std::boxed::Box 를 가려 그
전개가 깨지므로, prelude 는 그것을 빼 두었습니다. 한정된 경로로 씁니다.
모디파이어
Compose 에서 Modifier 가 어느 컴포저블에나 붙듯 여기서도 그렇습니다. 모든 위젯이 같은
열세 속성을 받습니다. weight, fill_max_width,
fill_max_height, width, height, padding,
padding_role, background, shape_role,
corner_radius, border_width, border_color,
elevation 입니다.
rsx! {
Card {
fill_max_width: true,
padding_role: SpaceRole::Md,
shape_role: ShapeRole::Large,
background: Paint::Role(ColorRole::SurfaceContainer),
Text { text: "in a panel" }
}
}
padding_role 과 shape_role 은 디자인 시스템의 답을, padding 과
corner_radius 는 직접 정한 값을 받습니다. 밀도와 모서리 반지름은 디자인 시스템마다
다르고 숫자는 레이아웃을 그중 하나에 묶으므로, 역할을 쓰는 편이 낫습니다. 쓰지 않은 속성은 아무것도
보내지 않습니다.
이벤트와 키 소비
핸들러는 보통의 클로저이고, 렌더러의 UI 스레드에서 동기로 불립니다. 핸들러가 만든 diff 는 같은 호출 안에서 돌아가므로 아무것도 큐에 쌓이지 않습니다.
| 핸들러 | 값 | 위젯 |
|---|---|---|
on_click | () | Button, NavigationItem 등 누를 수 있는 위젯 |
on_value_change | String | TextField |
on_submit | String | TextField |
on_focus_lost | () | TextField |
on_key_down | KeyEvent | TextField |
on_change | 컨트롤의 값 | Checkbox, RadioButton, Switch, Slider, Dropdown, DatePicker, TimePicker |
on_dismiss | () | Dialog, Menu, Sheet |
채팅 입력란은 Enter 로 보내고 Shift+Enter 로 줄을 바꿔야 합니다.
Dioxus 0.7 의 핸들러는 값을 돌려주지 않으므로, 대신 핸들러가 이벤트를 소비했다고 표시합니다. 웹의
preventDefault() 와 같은 생각입니다.
TextField {
placeholder: "Write a message",
multiline: true,
on_value_change: move |value| draft.set(value),
on_key_down: move |event: KeyEvent| {
if event.key() == Key::Enter && !event.shift_key() {
let message = draft().trim().to_owned();
if !message.is_empty() {
messages.write().push(message);
draft.set(String::new());
}
event.consume(); // the renderer's onKeyEvent returns true
}
}
}
입력기가 조합하는 동안 렌더러는 Rust 에 키 이벤트를 보내지 않습니다. 조합 중에 누른 Enter 는 음절을 확정할 뿐 제출하지 않습니다. 그래서 위의 단순한 조건이 편해서가 아니라 맞아서 쓰는 것입니다.
비제어 TextField
TextField 에는 value 속성이 없습니다. 편집 버퍼, 선택, 조합은 렌더러의
것이고, Rust 는 on_value_change, on_submit, on_focus_lost 로
소식을 듣습니다. 한글은 칠 때 자모를 모아 만들어지므로, 음절 중간에 Rust 가 값을 되돌려 쓰면 그
음절이 사라집니다. 버퍼를 렌더러에 두면 그 실패가 드문 것이 아니라 불가능한 것이 됩니다. 컴포넌트에서
입력란을 비우는 보통의 방법은 다른 Dioxus 앱에서처럼 새 정체성을 주는 것입니다.
목록과 스트리밍
보통 화면에 들어가는 몇백 행이라면 for 를 쓴 Column 으로 충분합니다. 한
행을 바꾸면 변경 하나만 가고 형제 행은 그대로입니다. 긴 목록에서는 LazyColumn 이 흐름을
뒤집습니다. 렌더러가 보이는 범위를 알려 주고, Host 는 정확히 그 범위와 여유분만 만듭니다. 항목이
10,000 개이고 보이는 행이 20 개이면 Host 는 28 개를 만듭니다.
rsx! {
LazyColumn {
item_count: messages.len(),
buffer: 4,
key_of: move |index| messages()[index].id.clone(),
item: move |index| rsx! {
Text { text: messages()[index].body.clone() }
},
}
}
막히는 일(네트워크 스트림, 파일, PTY)은 워커 스레드에서 합니다. 워커는 시그널에 쓰고, 프레임은
Host 가 직접 렌더러에 요청하며, 요청은 프레임 하나로 합쳐집니다. 토큰 단위로 길어지는 글은 덧붙일
꼬리만 프레임마다 모아서 보내므로, 긴 답이 토큰마다 통째로 다시 가지 않습니다. 이것을 위한
rsx! 수준 API 는 아직 없습니다. Host::append_text 는 프로토콜 노드 id 를
받으므로, 컴포넌트 코드는 시그널에 써서 스트리밍합니다.
디자인 시스템
위젯은 색이 아니라 역할을 말합니다. 색 역할, 타입 역할, 모양 역할, 간격 역할입니다. 렌더러가 디자인
시스템으로 역할을 풀기 때문에, 다크 모드 전환은 노드마다의 변경이 아니라 테마 변경 하나입니다. 일곱
시스템이 들어 있습니다. Material3, Cupertino, Fluent,
Gnome, Breeze, Deepin, LiquidGlass 입니다.
// The same design system everywhere
LaunchBuilder::new().with_theme(Theme::unified(DesignSystem::Material3)).launch(app);
// Follow the host platform; the fallback covers platforms with no look of their own
LaunchBuilder::new().with_theme(Theme::adaptive(DesignSystem::Material3)).launch(app);
아무것도 정하지 않으면 호스트 플랫폼을 따르고, 대체는 Material 3 입니다. 데스크톱 앱은 설정 없이도
그 데스크톱처럼 보여야 하기 때문입니다. use_design_system() 은 렌더러가 고른 시스템을
알려 줍니다. 디자인 시스템 자체는 Compose 코드이고
compose-rust 에 있습니다.
상태가 사는 곳
| 상태 | 주인 | 이유 |
|---|---|---|
| 앱과 도메인 상태 | Rust(시그널) | 프로그램 자체이고, Dioxus 가 변경으로 바꿉니다. |
| 편집 중인 글자, IME 조합 | 렌더러 | 조합은 절대 왕복하면 안 됩니다. |
| 스크롤 위치, 포커스 | 렌더러 | UI 의 지역 상태이고, Rust 가 가지면 한 프레임 늦습니다. |
| 애니메이션 진행 | 렌더러 | Compose 프레임 시계로 돕니다. |
| 대화 상자, 메뉴, 시트가 열렸는지 | 렌더러 | 넘긴 불리언이 시작값이고, 열고 닫는 과정은 렌더러가 갖습니다. |
예제
위젯 경로의 예제는 compose-rust/samples/ 에 있습니다. 계산기, 메모장, 채팅, 할 일 목록,
상점 등입니다. 메모장을 macOS 에서 릴리스로 빌드하면 옆에 런타임이 없는 파일 하나, 28.76 MB
입니다.