use crate::context::Context;
use std::cell::Cell;
use std::rc::Rc;
pub struct PluginCtx<'a, S, App = ()> {
pub state: &'a S,
pub app: Option<&'a App>,
pub render_count: u64,
}
impl<'a, S, App> PluginCtx<'a, S, App> {
pub fn app(&self) -> &'a App {
self.app.expect("plugin expected app state of type App")
}
}
pub trait Plugin {
type State: 'static;
fn name(&self) -> &'static str;
fn init(&self) -> Self::State
where
Self::State: Default,
{
Self::State::default()
}
fn on_before_render<A: 'static>(&self, _ctx: &PluginCtx<Self::State, A>) {}
fn on_render<A: 'static>(&self, _ctx: &PluginCtx<Self::State, A>) {}
fn on_shutdown<A: 'static>(&self, _ctx: &PluginCtx<Self::State, A>) {}
}
struct Registered<P: Plugin + 'static> {
plugin: P,
state: P::State,
}
trait AnyPlugin<App: 'static>: 'static {
fn name(&self) -> &'static str;
fn on_before_render(&self, app: Option<&App>, render_count: u64);
fn on_render(&self, app: Option<&App>, render_count: u64);
fn on_shutdown(&self, app: Option<&App>, render_count: u64);
}
impl<P: Plugin + 'static, App: 'static> AnyPlugin<App> for Registered<P> {
fn name(&self) -> &'static str {
self.plugin.name()
}
fn on_before_render(&self, app: Option<&App>, render_count: u64) {
let ctx: PluginCtx<'_, P::State, App> = PluginCtx {
state: &self.state,
app,
render_count,
};
self.plugin.on_before_render(&ctx);
}
fn on_render(&self, app: Option<&App>, render_count: u64) {
let ctx: PluginCtx<'_, P::State, App> = PluginCtx {
state: &self.state,
app,
render_count,
};
self.plugin.on_render(&ctx);
}
fn on_shutdown(&self, app: Option<&App>, render_count: u64) {
let ctx: PluginCtx<'_, P::State, App> = PluginCtx {
state: &self.state,
app,
render_count,
};
self.plugin.on_shutdown(&ctx);
}
}
pub struct PluginRegistry<App: 'static = ()> {
plugins: Vec<Rc<dyn AnyPlugin<App>>>,
}
impl<App: 'static> PluginRegistry<App> {
pub fn new() -> Self {
PluginRegistry {
plugins: Vec::new(),
}
}
pub fn register<P: Plugin + 'static>(&mut self, plugin: P) -> &'static str
where
P::State: Default,
{
let name = plugin.name();
if self.plugins.iter().any(|p| p.name() == name) {
panic!("appfront plugin registry: duplicate plugin name `{name}`");
}
let registered: Rc<dyn AnyPlugin<App>> = Rc::new(Registered {
state: plugin.init(),
plugin,
});
self.plugins.push(registered);
name
}
pub fn register_with_state<P: Plugin + 'static>(&mut self, plugin: P, state: P::State) -> &'static str {
let name = plugin.name();
if self.plugins.iter().any(|p| p.name() == name) {
panic!("appfront plugin registry: duplicate plugin name `{name}`");
}
let registered: Rc<dyn AnyPlugin<App>> = Rc::new(Registered { state, plugin });
self.plugins.push(registered);
name
}
pub fn run_before_render_hooks(&self, app: Option<&App>) {
for p in &self.plugins {
p.on_before_render(app, self.render_count());
}
}
pub fn run_render_hooks(&self, app: Option<&App>) {
let count = self.render_count();
for p in &self.plugins {
p.on_render(app, count);
}
}
pub fn run_shutdown_hooks(&self, app: Option<&App>) {
for p in &self.plugins {
p.on_shutdown(app, self.render_count());
}
}
pub fn len(&self) -> usize {
self.plugins.len()
}
pub fn is_empty(&self) -> bool {
self.plugins.is_empty()
}
fn render_count(&self) -> u64 {
RENDER_COUNT.with(|c| c.get())
}
pub fn bump_render_count(&self) {
RENDER_COUNT.with(|c| c.set(c.get() + 1));
}
}
thread_local! {
static RENDER_COUNT: Cell<u64> = const { Cell::new(0) };
}
impl<App: 'static> Default for PluginRegistry<App> {
fn default() -> Self {
Self::new()
}
}
impl<App: 'static> Clone for PluginRegistry<App> {
fn clone(&self) -> Self {
PluginRegistry {
plugins: self.plugins.clone(),
}
}
}
pub fn context_for_plugin<S: Clone + 'static>(state: S) -> Context<S> {
Context::new(state)
}
#[cfg(test)]
mod tests {
use super::*;
struct Counter;
impl Plugin for Counter {
type State = Cell<u32>;
fn name(&self) -> &'static str {
"counter"
}
fn init(&self) -> Self::State {
Cell::new(0)
}
fn on_render<A: 'static>(&self, ctx: &PluginCtx<Self::State, A>) {
ctx.state.set(ctx.state.get() + 1);
}
}
struct Named {
name: &'static str,
}
impl Plugin for Named {
type State = ();
fn name(&self) -> &'static str {
self.name
}
}
#[derive(Debug, PartialEq)]
struct Theme {
dark: bool,
}
struct ThemePlugin;
impl Plugin for ThemePlugin {
type State = Theme;
fn name(&self) -> &'static str {
"theme"
}
fn init(&self) -> Self::State {
Theme { dark: false }
}
}
#[test]
fn registers_and_runs_render_hooks() {
let mut reg = PluginRegistry::<()>::new();
reg.register(Counter);
assert_eq!(reg.len(), 1);
reg.run_render_hooks(None);
reg.bump_render_count();
reg.run_render_hooks(None);
reg.bump_render_count();
assert_eq!(reg.render_count(), 2);
}
#[test]
fn distinct_named_plugins_register_independently() {
let mut reg = PluginRegistry::<()>::new();
reg.register(Named { name: "a" });
reg.register(Named { name: "b" });
assert_eq!(reg.len(), 2);
}
#[test]
fn plugin_with_state_registers() {
let mut reg = PluginRegistry::<()>::new();
reg.register_with_state(ThemePlugin, Theme { dark: false });
assert!(!reg.is_empty());
}
#[test]
#[should_panic(expected = "duplicate plugin name")]
fn duplicate_names_panic() {
let mut reg = PluginRegistry::<()>::new();
reg.register(Named { name: "dup" });
reg.register(Named { name: "dup" });
}
}