Slint
マルチプラットフォーム対応のネイティブGUIツールキット。宣言的UIマークアップ言語と効率的なレンダリングエンジンを提供。組み込みシステムからデスクトップアプリまで幅広く対応し、商用サポートも利用可能。
GitHub概要
slint-ui/slint
Slint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.
トピックス
スター履歴
フレームワーク
Slint
概要
Slint(旧SixtyFPS)は、Rust、C++、JavaScript、Pythonアプリ向けのオープンソース宣言的GUIツールキットです。組み込みシステム、デスクトップ、モバイルプラットフォーム、さらにはWebAssembly経由でWebブラウザまで、多様な環境でネイティブユーザーインターフェースを構築できます。
詳細
Slint(スリント)は、2020年春にSixtyFPSとして開始されたプロジェクトが、2年間の開発と13回のリリースを経て3,000のGitHubスターを集めた後、2025年には安定した1.0 APIを誇る成熟したフレームワークに成長しました。
Slintの核心は宣言的なSlintマークアップ言語にあり、Qt QMLからインスピレーションを得て、ユーザーインターフェース全体を表現します。この.slintファイルは事前にコンパイルされ、純粋関数である式はコンパイラによって最適化されます。コンパイラはSlint言語を解析し、ネイティブRustまたはC++コードに直接コンパイルすることで、ネイティブパフォーマンスと効率的なメモリレイアウトを実現します。
技術的には、複数のレンダリングバックエンドとスタイルがコンパイル時に設定可能です:femtovgレンダラーはOpenGL ES 2.0を使用し、skiaレンダラーはSkiaを使用し、ソフトウェアレンダラーは追加の依存関係なしでCPUを使用します。この柔軟性により、ハイエンドデスクトップからリソース制約のある組み込みデバイスまで、幅広いターゲット環境に対応できます。
メリット・デメリット
メリット
- 真のクロスプラットフォーム: デスクトップ、モバイル、組み込み、Web対応
- 宣言的UI設計: 直感的で保守性の高いマークアップ言語
- マルチ言語サポート: Rust、C++、JavaScript、Python対応
- 高いパフォーマンス: 事前コンパイルとネイティブコード生成
- 多様なレンダリング: OpenGL、Skia、ソフトウェアレンダリング選択可能
- 組み込み特化: マイコンや裸環境でも動作
- 開発ツール充実: Live Preview、VSCode拡張、LSPサーバー
デメリット
- 学習コスト: 独自のSlint言語習得が必要
- エコシステム: ReactやFlutterに比べ第3パーティライブラリが少ない
- 新しさ: 比較的新しいフレームワークで実績が限定的
- ドキュメント: 日本語リソースが限定的
- デバッグ複雑性: マークアップ言語とバックエンド言語の両方の知識が必要
- コンパイル依存: 動的なUI変更には制約
主要リンク
書き方の例
Hello World アプリケーション
// hello.slint
export component MainWindow inherits Window {
Text {
text: "Hello, Slint!";
font-size: 24px;
}
}
// main.rs
use slint::ComponentHandle;
slint::slint! {
export component MainWindow inherits Window {
Text {
text: "Hello, Slint!";
font-size: 24px;
}
}
}
fn main() -> Result<(), slint::PlatformError> {
let ui = MainWindow::new()?;
ui.run()
}
インタラクティブなカウンターアプリ
// counter.slint
export component Counter inherits Window {
property <int> counter: 0;
VerticalBox {
Text {
text: "Counter: \{counter}";
font-size: 24px;
horizontal-alignment: center;
}
HorizontalBox {
Button {
text: "+";
clicked => { counter += 1; }
}
Button {
text: "-";
clicked => { counter -= 1; }
}
Button {
text: "Reset";
clicked => { counter = 0; }
}
}
}
}
// main.rs
use slint::ComponentHandle;
slint::include_modules!();
fn main() -> Result<(), slint::PlatformError> {
let ui = Counter::new()?;
ui.run()
}
複雑なUIレイアウトとスタイリング
// app.slint
struct Person := {
name: string,
age: int,
email: string,
}
export component PersonForm inherits Window {
property <Person> person: { name: "", age: 0, email: "" };
property <bool> is-valid: person.name != "" && person.email != "";
callback submit-form(Person);
VerticalBox {
padding: 20px;
spacing: 10px;
Text {
text: "Personal Information";
font-size: 20px;
font-weight: 700;
color: #2c3e50;
}
GridLayout {
Row {
Text { text: "Name:"; }
LineEdit {
text: person.name;
edited => { person.name = self.text; }
placeholder-text: "Enter your name";
}
}
Row {
Text { text: "Age:"; }
SpinBox {
value: person.age;
edited => { person.age = self.value; }
minimum: 0;
maximum: 150;
}
}
Row {
Text { text: "Email:"; }
LineEdit {
text: person.email;
edited => { person.email = self.text; }
placeholder-text: "[email protected]";
}
}
}
Button {
text: "Submit";
enabled: is-valid;
clicked => { submit-form(person); }
background: is-valid ? #3498db : #95a5a6;
}
if !is-valid : Text {
text: "Please fill in all required fields";
color: #e74c3c;
font-size: 12px;
}
}
}
// main.rs
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
slint::include_modules!();
fn main() -> Result<(), slint::PlatformError> {
let ui = PersonForm::new()?;
ui.on_submit_form(|person| {
println!("Form submitted:");
println!(" Name: {}", person.name);
println!(" Age: {}", person.age);
println!(" Email: {}", person.email);
});
ui.run()
}
リストとデータバインディング
// todo.slint
struct TodoItem := {
text: string,
completed: bool,
}
export component TodoApp inherits Window {
property <[TodoItem]> todos: [
{ text: "Learn Slint", completed: false },
{ text: "Build an app", completed: false },
{ text: "Ship it!", completed: false },
];
property <string> new-todo-text;
callback add-todo();
callback toggle-todo(int);
callback remove-todo(int);
VerticalBox {
padding: 20px;
spacing: 10px;
Text {
text: "Todo List";
font-size: 24px;
horizontal-alignment: center;
}
HorizontalBox {
LineEdit {
text: new-todo-text;
placeholder-text: "What needs to be done?";
edited => { new-todo-text = self.text; }
}
Button {
text: "Add";
enabled: new-todo-text != "";
clicked => { add-todo(); }
}
}
for todo[index] in todos : Rectangle {
height: 40px;
border-width: 1px;
border-color: #ddd;
HorizontalBox {
padding: 10px;
spacing: 10px;
CheckBox {
checked: todo.completed;
toggled => { toggle-todo(index); }
}
Text {
text: todo.text;
vertical-alignment: center;
color: todo.completed ? #888 : #000;
}
Button {
text: "×";
width: 30px;
clicked => { remove-todo(index); }
}
}
}
Text {
text: "Total: \{todos.length} items";
horizontal-alignment: center;
color: #666;
}
}
}
// main.rs
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
slint::include_modules!();
fn main() -> Result<(), slint::PlatformError> {
let ui = TodoApp::new()?;
let ui_handle = ui.as_weak();
// Add todo callback
ui.on_add_todo(move || {
let ui = ui_handle.unwrap();
let text = ui.get_new_todo_text();
if !text.is_empty() {
let mut todos = ui.get_todos();
todos.push(TodoItem {
text: text.clone(),
completed: false,
});
ui.set_todos(todos.into());
ui.set_new_todo_text(SharedString::new());
}
});
// Toggle todo callback
let ui_handle = ui.as_weak();
ui.on_toggle_todo(move |index| {
let ui = ui_handle.unwrap();
let mut todos = ui.get_todos();
if let Some(todo) = todos.get_mut(index as usize) {
todo.completed = !todo.completed;
}
ui.set_todos(todos.into());
});
// Remove todo callback
let ui_handle = ui.as_weak();
ui.on_remove_todo(move |index| {
let ui = ui_handle.unwrap();
let mut todos = ui.get_todos();
todos.remove(index as usize);
ui.set_todos(todos.into());
});
ui.run()
}
カスタムコンポーネントとアニメーション
// components.slint
export component AnimatedButton inherits Rectangle {
property <string> text;
property <color> base-color: #3498db;
property <bool> pressed: area.pressed;
callback clicked;
width: 120px;
height: 40px;
background: pressed ? base-color.darker(20%) : base-color;
border-radius: 8px;
animate background { duration: 150ms; }
area := TouchArea {
clicked => { clicked(); }
}
Text {
text: parent.text;
color: white;
font-size: 14px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
export component LoadingSpinner inherits Rectangle {
property <duration> rotation-duration: 1s;
property <bool> spinning: true;
width: 40px;
height: 40px;
spinner := Rectangle {
width: 100%;
height: 100%;
border-width: 4px;
border-color: transparent;
border-radius: 50%;
background: conic-gradient(0deg, transparent 0%, #3498db 50%, transparent 100%);
animate rotation-angle {
duration: rotation-duration;
iteration-count: spinning ? -1 : 0;
easing: linear;
}
rotation-angle: spinning ? 360deg : 0deg;
}
}
export component MainApp inherits Window {
property <bool> is-loading: false;
VerticalBox {
padding: 30px;
spacing: 20px;
Text {
text: "Custom Components Demo";
font-size: 24px;
horizontal-alignment: center;
}
HorizontalBox {
spacing: 15px;
AnimatedButton {
text: "Start";
base-color: #27ae60;
clicked => {
is-loading = true;
// タイマーで3秒後に停止
}
}
AnimatedButton {
text: "Stop";
base-color: #e74c3c;
clicked => { is-loading = false; }
}
}
if is-loading : LoadingSpinner {
horizontal-alignment: center;
}
Text {
text: is-loading ? "Loading..." : "Ready";
horizontal-alignment: center;
color: is-loading ? #3498db : #27ae60;
}
}
}
プロジェクトセットアップとビルド
# 新しいRustプロジェクト作成
cargo new slint-app
cd slint-app
# Cargo.tomlの依存関係設定
cat >> Cargo.toml << 'EOF'
[dependencies]
slint = "1.0"
[build-dependencies]
slint-build = "1.0"
EOF
# build.rsファイル作成(.slintファイルのコンパイル用)
cat > build.rs << 'EOF'
fn main() {
slint_build::compile("ui/app.slint").unwrap();
}
EOF
# UIディレクトリとファイル作成
mkdir ui
touch ui/app.slint
# 基本的なSlintファイル作成
cat > ui/app.slint << 'EOF'
export component MainWindow inherits Window {
Text {
text: "Hello from Slint!";
font-size: 24px;
}
}
EOF
# main.rsの更新
cat > src/main.rs << 'EOF'
slint::include_modules!();
fn main() -> Result<(), slint::PlatformError> {
let ui = MainWindow::new()?;
ui.run()
}
EOF
# 開発実行
cargo run
# リリースビルド
cargo build --release
# 異なるレンダリングバックエンド指定
# ソフトウェアレンダリング
cargo run --no-default-features --features backend-winit,renderer-software
# OpenGL ES
cargo run --no-default-features --features backend-winit,renderer-femtovg
# Skiaレンダリング
cargo run --features backend-winit,renderer-skia
# Webアプリケーション向けビルド
rustup target add wasm32-unknown-unknown
cargo install trunk
# index.htmlファイル作成
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Slint Web App</title>
</head>
<body></body>
</html>
EOF
# Cargo.tomlにwasm用の設定追加
cat >> Cargo.toml << 'EOF'
[target.'cfg(target_arch = "wasm32")'.dependencies]
slint = { version = "1.0", features = ["backend-winit", "renderer-femtovg"] }
EOF
# Webアプリのビルドと実行
trunk serve --release
# クロスプラットフォームビルド
# Windows向け
cargo build --target x86_64-pc-windows-gnu --release
# Android向け(NDKが必要)
cargo install cargo-apk
cargo apk build --release
# 組み込みLinux向け
cargo build --target armv7-unknown-linux-gnueabihf --release