What is Iced?
A cross-platform GUI library for Rust focused on simplicity and type-safety. Inspired by Elm.
Elmにインスパイアされた単純さと型安全性にフォーカスしたクロスプラットフォーム用のGUIライブラリ
処理の流れはbookにある遷移図を参照すること
Introduction
基本的な説明はbookを参照すること
状態(State)、タスク(Task)、ウィジェット(Widgets)しか使わない
状態ごとにタスクを生成し、更新するロジックを実行して状態を変える
これを繰り返す、これだけ
Hello World
簡単な画面を出す
0.12以前のバージョンだとSandboxやApplication Traitを使っていたが、0.13からは関数をそのまま代入できるようになった
状態(State)のためにHelloWorld構造体、タスク(Task)のために列挙型のOrigialTaskを定義していく(任意の名前に変えてください)
use iced::widget::{column, text, Column};
use iced::Task;
#[derive(Debug)]
enum OriginalTask {}
#[derive(Debug, Clone)]
struct HelloWorld(String);
impl Default for HelloWorld {
fn default() -> Self {
Self("Hello, World!".to_string())
}
}
fn update(_hw: &mut HelloWorld, _task: OriginalTask) -> impl Into<Task<OriginalTask>> {
Task::none()
}
fn view(hw: &HelloWorld) -> Column<OriginalTask> {
column![text(hw.0.clone())]
}
fn main() -> iced::Result {
iced::run("title here", update, view)
}
ウィンドウ系の設定
ここら辺から呼び出すAPIの名前が被ってくるため注意
これらが学んでいくときにつらいところの一つだった
LSPやCopilotに任せて入力させるとエラーだらけになるので、何を使いたいかできちんと補完する内容を整理しながら使う
use iced::widget::{column, text, Column};
use iced::window::{self, Position};
use iced::{Settings, Size, Task};
#[derive(Debug, Clone)]
enum OriginalTask {}
#[derive(Debug, Clone)]
struct HelloWorld(String);
impl Default for HelloWorld {
fn default() -> Self {
Self("Hello, World!".to_string())
}
}
fn update(_hw: &mut HelloWorld, _task: OriginalTask) -> impl Into<Task<OriginalTask>> {
Task::none()
}
fn view(hw: &HelloWorld) -> Column<OriginalTask> {
column![text(hw.0.clone())]
}
fn main() -> iced::Result {
let settings = Settings {
..Default::default()
};
let window_settings = window::Settings {
size: Size::new(300.0, 300.0),
max_size: Some(Size::new(500.0, 500.0)),
min_size: Some(Size::new(100.0, 100.0)),
position: Position::Centered,
..Default::default()
};
iced::application("title here", update, view)
.settings(settings)
.window(window_settings)
.run()
}
Buttonなど
見た目を変える場合の多くはview
に対して実装をする
単純にボタンを4つ配置するだけ
Clone
の実装が必要なのでDerive
に追加する
-- #[derive(Debug)]
++ #[derive(Debug, Clone)]
enum OriginalMessage {}
さらにcolumn!
とrow!
マクロを利用し配置する
columnとrowは列と行になりそうだがcolumn!は垂直方向に積み上げていき、row!水平方向に重ねていく
fn view(&self) -> Element<'_, Self::Message, Self::Theme, iced::Renderer> {
column![
row![button("Top Left"), button("Top Right")],
row![button("Bottom Left"), button("Bottom Right"]),
]
}
他のインタラクティブなウィジェットに関しても同様にデフォルトだと入力を受け付けないため、これらボタンはカーソルホバーなどのインタラクティブな動作をしない
どのタイミングで受け取るかを含めてあらかじめ定義する必要がある
タイミングによって押せない状態にする必要が特にはないので、常時押せる状態にして、ボタンが押された場所をコマンドで保持してそれをターミナルでプリントする
#[derive(Debug, Clone)]
enum OritinalTask {
Pressed(String),
}
加えてボタンをインタラクティブな状態するため.on_press()
を追加してOritinalTaskに押された判定を代入する
その後updateの中でtaskを受け取り、判定をしてやりたいことを書く
fn update(_hw: &mut HelloWorld, task: OriginalTask) -> impl Into<Task<OriginalTask>> {
match task {
OriginalTask::Pressed(task) => {
println!("Button Pressed: {}", task);
}
}
Task::none()
}
fn view(_hw: &HelloWorld) -> Column<OriginalTask> {
column![
row![
button("Top Left").on_press(OriginalTask::Pressed("Top Left".to_string())),
button("Top Right").on_press(OriginalTask::Pressed("Top Right".to_string()))
],
row![
button("Bottom Left").on_press(OriginalTask::Pressed("Bottom Left".to_string())),
button("Bottom Right").on_press(OriginalTask::Pressed("Bottom Right".to_string()))
],
]
}
テーマ変更
HelloWorld構造体が使われていないので、テーマ名を代入していく
impl Default for HelloWorld {
fn default() -> Self {
-- Self("Hello, World!".to_string())
++ Self("Nord".to_string())
}
}
...省略...
fn main() -> iced::Result {
let settings = Settings {
..Default::default()
};
let window_settings = window::Settings {
size: Size::new(300.0, 300.0),
max_size: Some(Size::new(500.0, 500.0)),
min_size: Some(Size::new(100.0, 100.0)),
position: Position::Centered,
..Default::default()
};
iced::application("title here", update, view)
.settings(settings)
.window(window_settings)
++ .theme(set_theme) // or .theme(|hw| set_theme(hw))
.run()
}
theme()
はview()
やupdate()
が最初に受け取った引数の構造体をState
として受け取るので、それをそのままset_theme()
に代入する
引数で&HelloWorld
を受け取り中身の文字列でマッチングをかける
fn set_theme(hw: &HelloWorld) -> Theme {
match hw.0.as_str() {
"Nord" => Theme::Nord,
"SolarizedDark" => Theme::SolarizedDark,
"KanagawaDragon" => Theme::KanagawaDragon,
"GruvboxDark" => Theme::GruvboxDark,
_ => Theme::Light,
}
}
use iced::theme::Theme;
use iced::widget::{button, column, row, text, Column};
use iced::window::{self, Position};
use iced::{Settings, Size, Task};
#[derive(Debug, Clone)]
enum OriginalTask {
Pressed(String),
}
#[derive(Debug, Clone)]
struct HelloWorld(String);
impl Default for HelloWorld {
fn default() -> Self {
Self("Nord".to_string())
}
}
// ここで状態としてテーマの文字列を代入する
fn update(hw: &mut HelloWorld, task: OriginalTask) -> impl Into<Task<OriginalTask>> {
match task {
OriginalTask::Pressed(task) => {
println!("Button Pressed: {}", task);
match task.as_str() {
"Top Left" => {
hw.0 = "Nord".to_string();
}
"Top Right" => {
hw.0 = "SolarizedDark".to_string();
}
"Bottom Left" => {
hw.0 = "GruvboxDark".to_string();
}
"Bottom Right" => {
hw.0 = "KanagawaDragon".to_string();
}
_ => unreachable!(),
}
}
}
Task::none()
}
fn view(_hw: &HelloWorld) -> Column<OriginalTask> {
column![
row![
button("Top Left").on_press(OriginalTask::Pressed("Top Left".to_string())),
button("Top Right").on_press(OriginalTask::Pressed("Top Right".to_string()))
],
row![
button("Bottom Left").on_press(OriginalTask::Pressed("Bottom Left".to_string())),
button("Bottom Right").on_press(OriginalTask::Pressed("Bottom Right".to_string()))
],
]
}
fn theme(hw: &HelloWorld) -> Theme {
match hw.0.as_str() {
"Nord" => Theme::Nord,
"SolarizedDark" => Theme::SolarizedDark,
"KanagawaDragon" => Theme::KanagawaDragon,
"GruvboxDark" => Theme::GruvboxDark,
_ => Theme::Light,
}
}
fn main() -> iced::Result {
let settings = Settings {
..Default::default()
};
let window_settings = window::Settings {
size: Size::new(300.0, 300.0),
max_size: Some(Size::new(500.0, 500.0)),
min_size: Some(Size::new(100.0, 100.0)),
position: Position::Centered,
..Default::default()
};
iced::application("title here", update, view)
.settings(settings)
.window(window_settings)
.theme(set_theme)
.run()
}
ボタンを押すとテーマが変わるようになる
インプット系
widgetのtext_input
かtext_editor
を利用する
text_inputで一行入力、text_editorで複数行入力ができるのと、一般的によく使われる機能がデフォルトでついてくる
例えば、セレクトしたりスクロールしたりマウスで移動できたり本来だと実装しないといけないような基本操作は全て実装されている
HelloWorld
とOriginTask
は名前そのままに、中身をそれぞれ入力された文字列を保持できるようにする
特にtext_editor::Content
(State)とtext_editor::Action
(Task)が用意されているでそれを受け取るようにする
use iced::font::{Family, Weight};
use iced::widget::{column, text_editor, text_input, Column};
use iced::window::{self, Position};
use iced::{Font, Size, Task};
#[derive(Debug, Clone)]
enum OriginalTask {
Message(String),
++ EditorAction(text_editor::Action),
}
#[derive(Debug)]
struct HelloWorld {
input: String,
++ editor: text_editor::Content,
}
impl Default for HelloWorld {
fn default() -> Self {
Self {
input: "".to_string(),
++ editor: text_editor::Content::new(),
}
}
}
fn update(hw: &mut HelloWorld, task: OriginalTask) -> impl Into<Task<OriginalTask>> {
match task {
OriginalTask::Message(message) => {
hw.input = message;
}
++ OriginalTask::EditorAction(action) => {
++ hw.editor.perform(action);
++ }
}
Task::none()
}
fn view(hw: &HelloWorld) -> Column<OriginalTask> {
++ let input_line = text_input("single inputs", hw.input.as_str())
++ .on_input(OriginalTask::Message);
++
++ let content = text_editor(&hw.editor)
++ .placeholder("please input something new")
++ .on_action(OriginalTask::EditorAction)
++ .height(200.0);
++
++ column![input_line, content].spacing(5.0).padding(20.0)
}
fn main() -> iced::Result {
let window_settings = window::Settings {
size: Size::new(300.0, 300.0),
max_size: Some(Size::new(500.0, 500.0)),
min_size: Some(Size::new(100.0, 100.0)),
position: Position::Centered,
..Default::default()
};
let default_font = Font {
family: Family::Monospace,
weight: Weight::Normal,
..Default::default()
};
iced::application("title here", update, view)
.default_font(default_font)
.window(window_settings)
.run()
}
フォーカスが当たるときちんと色も変わる
保存がしたい場合は保存ボタンを追加して、任意のファル名で保存できるようにメソッドとTaskを追加する
一行でファイル名保存したい場所をきめて、複数行の内容を保存する
#[derive(Debug, Clone)]
enum OriginalTask {
Message(String),
EditorAction(text_editor::Action),
Submit,
Bigger,
}
#[derive(Debug)]
struct HelloWorld {
input: String,
editor: text_editor::Content,
is_saved: bool,
size: u16,
}
impl Default for HelloWorld {
fn default() -> Self {
Self {
input: "".to_string(),
editor: text_editor::Content::new(),
is_saved: false,
size: 13,
}
}
}
impl HelloWorld {
fn save(&self) -> io::Result<()> {
let fname = &self.input;
let path = Path::new(&fname);
let context = &self.editor;
let text = context.text();
let mut file = File::create(path)?;
file.write_all(text.as_bytes())?;
Ok(())
}
}
fn update(hw: &mut HelloWorld, task: OriginalTask) -> impl Into<Task<OriginalTask>> {
match task {
OriginalTask::Message(message) => {
hw.input = message;
}
OriginalTask::EditorAction(action) => {
hw.editor.perform(action);
}
OriginalTask::Submit => {
if let Ok(()) = hw.save() {
hw.is_saved = true;
hw.editor = text_editor::Content::new();
hw.input = String::new();
hw.size = 13;
}
}
OriginalTask::Bigger => {
hw.size += 2;
} }
Task::none()
}
fn view(hw: &HelloWorld) -> Column<OriginalTask> {
let input_line = if hw.is_saved {
text_input("successfully saved", hw.input.as_str()).on_input(OriginalTask::Message)
} else {
text_input("single inputs", hw.input.as_str())
.on_input(OriginalTask::Message)
.size(hw.size)
};
let content = text_editor(&hw.editor)
.placeholder("please input something new")
.on_action(OriginalTask::EditorAction)
.height(170.0);
let btn = if hw.input.is_empty() {
button("Save").on_press(OriginalTask::Bigger)
} else {
button("Save").on_press(OriginalTask::Submit)
};
column![input_line, content, btn]
.align_x(Center)
.spacing(5.0)
.padding(20.0)
}
ボタンが押されたタイミングでのinputとcontextのテキストを使ってファイルに保存する
保存後の状態を単純なboolにしたが、enumで判断できるようにしても良い
保存に成功すると文字列がちょこっとかわるのと、書かれていた内容が消える
マウス操作系
ここからさらに名前が被り始める
利用するのがeventモジュールのEventとmouseモジュールのEventになる
use iced::event::{self, Event, Status};
use iced::mouse::{
self,
Event::{ButtonPressed, CursorMoved},
};
// iced::event::Event
pub enum Event {
Keyboard(Event),
Mouse(Event),
Window(Event),
Touch(Event),
}
// iced::mouse::Event
pub enum Event {
CursorEntered,
CursorLeft,
CursorMoved {
position: Point,
},
ButtonPressed(Button),
ButtonReleased(Button),
WheelScrolled {
delta: ScrollDelta,
},
}
アプリの画面上にあるときだけキーボードやマウスの状態を測れるのではなく、デーモンとして起動していたりドラックアンドドロップしたりして、画面上になくても起きているイベントを取得する必要がある
そのため、これらイベントを受動的に取得(listen)してイベントを発火させるのがSubscription
になる
先程の利用するEventのみ取得してそのTaskを送りそれを表示させる
use iced::event::{self, Event, Status};
use iced::font::{Family, Weight};
use iced::mouse::{
self,
Event::{ButtonPressed, CursorMoved},
};
use iced::widget::{column, horizontal_rule, mouse_area, text, Column};
use iced::window::{self, Position};
use iced::Alignment::Center;
use iced::{Font, Point, Size, Task};
#[derive(Debug, Clone)]
enum OriginalTask {
PointUpdate(Point),
ButtonPressed(mouse::Button),
}
#[derive(Debug)]
struct HelloWorld {
mouse_position: Point,
mouse_button: Option<mouse::Button>,
}
fn update(hw: &mut HelloWorld, task: OriginalTask) -> Task<OriginalTask> {
match task {
OriginalTask::PointMove(point) => hw.mouse_position = point,
OriginalTask::ButtonPressed(button) => hw.mouse_button = Some(button),
}
Task::none()
}
fn view(hw: &HelloWorld) -> Column<OriginalTask> {
let mouse_area = mouse_area(text(format!("{:?}", hw.mouse_position)));
let mouse_button = text(format!("{:?}", hw.mouse_button));
column![mouse_area, horizontal_rule(1.0), mouse_button]
.align_x(Center)
.spacing(20.0)
.padding(20.0)
}
fn mouse_event_handling(_hw: &HelloWorld) -> iced::Subscription<OriginalTask> {
event::listen_with(|event, status, window| match (event, status, window) {
(Event::Mouse(CursorMoved { position }), Status::Ignored, _) => {
Some(OriginalTask::PointUpdate(position))
}
(Event::Mouse(ButtonPressed(button)), Status::Ignored, _) => {
Some(OriginalTask::ButtonPressed(button))
}
_ => None,
})
}
fn main() -> iced::Result {
let window_settings = window::Settings {
size: Size::new(300.0, 300.0),
max_size: Some(Size::new(500.0, 500.0)),
min_size: Some(Size::new(100.0, 100.0)),
position: Position::Centered,
..Default::default()
};
let default_font = Font {
family: Family::Monospace,
weight: Weight::Bold,
..Default::default()
};
iced::application("title here", update, view)
.default_font(default_font)
.subscription(mouse_event_handling)
.window(window_settings)
.run()
}
画面上にあるカーソルの位置やマウスのボタンクリックをキャプチャして表示させることができるようになる
取得できるイベントは先ほどのEnumにあるEventが全て取れる
ここでKeyboardやWindowなどの欲しいEventを取得しTaskとして何かしらの値を保持、update()でStateの値を更新、view()でStateから情報を受け取り表示を変える
fn mouse_event_handling(_hw: &HelloWorld) -> iced::Subscription<OriginalTask> {
event::listen_with(|event, status, window| match (event, status, window) {
(Event::Mouse(CursorMoved { position }), Status::Ignored, _) => {
Some(OriginalTask::PointUpdate(position))
}
(Event::Mouse(ButtonPressed(button)), Status::Ignored, _) => {
Some(OriginalTask::ButtonPressed(button))
}
_ => None,
})
}
余談
解説記事などが上がっていたがver0.12未満が多く、バージョンによってかなりAPIが変更されていたためほとんど動かなかった
更にver0.13に上がったため0.12で動いていたのが更に動かなくなった
公式のexamplesも動かないため非常に写経がしずらく、そこそこ覚えるのが大変だった
Disclaimer
iced is experimental software. If you expect the documentation to hold your hand as you learn the ropes, you are in for a frustrating experience.
...
If you don’t like the sound of that, you expect to be spoonfed, or you feel frustrated and struggle to use the library; then I recommend you to wait patiently until the book is finished
公式もこれは実験的なcrateでありドキュメントがきちんと役立たないとして回答をしていた
慣れるまでに手間がかかるのはドキュメント通りだったが、Rustの強みであるTraitやStruct、Enumなどの理解が今回も学習をするうえで非常に頼もしかった
上級者向けと書いてはいるが、初心者の私でも少しずつ書いていくと今回の記事で書いたような簡単なGUIアプリもどきが書けた
もちろん複雑なものはまだ書けないし、widgetなどの特徴などなど細かい点の理解まではできてはいない
それでも簡単に配布可能なGUIアプリが作れるのは良いと思う