【Rust】UTAUプラグインを作ってみる話。(3/3) ～GUI作成編～

[前回]:https://qiita.com/dino3616/items/f6fa4460002915e4577a

#はじめに
この記事は[こちらの記事][前回]の続きです。

#プラグインをGUIに拡張する
前回プラグインのロジック部分が完成したので、今回はそこにUIを付け加えていきます。

##設計
UIを実装するにあたって、まずはどんなウィジェット(ボタン、チェックボックス、etc)をどのように配置するのかを大まかに考えておく必要があります。
そこで今回作成するプラグインの仕様をもう一度確認しましょう。

>**ノートを指定したスケール(音階)にスナップさせる**
>*[前回][前回]より*

スケールというのは基本的にはいくつかが既に存在していて、ユーザーが新しく作り出すということは滅多にありません。多分。
ということは、Googleの検索ボックスのような`TextInput`によってスケールを決定するよりも、リストのようなものから限られたスケールを選択する方が良さそうです。
また画面下部に`Run`ボタンを配置して、それによって最終的なスケールの決定と変更の反映を行うようにしましょう。

##技術構成を考える
設計が大まかに定まったので早速コーディングしたいところですが、残念ながらRustの標準ライブラリだけではGUIを作成することは非常に困難です。
よって、GUIを作成するにあたってどのようなライブラリを使用するのかを決める必要があります。
RustにどんなGUIライブラリがあるのかはこちらの記事がとても参考になりました。

https://qiita.com/ogata-k/items/d9007250e1f26197f922

記事では

* azul
* conrod
* gtk
* iced
* OrbTk

が紹介されていました。
その中でも今回はicedを使っていこうと思います。

icedとは、Elmライクなアーキテクチャを持つ型安全なGUIライブラリです。
また先の記事のexampleの大部分ではトレイトによる継承(？)のような実装がされていました。
これにより実装しなければいけない関数が明確になり、完成までの道のりがはっきりするのもいいですね。

##実装
設計と技術構成が決まったところで、いよいよ実装していきます。
そしてできたものがこちら。

https://github.com/dino3616/even_scale

###ソースコード

<details><summary>src/main.rs</summary><div>

```Rust:src/main.rs
#![windows_subsystem="windows"]

mod even_scale;
mod app;
mod style;

use std::process;
use iced::{Application,Settings};

const WINDOW_WIDTH: u32=640;
const WINDOW_HEIGHT: u32=480;

fn main(){
    let window_setting=iced::window::Settings{
        size: (WINDOW_WIDTH,WINDOW_HEIGHT),
        min_size: None,
        max_size: None,
        resizable: false,
        decorations: false,
        transparent: true,
        always_on_top: false,
        icon: None,
    };
    let settings=Settings{
        window: window_setting,
        default_font: Some(include_bytes!("../fonts/NotoSansJP-Bold.otf")),
        ..Settings::default()
    };

    if let Err(err)=app::State::run(settings){
        eprint!("Error：{}\n",err);
        process::exit(1);
    }
}
```

</div></details>

<details><summary>src/app.rs</summary><div>

