3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Iced入門

Last updated at Posted at 2024-09-23

:point_right: ver 0.13.0
:point_right: リポジトリ
:point_right: doc
:point_right: book

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

スクリーンショット 2024-07-28 122430.png

ウィンドウ系の設定

ここら辺から呼び出す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()
}

スクリーンショット 2024-07-28 201329.png

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

スクリーンショット 2024-07-28 215815.png

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

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

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

#[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()))
        ],
    ]
}

スクリーンショット 2024-08-12 201104.png

テーマ変更

なぜかbuilt-inテーマが豊富

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

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

スクリーンショット 2024-09-19 233607.png

インプット系

widgetのtext_inputtext_editorを利用する

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

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

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

特に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()
}

スクリーンショット 2024-09-23 221757.png

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


保存がしたい場合は保存ボタンを追加して、任意のファル名で保存できるようにメソッドと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のテキストを使ってファイルに保存する

スクリーンショット 2024-09-24 001044.png

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

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

スクリーンショット 2024-09-24 001107.png

マウス操作系

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

利用するのが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()
}

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

スクリーンショット 2024-09-26 000802.png

取得できるイベントは先ほどの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アプリが作れるのは良いと思う

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?