use std::time::Duration;
use crate::core::Rect;
use crate::ontology::OntologyRegistry;
#[derive(Clone)]
pub struct CancellationToken {
cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl CancellationToken {
pub fn new() -> Self {
Self {
cancelled: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn cancel(&self) {
self.cancelled
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
impl Default for CancellationToken {
fn default() -> Self {
Self::new()
}
}
pub enum Command<Msg> {
None,
Quit,
Batch(Vec<Command<Msg>>),
Message(Msg),
SetTickRate(Duration),
ExportOntology,
AgentAction {
agent_id: String,
action: String,
params: serde_json::Value,
},
Task(Box<dyn FnOnce() -> Msg + Send>),
TaskWithTimeout {
task: Box<dyn FnOnce() -> Msg + Send>,
timeout: Duration,
on_timeout: Msg,
},
TaskCancellable {
task: Box<dyn FnOnce(CancellationToken) -> Msg + Send>,
token: CancellationToken,
},
}
impl<Msg: std::fmt::Debug> std::fmt::Debug for Command<Msg> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "None"),
Self::Quit => write!(f, "Quit"),
Self::Batch(cmds) => f.debug_tuple("Batch").field(cmds).finish(),
Self::Message(msg) => f.debug_tuple("Message").field(msg).finish(),
Self::SetTickRate(d) => f.debug_tuple("SetTickRate").field(d).finish(),
Self::ExportOntology => write!(f, "ExportOntology"),
Self::AgentAction {
agent_id,
action,
params,
} => f
.debug_struct("AgentAction")
.field("agent_id", agent_id)
.field("action", action)
.field("params", params)
.finish(),
Self::Task(_) => write!(f, "Task(<fn>)"),
Self::TaskWithTimeout { timeout, .. } => {
write!(f, "TaskWithTimeout({}ms)", timeout.as_millis())
}
Self::TaskCancellable { .. } => write!(f, "TaskCancellable(<fn>)"),
}
}
}
pub trait Model: Sized {
type Msg: Send + 'static;
fn update(&mut self, msg: Self::Msg) -> Command<Self::Msg>;
fn view(&self, frame: &mut Frame<'_>);
fn handle_event(&self, event: crate::event::Event) -> Option<Self::Msg>;
fn init(&self) -> Command<Self::Msg> {
Command::None
}
fn register_ontology(&self, _registry: &mut OntologyRegistry) {}
fn title(&self) -> &str {
"Dewey App"
}
}
pub struct Frame<'a> {
pub area: Rect,
pub hit_map: &'a mut crate::event::HitMap,
ui_nodes: Vec<crate::ontology::UiNode>,
painter: &'a mut dyn crate::paint::Painter,
}
impl<'a> Frame<'a> {
pub fn new(
area: Rect,
hit_map: &'a mut crate::event::HitMap,
painter: &'a mut dyn crate::paint::Painter,
) -> Self {
Self {
area,
hit_map,
ui_nodes: Vec::new(),
painter,
}
}
pub fn painter(&mut self) -> &mut dyn crate::paint::Painter {
self.painter
}
pub fn register_widget(&mut self, node: crate::ontology::UiNode) {
self.ui_nodes.push(node);
}
pub fn register_hitbox(&mut self, agent_id: impl Into<String>, bounds: Rect, z_order: u32) {
self.hit_map.register(agent_id, bounds, z_order);
}
pub fn take_nodes(&mut self) -> Vec<crate::ontology::UiNode> {
std::mem::take(&mut self.ui_nodes)
}
}
pub struct ProgramOptions {
pub tick_rate: Option<Duration>,
pub width: f32,
pub height: f32,
pub fullscreen: bool,
pub resizable: bool,
pub vsync: bool,
pub transparent: bool,
}
impl Default for ProgramOptions {
fn default() -> Self {
Self {
tick_rate: Some(Duration::from_millis(16)), width: 800.0,
height: 600.0,
fullscreen: false,
resizable: true,
vsync: true,
transparent: false,
}
}
}
#[cfg(feature = "egui-backend")]
pub struct Program<M: Model> {
model: M,
options: ProgramOptions,
}
#[cfg(feature = "egui-backend")]
impl<M: Model + 'static> Program<M> {
pub fn new(model: M) -> Self {
Self {
model,
options: ProgramOptions::default(),
}
}
pub fn with_options(mut self, options: ProgramOptions) -> Self {
self.options = options;
self
}
pub fn run(self) -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([self.options.width, self.options.height])
.with_resizable(self.options.resizable)
.with_transparent(self.options.transparent),
vsync: self.options.vsync,
..Default::default()
};
let title = self.model.title().to_string();
eframe::run_native(
&title,
options,
Box::new(move |_cc| Ok(Box::new(DeweyApp::new(self.model, self.options)))),
)
}
}
#[cfg(feature = "egui-backend")]
struct DeweyApp<M: Model> {
model: M,
hit_map: crate::event::HitMap,
ontology: OntologyRegistry,
options: ProgramOptions,
running: bool,
last_tick: std::time::Instant,
}
#[cfg(feature = "egui-backend")]
impl<M: Model> DeweyApp<M> {
fn new(model: M, options: ProgramOptions) -> Self {
let mut ontology = OntologyRegistry::new();
model.register_ontology(&mut ontology);
let init_cmd = model.init();
let mut app = Self {
model,
hit_map: crate::event::HitMap::new(),
ontology,
options,
running: true,
last_tick: std::time::Instant::now(),
};
app.process_command(init_cmd);
app
}
fn process_command(&mut self, cmd: Command<M::Msg>) {
match cmd {
Command::None => {}
Command::Quit => {
self.running = false;
}
Command::Batch(cmds) => {
for c in cmds {
self.process_command(c);
}
}
Command::Message(msg) => {
let cmd = self.model.update(msg);
self.process_command(cmd);
}
Command::SetTickRate(_duration) => {
}
Command::ExportOntology => {
self.model.register_ontology(&mut self.ontology);
}
Command::AgentAction {
agent_id,
action,
params,
} => {
log::debug!("AgentAction: {agent_id}.{action}({params})");
}
Command::Task(task) => {
let msg = task();
let cmd = self.model.update(msg);
self.process_command(cmd);
}
Command::TaskWithTimeout {
task,
timeout,
on_timeout,
} => {
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let result = task();
let _ = tx.send(result);
});
let msg = match rx.recv_timeout(timeout) {
Ok(result) => result,
Err(_) => on_timeout,
};
let cmd = self.model.update(msg);
self.process_command(cmd);
}
Command::TaskCancellable { task, token } => {
let msg = task(token);
let cmd = self.model.update(msg);
self.process_command(cmd);
}
}
}
}
#[cfg(feature = "egui-backend")]
impl<M: Model + 'static> eframe::App for DeweyApp<M> {
fn update(&mut self, ctx: &egui::Context, _eframe: &mut eframe::Frame) {
if !self.running {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
let events = convert_egui_events(ctx);
for event in events {
if let Some(msg) = self.model.handle_event(event) {
let cmd = self.model.update(msg);
self.process_command(cmd);
}
}
if let Some(tick_rate) = self.options.tick_rate {
if self.last_tick.elapsed() >= tick_rate {
self.last_tick = std::time::Instant::now();
if let Some(msg) = self.model.handle_event(crate::event::Event::Tick) {
let cmd = self.model.update(msg);
self.process_command(cmd);
}
}
}
self.hit_map.clear();
let available = ctx.available_rect();
let area = Rect::new(
available.min.x,
available.min.y,
available.width(),
available.height(),
);
egui::CentralPanel::default().show(ctx, |_ui| {
let mut egui_painter = crate::backend::egui_backend::EguiPainter::new(ctx);
let mut frame = Frame::new(area, &mut self.hit_map, &mut egui_painter);
self.model.view(&mut frame);
let nodes = frame.take_nodes();
if !nodes.is_empty() {
let root =
crate::ontology::UiNode::new("root", crate::ontology::SemanticRole::Container);
let mut root = root;
root.children = nodes;
self.ontology.set_tree(crate::ontology::UiTree::new(root));
}
});
if let Some(tick_rate) = self.options.tick_rate {
ctx.request_repaint_after(tick_rate);
}
}
}
#[cfg(feature = "egui-backend")]
fn convert_egui_events(ctx: &egui::Context) -> Vec<crate::event::Event> {
let mut events = Vec::new();
let input = ctx.input(|i| i.clone());
for event in &input.events {
match event {
egui::Event::Key {
key,
pressed,
modifiers,
..
} => {
if let Some(code) = convert_egui_key(*key) {
let mut mods = crate::event::KeyModifiers::empty();
if modifiers.shift {
mods |= crate::event::KeyModifiers::SHIFT;
}
if modifiers.ctrl || modifiers.command {
mods |= crate::event::KeyModifiers::CONTROL;
}
if modifiers.alt {
mods |= crate::event::KeyModifiers::ALT;
}
let kind = if *pressed {
crate::event::KeyEventKind::Press
} else {
crate::event::KeyEventKind::Release
};
events.push(crate::event::Event::Key(crate::event::KeyEvent {
code,
modifiers: mods,
kind,
}));
}
}
egui::Event::Text(text) => {
events.push(crate::event::Event::TextInput(text.clone()));
}
egui::Event::PointerButton {
pos,
button,
pressed,
modifiers,
} => {
let btn = match button {
egui::PointerButton::Primary => crate::event::MouseButton::Left,
egui::PointerButton::Secondary => crate::event::MouseButton::Right,
egui::PointerButton::Middle => crate::event::MouseButton::Middle,
_ => crate::event::MouseButton::Left,
};
let mut mods = crate::event::KeyModifiers::empty();
if modifiers.shift {
mods |= crate::event::KeyModifiers::SHIFT;
}
if modifiers.ctrl || modifiers.command {
mods |= crate::event::KeyModifiers::CONTROL;
}
if modifiers.alt {
mods |= crate::event::KeyModifiers::ALT;
}
let kind = if *pressed {
crate::event::MouseEventKind::Click(btn)
} else {
crate::event::MouseEventKind::Release(btn)
};
events.push(crate::event::Event::Mouse(crate::event::MouseEvent {
kind,
position: crate::core::Position::new(pos.x, pos.y),
modifiers: mods,
}));
}
egui::Event::PointerMoved(pos) => {
events.push(crate::event::Event::Mouse(crate::event::MouseEvent {
kind: crate::event::MouseEventKind::Move,
position: crate::core::Position::new(pos.x, pos.y),
modifiers: crate::event::KeyModifiers::empty(),
}));
}
egui::Event::MouseWheel { delta, .. } => {
events.push(crate::event::Event::Mouse(crate::event::MouseEvent {
kind: crate::event::MouseEventKind::Scroll {
delta_x: delta.x,
delta_y: delta.y,
},
position: crate::core::Position::ZERO,
modifiers: crate::event::KeyModifiers::empty(),
}));
}
_ => {}
}
}
if input.viewport().close_requested() {
events.push(crate::event::Event::CloseRequested);
}
events
}
#[cfg(feature = "egui-backend")]
fn convert_egui_key(key: egui::Key) -> Option<crate::event::KeyCode> {
use crate::event::KeyCode;
Some(match key {
egui::Key::ArrowDown => KeyCode::Down,
egui::Key::ArrowLeft => KeyCode::Left,
egui::Key::ArrowRight => KeyCode::Right,
egui::Key::ArrowUp => KeyCode::Up,
egui::Key::Escape => KeyCode::Esc,
egui::Key::Tab => KeyCode::Tab,
egui::Key::Backspace => KeyCode::Backspace,
egui::Key::Enter => KeyCode::Enter,
egui::Key::Space => KeyCode::Char(' '),
egui::Key::Insert => KeyCode::Insert,
egui::Key::Delete => KeyCode::Delete,
egui::Key::Home => KeyCode::Home,
egui::Key::End => KeyCode::End,
egui::Key::PageUp => KeyCode::PageUp,
egui::Key::PageDown => KeyCode::PageDown,
egui::Key::F1 => KeyCode::F(1),
egui::Key::F2 => KeyCode::F(2),
egui::Key::F3 => KeyCode::F(3),
egui::Key::F4 => KeyCode::F(4),
egui::Key::F5 => KeyCode::F(5),
egui::Key::F6 => KeyCode::F(6),
egui::Key::F7 => KeyCode::F(7),
egui::Key::F8 => KeyCode::F(8),
egui::Key::F9 => KeyCode::F(9),
egui::Key::F10 => KeyCode::F(10),
egui::Key::F11 => KeyCode::F(11),
egui::Key::F12 => KeyCode::F(12),
egui::Key::A => KeyCode::Char('a'),
egui::Key::B => KeyCode::Char('b'),
egui::Key::C => KeyCode::Char('c'),
egui::Key::D => KeyCode::Char('d'),
egui::Key::E => KeyCode::Char('e'),
egui::Key::F => KeyCode::Char('f'),
egui::Key::G => KeyCode::Char('g'),
egui::Key::H => KeyCode::Char('h'),
egui::Key::I => KeyCode::Char('i'),
egui::Key::J => KeyCode::Char('j'),
egui::Key::K => KeyCode::Char('k'),
egui::Key::L => KeyCode::Char('l'),
egui::Key::M => KeyCode::Char('m'),
egui::Key::N => KeyCode::Char('n'),
egui::Key::O => KeyCode::Char('o'),
egui::Key::P => KeyCode::Char('p'),
egui::Key::Q => KeyCode::Char('q'),
egui::Key::R => KeyCode::Char('r'),
egui::Key::S => KeyCode::Char('s'),
egui::Key::T => KeyCode::Char('t'),
egui::Key::U => KeyCode::Char('u'),
egui::Key::V => KeyCode::Char('v'),
egui::Key::W => KeyCode::Char('w'),
egui::Key::X => KeyCode::Char('x'),
egui::Key::Y => KeyCode::Char('y'),
egui::Key::Z => KeyCode::Char('z'),
egui::Key::Num0 => KeyCode::Char('0'),
egui::Key::Num1 => KeyCode::Char('1'),
egui::Key::Num2 => KeyCode::Char('2'),
egui::Key::Num3 => KeyCode::Char('3'),
egui::Key::Num4 => KeyCode::Char('4'),
egui::Key::Num5 => KeyCode::Char('5'),
egui::Key::Num6 => KeyCode::Char('6'),
egui::Key::Num7 => KeyCode::Char('7'),
egui::Key::Num8 => KeyCode::Char('8'),
egui::Key::Num9 => KeyCode::Char('9'),
_ => return None,
})
}