```Rust:src/app.rs
use std::process;
use iced::{Align,Application,button,Button,Clipboard,Color,Column,Command,Container,Element,executor,Font,HorizontalAlignment,image,Image,Length,pick_list,PickList,Text};
use utau_rs::*;
use super::{even_scale::*,style::*};

#[derive(Default)]
pub struct State{
    selected_scale: Option<Scale>,
    uta_sections: UtaSections,
    pick_list: pick_list::State<Scale>,
    run: button::State,
    exit: bool,
}

#[derive(Clone,Debug)]
pub enum Message{
    ScaleSelect(Scale),
    Run,
}

impl Application for State{
    type Executor=executor::Default;
    type Message=Message;
    type Flags=();

    fn new(_flags: ())->(Self,Command<Self::Message>){
        (Self::default(),Command::none())
    }

    fn title(&self)->String{
        String::from("even scale")
    }

    fn update(&mut self,message: Self::Message,_clipboard: &mut Clipboard)->Command<Self::Message>{
        match message{
            Message::ScaleSelect(scale)=>self.selected_scale=Some(scale),
            Message::Run=>{
                let selected_scale=match self.selected_scale{
                    Some(some)=>some,
                    None=>{
                        self.exit=true;
                        return Command::none();
                    }
                };
                if let Err(err)=even_scale(&mut self.uta_sections,selected_scale){
                    eprint!("Error：{}\n",err);
                    process::exit(1);
                }

                if let Err(err)=self.uta_sections.write(){
                    eprint!("Error：{}\n",err);
                    process::exit(1);
                }
                self.exit=true;
            }
        }

        Command::none()
    }

    fn view(&mut self)->Element<Message>{
        let comment_text=Text::new("↓choose scale that you want↓")
            .font(Font::External{name: "BRADHITC",bytes: include_bytes!("../fonts/karakaze-R.otf")})
            .size(30)
            .horizontal_alignment(HorizontalAlignment::Center);

        let scale_list=PickList::new(
            &mut self.pick_list,
            &Scale::ALL[..],
            self.selected_scale,
            Message::ScaleSelect,
        )
            .text_size(20)
            .style(PickList);

        let image=Container::new(Image::new(image::Handle::from_path("resource/background0.png")))
            .align_x(Align::Center)
            .align_y(Align::Center);

        let run_button=Button::new(
            &mut self.run,
            Text::new("Run")
                .size(30)
                .horizontal_alignment(HorizontalAlignment::Center)
        )
            .width(Length::Shrink)
            .height(Length::Shrink)
            .min_width(80)
            .style(Button)
            .on_press(Message::Run);

        let contents=Column::new()
            .align_items(Align::Center)
            .push(comment_text)
            .spacing(10)
            .push(scale_list)
            .push(image)
            .push(run_button);

        Container::new(contents)
            .width(Length::Fill)
            .height(Length::Fill)
            .center_x()
            .center_y()
            .style(Container)
            .into()
    }

    fn background_color(&self)->Color{
        Color::TRANSPARENT
    }

    fn should_exit(&self)->bool{
        self.exit
    }
}
```

</div></details>

<details><summary>src/style.rs</summary><div>

```Rust:src/style.rs
use iced::{Background,button,Color,container,pick_list};

const CONTAINER_R: u8=0x36;
const CONTAINER_G: u8=0x39;
const CONTAINER_B: u8=0x3F;
const BUTTON_R: u8=0xFF;
const BUTTON_G: u8=0x65;
const BUTTON_B: u8=0x1C;
const TEXT_R: u8=0xDE;
const TEXT_G: u8=0xDE;
const TEXT_B: u8=0xDE;

pub struct PickList;
impl pick_list::StyleSheet for PickList{
    fn menu(&self)->pick_list::Menu{
        pick_list::Menu::default()
    }

    fn active(&self)->pick_list::Style{
        pick_list::Style{
            text_color: Color::from_rgb8(CONTAINER_R,CONTAINER_G,CONTAINER_B),
            icon_size: 0.5,
            ..pick_list::Style::default()
        }
    }

    fn hovered(&self)->pick_list::Style{
        pick_list::Style{
            border_color: Color::BLACK,
            ..self.active()
        }
    }
}

pub struct Button;
impl button::StyleSheet for Button{
    fn active(&self)->button::Style{
        button::Style{
            background: Some(Background::Color(Color::from_rgb8(BUTTON_R,BUTTON_G,BUTTON_B))),
            border_radius: 10.0,
            text_color: Color::from_rgb8(CONTAINER_R,CONTAINER_G,CONTAINER_B),
            ..button::Style::default()
        }
    }

    fn hovered(&self)->button::Style{
        button::Style{
            border_width: 1.0,
            border_color: Color::WHITE,
            ..self.active()
        }
    }

    fn pressed(&self)->button::Style{
        let active=self.active();
        let hovered=self.hovered();

        button::Style{
            background: Some(Background::Color(active.text_color)),
            border_width: hovered.border_width,
            border_color: hovered.border_color,
            text_color: match active.background{
                Some(Background::Color(some))=>some,
                None=>panic!("Error<{}:{}>：不明なエラーが発生しました.\n",file!(),line!()),
            },
            ..active
        }
    }
}

pub struct Container;
impl container::StyleSheet for Container{
    fn style(&self)->container::Style{
        container::Style{
            text_color: Some(Color::from_rgb8(TEXT_R,TEXT_G,TEXT_B)),
            background: Some(Background::Color(Color::from_rgb8(CONTAINER_R,CONTAINER_G,CONTAINER_B))),
            border_radius: 10.0,
            ..container::Style::default()
        }
    }
}
```

</div></details>

<details><summary>src/even_scale.rs</summary><div>

