Created
June 6, 2021 01:28
-
-
Save kellpossible/ad62790ed12647331b4d0373b509509f to your computer and use it in GitHub Desktop.
Rust Gui Abstraction
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use serde::{Serialize, Deserialize}; | |
use std::collections::HashMap; | |
use core::marker::PhantomData; | |
#[derive(Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Hash, Debug)] | |
struct CallbackId(u32); | |
impl CallbackId { | |
pub fn none() -> Self { | |
CallbackId(0) | |
} | |
} | |
impl Default for CallbackId { | |
fn default() -> Self { | |
Self::none() | |
} | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct SelectionIndex(u32); | |
#[derive(Serialize, Deserialize, Debug)] | |
#[non_exhaustive] | |
enum OnChangeValue { | |
Text(String), | |
Checkbox(CheckboxState), | |
Selection(SelectionIndex), | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct OnChangeEvent { | |
value: OnChangeValue, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
#[non_exhaustive] | |
enum Event { | |
OnChange(OnChangeEvent), | |
OnClick, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct CallbackEvent { | |
callback: CallbackId, | |
event: Event, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
enum TextEditorType { | |
Password, | |
OneLine, | |
MultiLine, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct TextEditor { | |
on_change: CallbackId, | |
editor_type: TextEditorType, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct RadioEditor { | |
on_change: CallbackId, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
enum CheckboxState { | |
Checked, | |
Unchecked | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct CheckboxEditor { | |
onchange: CallbackId, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
enum FieldEditor { | |
Text(TextEditor), | |
Radio(RadioEditor), | |
} | |
impl From<TextEditor> for FieldEditor { | |
fn from(text_editor: TextEditor) -> Self { | |
Self::Text(text_editor) | |
} | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct FormField { | |
label: Option<String>, | |
editor: FieldEditor, | |
enabled: bool, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct Button { | |
title: String, | |
on_click: CallbackId, | |
enabled: bool, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
enum FormElement { | |
Field(FormField), | |
Button(Button), | |
Horizontal(Vec<FormElement>), | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct Form { | |
elements: Vec<FormElement>, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
enum PageContents { | |
Form(Form), | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct Page { | |
title: String, | |
contents: PageContents, | |
} | |
#[derive(Default)] | |
struct CallbackRegister<D: Default> { | |
prev_id: CallbackId, | |
callbacks: HashMap<CallbackId, Box<dyn Fn(Event, &mut D)>>, | |
data: PhantomData<D>, | |
} | |
impl<D: Default> CallbackRegister<D> { | |
pub fn new() -> Self { | |
Self::default() | |
} | |
fn next_id(&self) -> CallbackId { | |
CallbackId(self.prev_id.0 + 1) | |
} | |
pub fn call(&self, callback: CallbackId, event: Event, data: &mut D) { | |
self.callbacks.get(&callback).unwrap()(event, data) | |
} | |
fn assign_id(&mut self, callback: Box<dyn Fn(Event, &mut D)>) -> CallbackId { | |
let next_id = self.next_id(); | |
self.callbacks.insert(next_id, callback); | |
self.prev_id = next_id; | |
next_id | |
} | |
pub fn on_click(&mut self, callback: impl Fn(&mut D) + 'static) -> CallbackId { | |
let on_click_callback = Box::new(move |event: Event, data: &mut D| { | |
match event { | |
Event::OnClick => { | |
callback(data) | |
} | |
_ => eprintln!("Unexpected event"), | |
} | |
}); | |
self.assign_id(on_click_callback) | |
} | |
pub fn on_change(&mut self, callback: impl Fn(OnChangeEvent, &mut D) + 'static) -> CallbackId { | |
let on_change_callback = Box::new(move |event: Event, data: &mut D| { | |
match event { | |
Event::OnChange(on_change_event) => { | |
callback(on_change_event, data) | |
} | |
_ => eprintln!("Unexpected event"), | |
} | |
}); | |
self.assign_id(on_change_callback) | |
} | |
} | |
#[derive(Default, Debug)] | |
struct Data { | |
password: String | |
} | |
impl Data { | |
pub fn is_valid(&self) -> bool { | |
!self.password.is_empty() | |
} | |
} | |
fn login_page(data: &Data, callbacks: &mut CallbackRegister<Data>) -> Page { | |
let password_callback: CallbackId = callbacks.on_change(|event: OnChangeEvent, data: &mut Data| { | |
match event.value { | |
OnChangeValue::Text(text) => { | |
println!("The password changed to: {:?}", text); | |
data.password = text; | |
} | |
unexpected => { | |
eprintln!("Unexpected event value: {:?}", unexpected) | |
} | |
} | |
}); | |
let login_callback: CallbackId = callbacks.on_click(|_data: &mut Data| { | |
println!("The login button was clicked") | |
}); | |
Page { | |
title: "Login".into(), | |
contents: PageContents::Form(Form { | |
elements: vec![ | |
FormElement::Field(FormField { | |
label: Some("Password".into()), | |
editor: TextEditor { | |
editor_type: TextEditorType::Password, | |
on_change: password_callback, | |
}.into(), | |
enabled: true, | |
}), | |
FormElement::Button(Button { | |
title: "Login".into(), | |
on_click: login_callback, | |
enabled: data.is_valid(), | |
}) | |
] | |
}) | |
} | |
} | |
struct Gui { | |
data: Data, | |
callbacks: CallbackRegister<Data>, | |
} | |
impl Gui { | |
pub fn new() -> Self { | |
Self { | |
data: Default::default(), | |
callbacks: CallbackRegister::new(), | |
} | |
} | |
pub fn process_event(&mut self, callback_event: CallbackEvent) { | |
self.callbacks.call(callback_event.callback, callback_event.event, &mut self.data) | |
} | |
pub fn render(&mut self) { | |
println!("Render: {:#?}", login_page(&self.data, &mut self.callbacks)); | |
} | |
} | |
fn main() { | |
let mut gui = Gui::new(); | |
gui.render(); | |
gui.process_event(CallbackEvent { | |
callback: CallbackId(1), | |
event: Event::OnChange({ | |
OnChangeEvent { | |
value: OnChangeValue::Text("hello".into()) | |
} | |
}), | |
}); | |
gui.render(); | |
gui.process_event(CallbackEvent { | |
callback: CallbackId(2), | |
event: Event::OnClick, | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment