dioxus-compose 가이드

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_changeStringTextField
on_submitStringTextField
on_focus_lost()TextField
on_key_downKeyEventTextField
on_change컨트롤의 값Checkbox, RadioButton, Switch, Slider, Dropdown, DatePicker, TimePicker
on_dismiss()Dialog, Menu, Sheet

채팅 입력란은 Enter 로 보내고 Shift+Enter 로 줄을 바꿔야 합니다. Dioxus 0.7 의 핸들러는 값을 돌려주지 않으므로, 대신 핸들러가 이벤트를 소비했다고 표시합니다. 웹의 preventDefault() 와 같은 생각입니다.

dioxus-compose/examples/desktop_demo.rs
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
        }
    }
}
IME 조합 중의 Enter

입력기가 조합하는 동안 렌더러는 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 입니다.