Last active
February 18, 2023 10:41
-
-
Save tesfabpel/9a00807cfe8042cd01e623520bcb875f to your computer and use it in GitHub Desktop.
Rust GUI framework design
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
mod core { | |
use generational_arena::{Arena, Index}; | |
use qcell::LCell; | |
use qcell::LCellOwner; | |
use queues::Queue; | |
use std::marker::PhantomData; | |
use std::rc::Rc; | |
#[derive(PartialEq)] | |
pub enum KeyCode { | |
N, | |
} | |
pub trait Bundle { | |
fn name(&self) -> &Name; | |
} | |
pub struct EmptyBundle { | |
pub name: Name, | |
} | |
impl Bundle for EmptyBundle { | |
fn name(&self) -> &Name { | |
&self.name | |
} | |
} | |
#[derive(Clone)] | |
pub struct Handle<'a>(Rc<LCell<'a, Index>>); | |
#[derive(Clone)] | |
pub struct Name(pub Option<String>); | |
impl Default for Name { | |
fn default() -> Self { | |
Self(None) | |
} | |
} | |
pub struct Context { | |
root: Index, | |
entities: Arena<(Name, Box<dyn Bundle>)>, | |
} | |
impl Context { | |
pub fn new() -> Self { | |
let mut arena: Arena<(Name, Box<(dyn Bundle + 'static)>)> = Arena::new(); | |
let root = arena.insert((Name(None), Box::new(EmptyBundle { name: Name(None) }))); | |
Self { | |
root, | |
entities: arena, | |
} | |
} | |
pub fn create_entity(&mut self, b: impl Bundle + 'static) -> Index { | |
let name = b.name().clone(); | |
self.entities.insert((name, Box::new(b))) | |
} | |
pub fn remove_entity(&mut self, i: Index) -> bool { | |
if i == self.root { | |
return false; | |
} | |
self.entities.remove(i).is_some() | |
} | |
pub fn effectful<F>(&mut self, fun: F) | |
where | |
F: for<'scope> FnOnce(DomCommands<'_, 'scope>), | |
{ | |
LCellOwner::scope(|owner| { | |
let cmd = DomCommands { | |
ctx: self, | |
owner, | |
queue: Queue::new(), | |
handles: Vec::new(), | |
}; | |
fun(cmd); | |
// process `cmd` | |
}); | |
} | |
} | |
enum HandleCommand { | |
Created, | |
Removed, | |
} | |
pub struct DomCommands<'a, 'scope> { | |
ctx: &'a mut Context, | |
owner: LCellOwner<'scope>, | |
queue: Queue<DomCommand>, | |
handles: Vec<(Handle<'scope>, HandleCommand)>, | |
} | |
impl<'a, 'scope> DomCommands<'a, 'scope> { | |
pub fn root_entity(&self) -> Handle<'scope> { | |
let root = self.ctx.root; | |
Handle(Rc::new(self.owner.cell(root))) | |
} | |
pub fn find_entity_by_name(&self, name: &str) -> Handle<'scope> { | |
let h = self.try_find_entity_by_name(name); | |
match h { | |
None => panic!(), | |
Some(h) => h, | |
} | |
} | |
pub fn try_find_entity_by_name(&self, name: &str) -> Option<Handle<'scope>> { | |
for ent in self.ctx.entities.iter() { | |
if let Name(Some(ref ent_name)) = ent.1 .0 { | |
let ent_name = ent_name.as_str(); | |
if name == ent_name { | |
return Some(Handle(Rc::new(self.owner.cell(ent.0)))); | |
} | |
} | |
} | |
None | |
} | |
pub fn spawn(&mut self, b: impl Bundle + 'static) -> Option<Handle<'scope>> { | |
if let Name(Some(ref name)) = b.name() { | |
let s = name.as_str(); | |
let h = self.try_find_entity_by_name(s); | |
if h.is_some() { | |
return None; | |
} | |
} | |
let id = self.ctx.create_entity(b); | |
let h = Handle(Rc::new(self.owner.cell(id))); | |
self.handles.push((h.clone(), HandleCommand::Created)); | |
Some(h) | |
} | |
pub fn insert_entity_after(&mut self, entity: &Handle, target: &Handle) {} | |
pub fn append_entity(&mut self, entity: &Handle, container: &Handle) {} | |
} | |
#[derive(Clone)] | |
pub enum DomCommand { | |
Add(), | |
Remove(), | |
} | |
pub struct Color; | |
impl Color { | |
pub fn from_hex(hex: u32) -> Color { | |
Color | |
} | |
} | |
impl Default for Color { | |
fn default() -> Self { | |
Color | |
} | |
} | |
} | |
mod ui { | |
use crate::core::Name; | |
#[derive(Default)] | |
pub struct TextBundle { | |
pub name: Name, | |
pub text: String, | |
pub color: crate::core::Color, | |
} | |
impl crate::core::Bundle for TextBundle { | |
fn name(&self) -> &Name { | |
&self.name | |
} | |
} | |
pub struct KeyPress { | |
pub key_code: crate::core::KeyCode, | |
} | |
pub enum Message { | |
KeyPress(KeyPress), | |
} | |
pub trait UiComponent { | |
fn on_message(&mut self, ctx: &mut crate::core::Context, msg: &Message); | |
} | |
} | |
use std::marker::PhantomData; | |
use crate::core::Context; | |
use crate::core::DomCommands; | |
use crate::core::Handle; | |
use crate::core::KeyCode; | |
use crate::core::{Color, Name}; | |
use crate::ui::KeyPress; | |
use crate::ui::Message; | |
use crate::ui::TextBundle; | |
use crate::ui::UiComponent; | |
struct MainForm; | |
const ENT_NAME_LBL_HELLO: &str = "lbl_hello"; | |
const ENT_NAME_LBL_WORLD: &str = "lbl_world"; | |
impl UiComponent for MainForm { | |
fn on_message(&mut self, ctx: &mut Context, msg: &Message) { | |
match msg { | |
Message::KeyPress(ref kp) => self.on_key_press(ctx, kp), | |
} | |
} | |
} | |
impl MainForm { | |
fn on_key_press(&mut self, ctx: &mut Context, ev: &KeyPress) { | |
if ev.key_code != KeyCode::N { | |
return; | |
} | |
//let mut aaa = Handle { | |
// _pd: PhantomData | |
//}; | |
// ctx.effectful takes ctx as `&mut self` and executes the FnOnce | |
// with a purposely created `DomCommands` | |
ctx.effectful(|mut dom: DomCommands| { | |
let root = dom.root_entity(); | |
//dom.try_find_entity_by_name(ENT_NAME_LBL_HELLO); | |
let lbl_hello = dom.find_entity_by_name(ENT_NAME_LBL_HELLO); | |
// Handles are temporary, tied to the lifetime of `dom` | |
// which gets created for every `ctx.effectful` call | |
let lbl_world = dom | |
.spawn(TextBundle { | |
name: Name(Some(ENT_NAME_LBL_WORLD.to_string())), | |
text: ", World!".to_string(), | |
color: Color::from_hex(0xFF0000), | |
}) | |
.unwrap(); | |
dom.insert_entity_after(&lbl_world, &lbl_hello); | |
for _ in 0..10 { | |
let handle = dom | |
.spawn(TextBundle { | |
name: Name(None), | |
text: "Hello, World!".to_string(), | |
..Default::default() | |
}) | |
.unwrap(); | |
dom.append_entity(&handle, &root); | |
} | |
//aaa = lbl_hello; | |
// only at the end of `effectful`, the commands are | |
// really applied to the "DOM". | |
// until then, entities exist only with their | |
// temporary handle. | |
}); | |
ctx.effectful(|dom: DomCommands| { | |
//... | |
}); | |
} | |
} | |
fn main() {} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment