Iced入門


:point_right: ver 0.13.0
:point_right: [リポジトリ](https://github.com/iced-rs/iced)
:point_right: [doc](https://docs.rs/iced/latest/iced/)
:point_right: [book](https://book.iced.rs/)

## What is Iced?

> A cross-platform GUI library for Rust focused on simplicity and type-safety. Inspired by Elm.

[Elm](https://elm-lang.org/)にインスパイアされた単純さと型安全性にフォーカスしたクロスプラットフォーム用のGUIライブラリ

処理の流れは[bookにある遷移図](https://book.iced.rs/architecture.html#the-gui-trinity)を参照すること

## Introduction

基本的な説明は[book](https://book.iced.rs/)を参照すること

状態(State)、タスク(Task)、ウィジェット(Widgets)しか使わない

状態ごとにタスクを生成し、更新するロジックを実行して状態を変える

これを繰り返す、これだけ

## Hello World

簡単な画面を出す

0.12以前のバージョンだとSandboxやApplication Traitを使っていたが、0.13からは関数をそのまま代入できるようになった

状態(State)のためにHelloWorld構造体、タスク(Task)のために列挙型のOrigialTaskを定義していく(任意の名前に変えてください)

```rust
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)
}
```

![スクリーンショット 2024-07-28 122430.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2630694/95b63107-8c3e-87cf-9926-6ffdc048cc6b.png)

### ウィンドウ系の設定

ここら辺から呼び出すAPIの名前が被ってくるため注意

これらが学んでいくときにつらいところの一つだった

LSPやCopilotに任せて入力させるとエラーだらけになるので、何を使いたいかできちんと補完する内容を整理しながら使う

```rust
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()
}
```

![スクリーンショット 2024-07-28 201329.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2630694/e564ca42-ad14-c6bf-199d-6df36bfa3561.png)

### Buttonなど

見た目を変える場合の多くは`view`に対して実装をする

単純にボタンを4つ配置するだけ

`Clone`の実装が必要なので`Derive`に追加する

```diff_rust
-- #[derive(Debug)]
++ #[derive(Debug, Clone)]
enum OriginalMessage {}
```

さらに`column!`と`row!`マクロを利用し配置する

columnとrowは列と行になりそうだがcolumn!は垂直方向に積み上げていき、row!水平方向に重ねていく

```rust
    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"]),
        ]
    }
```

![スクリーンショット 2024-07-28 215815.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2630694/6a665715-047f-be51-dd45-62f2e1d72578.png)

他のインタラクティブなウィジェットに関しても同様にデフォルトだと入力を受け付けないため、これらボタンはカーソルホバーなどのインタラクティブな動作をしない

どのタイミングで受け取るかを含めてあらかじめ定義する必要がある

タイミングによって押せない状態にする必要が特にはないので、常時押せる状態にして、ボタンが押された場所をコマンドで保持してそれをターミナルでプリントする

```rust
#[derive(Debug, Clone)]
enum OritinalTask {
    Pressed(String),
}
```

加えてボタンをインタラクティブな状態するため`.on_press()`を追加してOritinalTaskに押された判定を代入する

その後updateの中でtaskを受け取り、判定をしてやりたいことを書く

```rust
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()))
        ],
    ]
}
```

![スクリーンショット 2024-08-12 201104.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2630694/a1547950-2068-19c3-79f6-4a825cb4ec93.png)

## テーマ変更

なぜか[built-inテーマが豊富](https://docs.iced.rs/iced/theme/enum.Theme.html)

HelloWorld構造体が使われていないので、テーマ名を代入していく

```diff_rust
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`を受け取り中身の文字列でマッチングをかける

```rust
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,
    }
}
```

```rust
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()
}
```

ボタンを押すとテーマが変わるようになる

![スクリーンショット 2024-09-19 233607.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2630694/0b651e39-f582-8ff9-7d5d-be051d86ff16.png)

## インプット系

widgetの`text_input`か`text_editor`を利用する

text_inputで一行入力、text_editorで複数行入力ができるのと、一般的によく使われる機能がデフォルトでついてくる

例えば、セレクトしたりスクロールしたりマウスで移動できたり本来だと実装しないといけないような基本操作は全て実装されている

`HelloWorld`と`OriginTask`は名前そのままに、中身をそれぞれ入力された文字列を保持できるようにする

特に`text_editor::Content`(State)と`text_editor::Action`(Task)が用意されているでそれを受け取るようにする

```diff_rust
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()
}
```

![スクリーンショット 2024-09-23 221757.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2630694/d929eb65-07d8-65fd-ab00-014727179c46.png)

フォーカスが当たるときちんと色も変わる

---

保存がしたい場合は保存ボタンを追加して、任意のファル名で保存できるようにメソッドとTaskを追加する

一行でファイル名保存したい場所をきめて、複数行の内容を保存する

```rust
#[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のテキストを使ってファイルに保存する

![スクリーンショット 2024-09-24 001044.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2630694/c8f8077e-752a-a0b6-46ec-174cd22dfc70.png)

保存後の状態を単純なboolにしたが、enumで判断できるようにしても良い

保存に成功すると文字列がちょこっとかわるのと、書かれていた内容が消える

![スクリーンショット 2024-09-24 001107.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2630694/2c20a6df-af33-1f71-49aa-6fb155368c42.png)

## マウス操作系

ここからさらに名前が被り始める

利用するのがeventモジュールのEventとmouseモジュールのEventになる

```rust
use iced::event::{self, Event, Status};
use iced::mouse::{
    self,
    Event::{ButtonPressed, CursorMoved},
};
```
```rust
// 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を送りそれを表示させる

```rust
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()
}
```

画面上にあるカーソルの位置やマウスのボタンクリックをキャプチャして表示させることができるようになる

![スクリーンショット 2024-09-26 000802.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2630694/c5d73c0c-d790-6e1d-6223-b6c92ddeb9fd.png)

取得できるイベントは先ほどのEnumにあるEventが全て取れる

ここでKeyboardやWindowなどの欲しいEventを取得しTaskとして何かしらの値を保持、update()でStateの値を更新、view()でStateから情報を受け取り表示を変える



```rust
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](https://github.com/iced-rs/iced/tree/master/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アプリが作れるのは良いと思う