```Rust:src/even_scale
use utau_rs::*;

#[allow(non_camel_case_types)]
#[derive(Clone,Copy,Debug,Eq,PartialEq)]
pub enum Scale{
    C,C_sharp,Cm,Cm_sharp,
    D,D_sharp,Dm,Dm_sharp,
    E,E_sharp,Em,Em_sharp,
    F,F_sharp,Fm,Fm_sharp,
    G,G_sharp,Gm,Gm_sharp,
    A,A_sharp,Am,Am_sharp,
    B,B_sharp,Bm,Bm_sharp,
}

impl Scale{
    pub const ALL: [Scale;28]=[
        Scale::C,Scale::C_sharp,Scale::Cm,Scale::Cm_sharp,
        Scale::D,Scale::D_sharp,Scale::Dm,Scale::Dm_sharp,
        Scale::E,Scale::E_sharp,Scale::Em,Scale::Em_sharp,
        Scale::F,Scale::F_sharp,Scale::Fm,Scale::Fm_sharp,
        Scale::G,Scale::G_sharp,Scale::Gm,Scale::Gm_sharp,
        Scale::A,Scale::A_sharp,Scale::Am,Scale::Am_sharp,
        Scale::B,Scale::B_sharp,Scale::Bm,Scale::Bm_sharp,
    ];
}

impl Default for Scale{
    fn default()->Self{
        Scale::C
    }
}

impl std::fmt::Display for Scale{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>)->std::fmt::Result{
        write!(f,"{}",match self{
            Scale::C=>"C",Scale::C_sharp=>"C#",Scale::Cm=>"Cm",Scale::Cm_sharp=>"C#m",
            Scale::D=>"D",Scale::D_sharp=>"D#",Scale::Dm=>"Dm",Scale::Dm_sharp=>"D#m",
            Scale::E=>"E",Scale::E_sharp=>"E#",Scale::Em=>"Em",Scale::Em_sharp=>"E#m",
            Scale::F=>"F",Scale::F_sharp=>"F#",Scale::Fm=>"Fm",Scale::Fm_sharp=>"F#m",
            Scale::G=>"G",Scale::G_sharp=>"G#",Scale::Gm=>"Gm",Scale::Gm_sharp=>"G#m",
            Scale::A=>"A",Scale::A_sharp=>"A#",Scale::Am=>"Am",Scale::Am_sharp=>"A#m",
            Scale::B=>"B",Scale::B_sharp=>"B#",Scale::Bm=>"Bm",Scale::Bm_sharp=>"B#m",
        })
    }
}

pub struct KeyTone([u32;7]);

const C: u32=24;
const D: u32=26;
const E: u32=28;
const F: u32=29;
const G: u32=31;
const A: u32=33;
const B: u32=35;

impl KeyTone{
    pub fn new(scale: &Scale)->Result<KeyTone,&'static str>{
        Ok(match scale{
            Scale::C      |Scale::Am      =>KeyTone([C,D,E,F,G,A,B]),
            Scale::C_sharp|Scale::Am_sharp=>KeyTone([C+1,D+1,E+1,F+1,G+1,A+1,B+1]),
            Scale::D      |Scale::Bm      =>KeyTone([C+1,D,E,F+1,G,A,B]),
            Scale::D_sharp|Scale::Cm      =>KeyTone([C,D,E-1,F,G,A-1,B-1]),
            Scale::E      |Scale::Cm_sharp=>KeyTone([C+1,D+1,E,F+1,G+1,A,B]),
            Scale::F      |Scale::Dm      =>KeyTone([C,D,E,F,G,A,B-1]),
            Scale::F_sharp|Scale::Dm_sharp=>KeyTone([C+1,D+1,E+1,F+1,G+1,A+1,B]),
            Scale::G      |Scale::Em      =>KeyTone([C,D,E,F+1,G,A,B]),
            Scale::G_sharp|Scale::Fm      =>KeyTone([C,D-1,E-1,F,G,A-1,B-1]),
            Scale::A      |Scale::Fm_sharp=>KeyTone([C+1,D,E,F+1,G+1,A,B]),
            Scale::A_sharp|Scale::Gm      =>KeyTone([C,D,E-1,F,G,A,B-1]),
            Scale::B      |Scale::Gm_sharp=>KeyTone([C+1,D+1,E,F+1,G+1,A+1,B]),
            _=>return Err("不明なエラーが発生しました."),
        })
    }

    fn unwrap(&self)->[u32;7]{
        match self{
            &KeyTone(some)=>some,
        }
    }
}

pub fn even_scale(uta_sections: &mut UtaSections,scale: Scale)->Result<(),&'static str>{
    let tones=KeyTone::new(&scale).unwrap();

    for section in uta_sections.sections.iter_mut(){
        if tones.unwrap().iter().all(|&x|(x%section.note_num)!=0){
            let mut near=section.note_num as i32-tones.unwrap()[0] as i32;
            for tone in tones.unwrap(){
                let tone=match section.note_num{
                    24..=35=>tone+12*0,
                    36..=47=>tone+12*1,
                    48..=59=>tone+12*2,
                    60..=71=>tone+12*3,
                    72..=83=>tone+12*4,
                    84..=95=>tone+12*5,
                    96..=107=>tone+12*6,
                    _=>return Err("不明なエラーが発生しました."),
                };
                if (section.note_num as i32-tone as i32).abs()<near{
                    near=(section.note_num as i32-tone as i32).abs();
                }
            }
            section.note_num=(section.note_num as i32+near) as u32;
        }
    };

    Ok(())
}
```

