e-tipsmemo

ごった煮

TD4 Emulator GUI in Rust iced

TD4のエミュレータを作った
e-tipsmemo.hatenablog.com

が、それだけではつまらないので、GUIを被せた。
RustのGUIライブラリicedを使用した。
シンプルそうだったからというのが選んだ理由。
GitHub - katakanan/td4-gui

Ticker

Icedは周期を指定してイベントを起こす機能が備わっているのでそれを使う。
サンプル StopWatch
この周期でエミュレータのfetch-decode-exec実行を回している。

    fn subscription(&self) -> Subscription<Message> {
        match self.state {
            State::Idle => Subscription::none(),
            State::Active => {
                time::every(std::time::Duration::from_millis(self.period)).map(|_| Message::Tick)
            }
        }
    }

Run-Stop-Step-Resetボタン

CPUの作り方の実機でもResetボタンとStepボタンがあった気がする。

        let run = Button::new(&mut self.run, Text::new("Run"))
            .padding(10)
            .on_press(Message::Run)
            .style(self.theme);

        let step = Button::new(&mut self.step, Text::new("Step"))
            .padding(10)
            .on_press(Message::Step)
            .style(self.theme);

        let stop = Button::new(&mut self.stop, Text::new("Stop"))
            .padding(10)
            .on_press(Message::Stop)
            .style(self.theme);

        let reset = Button::new(&mut self.reset, Text::new("Reset"))
            .width(Length::from(300))
            .padding(10)
            .on_press(Message::Reset)
            .style(self.theme);

Slider

1Hz動作だと遅いので、
Tickerの周期を変更できるようになっている。
10ミリ秒だとTickerの割り込みが多くなって、ボタン操作に影響が出ている気がする。
(UIと処理を別スレッドにしたりする必要があるが面倒なのでしない)

        let slider = Slider::new(
            &mut self.slider,
            100.0..=1000.0,
            self.period as f64,
            Message::SliderChanged,
        );

ビット操作

一番大変?だった?
まず最初はチェックボックスでこれを行おうとしたが、チェックボックスはメッセージにチェックされているかどうかと言うboolの値が1個しか送れないので、
チェックボックスに同じメッセージを設定すると、どのアドレスの何ビット目なのかを判別できない。

checkboxの引数の型がF: 'static + Fn(bool) となっている。
icedのライブラリ内

    pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self
    where
        F: 'static + Fn(bool) -> Message,
    {
        Checkbox {
            is_checked,
            on_toggle: Box::new(f),
            label: label.into(),
            width: Length::Shrink,
            size: <Renderer as self::Renderer>::DEFAULT_SIZE,
            spacing: Renderer::DEFAULT_SPACING,
            text_size: None,
            font: Renderer::Font::default(),
            text_color: None,
            style: Renderer::Style::default(),
        }
    }

Buttonではon_eventのメッセージにはEnumで定義されたメッセージ with Tupleを与えられるので、
順にaddr:usize、bit:u8、現在の状態:boolと決めておく。

#[derive(Debug, Clone, Copy)]
pub enum Message {
    Reset,
    Tick,
    Run,
    Step,
    Stop,
    RomEdit(usize, u8, bool),
    InputEdit(u8, bool),
    SliderChanged(f64),
}

ボタンをnewするときに同時にボタンの見た目を指定できるので、exampleにあったLightテーマとDarkテーマを1と0で切り替えて使う。
それをbit数だけ並べる。
method chainでbuttonのarrayを作ってRowにpushする。
(Rowはコンテンツを左から横にレイアウトしていく)

impl InputHalfByte {
    pub fn crate_layout(&mut self, value: &u8) -> Row<Message> {
        self.buttons(value)
            .into_iter()
            .fold(Row::new().spacing(1), |row, button| row.push(button))
    }

    fn buttons(&mut self, value: &u8) -> Vec<Button<Message>> {
        self.bit_state
            .iter_mut()
            .enumerate()
            .map(|(i, state)| {
                let bit = value & (0x01 << i) != 0;
                let btn = Button::new(state, bit2text(bit))
                    .on_press(Message::InputEdit(i as u8, bit))
                    .style(bit2style(bit));
                btn
            })
            .rev()
            .collect::<_>()
    }
}

romも8bitにしたものをColumnで並べる。

lifetimeやら所有権やらでここが一番よくわからなかった。
registerごとにgetとか実装したほうが良かったのか?
(エラーが出ないように修正しただけなので結局よくわかっていない)

LED表示

レジスタの情報の表示。

pub fn led4bit(halfbyte: &u8) -> Row<Message> {
    (0..4)
        .into_iter()
        .rev()
        .fold(Row::new().spacing(1), |row, i| {
            row.push(circle::Circle::new(
                10.0,
                bit2color(&((halfbyte & (0x01 << i)) != 0)),
            ))
        })
}

icedのexampleにあったcircleを改造したものを並べている。

完成したもの

いろいろとこだわれるところはあると思うが、これでおしまい。
バグに気づいたら直すかもしれない。
f:id:katakanan:20210531214050p:plain

感想

今更だけどRegとProgramRomが同じ階層にあるのはおかしかった。(動作には問題ないが)

エミュレータ本体を作るよりもGUIをいじるところが一番手間がかかってしまった。
Styleの色合いが微妙な感じだがそこを拘るのは更に大変そうなので気が向いたら
別のGUIアプリケーションを作るときには、お洒落にしたい。
icedはCross platformなのでwasmにすればブラウザでも動くと思う。