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 {
"agpu App"
}
fn subscriptions(&self) -> Vec<Subscription<Self::Msg>> {
Vec::new()
}
fn current_route(&self) -> &str {
"/"
}
}
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,
pub backend: crate::types::BackendPreference,
pub msaa_samples: u32,
}
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,
backend: crate::types::BackendPreference::default(),
msaa_samples: 4,
}
}
}
pub enum Subscription<Msg> {
Timer {
id: String,
interval: Duration,
msg: Box<dyn Fn() -> Msg + Send>,
},
Delay {
id: String,
duration: Duration,
msg: Msg,
},
}
impl<Msg: std::fmt::Debug> std::fmt::Debug for Subscription<Msg> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Timer { id, interval, .. } => f
.debug_struct("Timer")
.field("id", id)
.field("interval", interval)
.finish(),
Self::Delay { id, duration, msg } => f
.debug_struct("Delay")
.field("id", id)
.field("duration", duration)
.field("msg", msg)
.finish(),
}
}
}
#[derive(Debug, Clone)]
pub struct Router {
current: String,
history: Vec<String>,
}
impl Router {
pub fn new(initial: impl Into<String>) -> Self {
let initial = initial.into();
Self {
current: initial.clone(),
history: vec![initial],
}
}
pub fn navigate(&mut self, route: impl Into<String>) {
let route = route.into();
self.history.push(self.current.clone());
self.current = route;
}
pub fn back(&mut self) -> bool {
if let Some(prev) = self.history.pop() {
self.current = prev;
true
} else {
false
}
}
pub fn current(&self) -> &str {
&self.current
}
pub fn history_len(&self) -> usize {
self.history.len()
}
pub fn matches(&self, pattern: &str) -> Option<Vec<(String, String)>> {
let route_parts: Vec<&str> = self.current.split('/').collect();
let pattern_parts: Vec<&str> = pattern.split('/').collect();
if route_parts.len() != pattern_parts.len() {
return None;
}
let mut params = Vec::new();
for (r, p) in route_parts.iter().zip(pattern_parts.iter()) {
if let Some(name) = p.strip_prefix(':') {
params.push((name.to_string(), r.to_string()));
} else if r != p {
return None;
}
}
Some(params)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancellation_token_not_cancelled_initially() {
let token = CancellationToken::new();
assert!(!token.is_cancelled());
}
#[test]
fn cancellation_token_cancel() {
let token = CancellationToken::new();
token.cancel();
assert!(token.is_cancelled());
}
#[test]
fn cancellation_token_clone_shares_state() {
let token = CancellationToken::new();
let clone = token.clone();
token.cancel();
assert!(clone.is_cancelled());
}
#[test]
fn cancellation_token_default() {
let token = CancellationToken::default();
assert!(!token.is_cancelled());
}
#[test]
fn program_options_defaults() {
let opts = ProgramOptions::default();
assert_eq!(opts.width, 800.0);
assert_eq!(opts.height, 600.0);
assert!(!opts.fullscreen);
assert!(opts.resizable);
assert!(opts.vsync);
assert!(!opts.transparent);
assert!(opts.tick_rate.is_some());
}
#[test]
fn command_debug_variants() {
let none: Command<String> = Command::None;
assert_eq!(format!("{:?}", none), "None");
let quit: Command<String> = Command::Quit;
assert_eq!(format!("{:?}", quit), "Quit");
let msg: Command<String> = Command::Message("hello".into());
assert!(format!("{:?}", msg).contains("hello"));
let export: Command<String> = Command::ExportOntology;
assert_eq!(format!("{:?}", export), "ExportOntology");
}
#[test]
fn frame_take_nodes() {
let mut hit_map = crate::event::HitMap::new();
let mut painter = crate::paint::NullPainter;
let mut frame = Frame::new(
Rect::new(0.0, 0.0, 800.0, 600.0),
&mut hit_map,
&mut painter,
);
assert!(frame.take_nodes().is_empty());
frame.register_widget(crate::ontology::UiNode::new(
"Button",
crate::ontology::SemanticRole::Action,
));
let nodes = frame.take_nodes();
assert_eq!(nodes.len(), 1);
assert!(frame.take_nodes().is_empty()); }
#[test]
fn frame_register_hitbox() {
let mut hit_map = crate::event::HitMap::new();
let mut painter = crate::paint::NullPainter;
let bounds = Rect::new(10.0, 10.0, 50.0, 50.0);
{
let mut frame = Frame::new(
Rect::new(0.0, 0.0, 800.0, 600.0),
&mut hit_map,
&mut painter,
);
frame.register_hitbox("btn-1", bounds, 0);
}
assert_eq!(
hit_map.hit_test(crate::core::Position::new(30.0, 30.0)),
Some("btn-1")
);
}
#[test]
fn router_basic_navigation() {
let mut router = Router::new("/");
assert_eq!(router.current(), "/");
router.navigate("/about");
assert_eq!(router.current(), "/about");
assert!(router.back());
assert_eq!(router.current(), "/");
}
#[test]
fn router_pattern_matching() {
let router = Router::new("/users/42");
let params = router.matches("/users/:id").unwrap();
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "id");
assert_eq!(params[0].1, "42");
assert!(router.matches("/posts/:id").is_none());
}
#[test]
fn router_history_depth() {
let mut router = Router::new("/");
assert_eq!(router.history_len(), 1);
router.navigate("/a");
assert_eq!(router.history_len(), 2);
router.navigate("/b");
assert_eq!(router.history_len(), 3);
router.back();
assert_eq!(router.history_len(), 2);
}
#[test]
fn program_options_msaa_default() {
let opts = ProgramOptions::default();
assert_eq!(opts.msaa_samples, 4);
}
#[test]
fn subscription_timer_debug() {
let sub: Subscription<String> = Subscription::Timer {
id: "test".into(),
interval: Duration::from_secs(1),
msg: Box::new(|| "tick".into()),
};
let dbg = format!("{:?}", sub);
assert!(dbg.contains("Timer"));
assert!(dbg.contains("test"));
}
}