</div></details>

疲れました(本音
気をとりなしてコードの説明をしていきます。
今回は前回と違い、icedの説明も交えながらいきます。

###解説

<details><summary>src/main.rs</summary><div>

```Rust:src/main.rs
#![windows_subsystem="windows"]
```

DOS窓を表示しないように設定します。
ちなみにDOS窓というのは、いわゆるコマンドプロンプトみたいなウィンドウのことを指します。
これをしないと、プラグインを実行したときに~~空黒~~空白のDOS窓とGUIの二つのウィンドウが表示されることになります。

***

```Rust:src/main.rs
let window_setting=iced::window::Settings{/*省略*/}
```

ウィンドウの設定情報を変数に束縛します。
今回ウィンドウを伸縮不可のタイルライクなものにしたかったので、`size`と`resizable`と`decorations`を設定していきます。

***

```Rust:src/main.rs
let settings=Settings{/*省略*/}
```

実行に必要な設定情報を変数に束縛します。
今回はicedで日本語表示を可能にするためにデフォルトで使用するフォントを指定し、先程指定したウィンドウについての設定を追加し、あとの設定は`..Settings::default()`で片づけます。
`..○○::default()`ってめちゃくちゃ便利ですね。びっくりしました。

***

```Rust:src/main.rs
if let Err(err)=app::State::run(settings){/*省略*/}
```

先程の設定情報をもとにプラグインを実行します。
icedを用いたGUIアプリケーションでは、`main()`は基本的にこの実行のみを請け負います。
後述する`should_exit()`次第では`run()`の後に他の処理を実装することも可能ですので、ケースバイケースでいきましょう。

</div></details>

<details><summary>src/app.rs</summary><div>

```Rust:src/app.rs
#[derive(Default)]
pub struct State{/*省略*/}
```

各ウィジェットの状態を格納する変数や実行に関連する変数を、ひとつのオブジェクトとして定義します。
`#[derive(Default)]`で`Default`トレイトを実装しておくと便利です。
しかし型の中には手動で`Default`トレイトを実装しなければならないものもあるので注意しましょう。

***

```Rust:src/app.rs
#[derive(Clone,Debug)]
pub enum Message{/*省略*/}
```

ボタンを押したときやリストを選択したときなどに送受信される`Message`列挙体を定義します。
`Clone`トレイトや`Debug`トレイトは後の関数で要求されるため、`derive`で実装する必要があります。

***

```Rust:src/app.rs
impl Application for State{/*省略*/}
```

`Application`トレイトを`State`構造体に実装します。
この実装が完了すると`State::run()`を使用できるようになります。
ちなみにこの`Application`トレイトに似たものに`Sandbox`トレイトがありますが、これは`Application`トレイトの機能制限版になります。
`Application`トレイトよりも軽量な半面、`should_exit()`のような関数は実装できません。
はじめは`Sandbox`トレイトで実装を進め、必要があれば`Application`トレイトに移行しましょう。
IDEにもよりますが、移行は関数の引数と返り値を若干変えるだけなので2、3分程度で完了します。

***

```Rust:src/app.rs
fn new(_flags: ())->(Self,Command<Self::Message>){/*省略*/}
```

`State`の初期値を定義します。
ここで返り値は`(Self,Command<Self::Message>)`というタプルになりますが、この`Command<Self::Message>`は非同期処理用のものです。
今回非同期処理を実装する必要はないので、`Command::none()`を返します。
ちなみに他の非同期処理用の関数には`subscription()`があります。
`Command`と`subscription()`の詳しい説明については以下の記事が参考になりました。

https://sukawasatoru.com/docs/2021-02-13-iced

***

```Rust:src/app.rs
fn update(&mut self,message: Self::Message,_clipboard: &mut Clipboard)
    ->Command<Self::Message>{/*省略*/}
```

後述の`view()`等で何らかの`Message`が送られたときに任意の処理を実行します。
基本的には

```Rust:
match message{
    Message::ScaleSelect(scale)=>/*省略*/,
    Message::Run=>/*省略*/,
}
```

上記のように、引数の`massage`に対してパターンマッチングを行い、各`Message`についての処理を実装します。
今回は`ScaleSelect(scale)`の場合には`self.selected_scale=scale`をし、`Run`の場合には`even_scale()`と`.write()`を行うようにしました。

***

```Rust:src/app.rs
fn view(&mut self)->Element<Message>{/*省略*/}
```

ウィンドウにウィジェットやテキストをどのように配置するのかを定義します。

自分なりのicedでウィジェットをいい感じにコーディングするためのコツですが、まずウィジェット毎に変数を用意するといいと思います。

この時点で`.style()`や`.size()`のようなメソッドは付けておくと、次の`Column::new()`で`.push()`するだけでよくなります。
また`Column::new()`では`.spacing()`などのメソッドでウィジェット間の縦方向のスペースを決定することができます。
ウィジェットを横に並べたい場合には

```Rust:
.push(
    Row::new()
        .push(some_widget)
)
```

上記を`Column::new()`のメソッドチェーンに付け加えます。

一通り`Column::new()`にメソッドを付け加えたら、それを束縛する変数を引数として`Container::new(some_contents)`をします。
これは`view()`の返り値となるので、`;`は付けません。
また`Container::new()`のメソッドチェーンに

```Rust:
Container::new(contents)
    .width(Length::Fill)
    .height(Length::Fill)
    .center_x()
    .center_y()
    .style(Container)
    .into()
```

上記を加えておくと、ウィジェットが画面中央に配置されて見栄えがよくなります。
`.style()`は後述の`container::StyleSheet`トレイトを実装しているならば付けておきましょう。
メソッドチェーンの最後に`.into()`を付け加えるのも忘れずに。

***

```Rust:src/app.rs
fn should_exit(&self)->bool{
    self.exit
}
```

`self.exit`の値によって、`run()`を終了するべきかそうでないかを判断します。
`update()`等で`self.exit`に`true`を代入することで`run()`を終えつつ`main()`は続かせるのが主な使い道だと思います。

</div></details>

<details><summary>src/style.rs</summary><div>

```Rust:src/style.rs
impl pick_list::StyleSheet for PickList{/*省略*/}
```

`pick_list`のスタイルを設定します。
アクティブ時の場合やホバー時の場合など、ウィジェットの状態別にスタイルを設定できます。
icedのCSSのようなものだと思っていただいて多分間違いないでしょう。
またここで`StyleSheet`を実装した構造体を先述の`.style()`の引数とすることで、そのウィジェットにスタイルを適用することができます。
`button`、`container`についても同様の説明ができるので省略します。

</div></details>

#完成！！！
長い戦いでした。
GUI作成自体が初めてであるとはいえ、結構完成までに遠回りをしました。
最後に完成品のスクショを貼っておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2470169/92346322-2373-983f-78d6-26f809dee18d.png)

本当は五線譜のところで選択したスケールの構成音を表示したりとかもしたかったのですが、流石にそこまでする気力は残っていませんでした。
これはまた次の機会のアイディアとして残しておこうと思います。

#おわりに
API作成編からGUI作成編までの三部構成だった記事もようやっと終わりです。
初めての記事執筆でしたが、自分の理解していることの再確認をするにはもってこいの場でした。
この記事でRustでUTAUプラグインを作成する方が一人でも増えたり、icedの実装で困っている方が少しでも解決へと繋がってくれたら、本当に嬉しい限りです。

また何かあればQiitaに記事を投稿するつもりです。
その時はまたよしなに(？)していただければと思います。

またね。