tpt_appfront_core/
plugin.rs1use crate::context::Context;
29use std::cell::Cell;
30use std::rc::Rc;
31
32pub struct PluginCtx<'a, S, App = ()> {
39 pub state: &'a S,
41 pub app: Option<&'a App>,
43 pub render_count: u64,
46}
47
48impl<'a, S, App> PluginCtx<'a, S, App> {
49 pub fn app(&self) -> &'a App {
53 self.app.expect("plugin expected app state of type App")
54 }
55}
56
57pub trait Plugin {
63 type State: 'static;
65
66 fn name(&self) -> &'static str;
69
70 fn init(&self) -> Self::State
73 where
74 Self::State: Default,
75 {
76 Self::State::default()
77 }
78
79 fn on_before_render<A: 'static>(&self, _ctx: &PluginCtx<Self::State, A>) {}
83
84 fn on_render<A: 'static>(&self, _ctx: &PluginCtx<Self::State, A>) {}
87
88 fn on_shutdown<A: 'static>(&self, _ctx: &PluginCtx<Self::State, A>) {}
91}
92
93struct Registered<P: Plugin + 'static> {
96 plugin: P,
97 state: P::State,
98}
99
100trait AnyPlugin<App: 'static>: 'static {
103 fn name(&self) -> &'static str;
104 fn on_before_render(&self, app: Option<&App>, render_count: u64);
105 fn on_render(&self, app: Option<&App>, render_count: u64);
106 fn on_shutdown(&self, app: Option<&App>, render_count: u64);
107}
108
109impl<P: Plugin + 'static, App: 'static> AnyPlugin<App> for Registered<P> {
110 fn name(&self) -> &'static str {
111 self.plugin.name()
112 }
113 fn on_before_render(&self, app: Option<&App>, render_count: u64) {
114 let ctx: PluginCtx<'_, P::State, App> = PluginCtx {
115 state: &self.state,
116 app,
117 render_count,
118 };
119 self.plugin.on_before_render(&ctx);
120 }
121 fn on_render(&self, app: Option<&App>, render_count: u64) {
122 let ctx: PluginCtx<'_, P::State, App> = PluginCtx {
123 state: &self.state,
124 app,
125 render_count,
126 };
127 self.plugin.on_render(&ctx);
128 }
129 fn on_shutdown(&self, app: Option<&App>, render_count: u64) {
130 let ctx: PluginCtx<'_, P::State, App> = PluginCtx {
131 state: &self.state,
132 app,
133 render_count,
134 };
135 self.plugin.on_shutdown(&ctx);
136 }
137}
138
139pub struct PluginRegistry<App: 'static = ()> {
144 plugins: Vec<Rc<dyn AnyPlugin<App>>>,
145}
146
147impl<App: 'static> PluginRegistry<App> {
148 pub fn new() -> Self {
150 PluginRegistry {
151 plugins: Vec::new(),
152 }
153 }
154
155 pub fn register<P: Plugin + 'static>(&mut self, plugin: P) -> &'static str
159 where
160 P::State: Default,
161 {
162 let name = plugin.name();
163 if self.plugins.iter().any(|p| p.name() == name) {
164 panic!("appfront plugin registry: duplicate plugin name `{name}`");
165 }
166 let registered: Rc<dyn AnyPlugin<App>> = Rc::new(Registered {
167 state: plugin.init(),
168 plugin,
169 });
170 self.plugins.push(registered);
171 name
172 }
173
174 pub fn register_with_state<P: Plugin + 'static>(&mut self, plugin: P, state: P::State) -> &'static str {
176 let name = plugin.name();
177 if self.plugins.iter().any(|p| p.name() == name) {
178 panic!("appfront plugin registry: duplicate plugin name `{name}`");
179 }
180 let registered: Rc<dyn AnyPlugin<App>> = Rc::new(Registered { state, plugin });
181 self.plugins.push(registered);
182 name
183 }
184
185 pub fn run_before_render_hooks(&self, app: Option<&App>) {
187 for p in &self.plugins {
188 p.on_before_render(app, self.render_count());
189 }
190 }
191
192 pub fn run_render_hooks(&self, app: Option<&App>) {
194 let count = self.render_count();
195 for p in &self.plugins {
196 p.on_render(app, count);
197 }
198 }
199
200 pub fn run_shutdown_hooks(&self, app: Option<&App>) {
202 for p in &self.plugins {
203 p.on_shutdown(app, self.render_count());
204 }
205 }
206
207 pub fn len(&self) -> usize {
209 self.plugins.len()
210 }
211
212 pub fn is_empty(&self) -> bool {
214 self.plugins.is_empty()
215 }
216
217 fn render_count(&self) -> u64 {
218 RENDER_COUNT.with(|c| c.get())
219 }
220
221 pub fn bump_render_count(&self) {
225 RENDER_COUNT.with(|c| c.set(c.get() + 1));
226 }
227}
228
229thread_local! {
230 static RENDER_COUNT: Cell<u64> = const { Cell::new(0) };
231}
232
233impl<App: 'static> Default for PluginRegistry<App> {
234 fn default() -> Self {
235 Self::new()
236 }
237}
238
239impl<App: 'static> Clone for PluginRegistry<App> {
240 fn clone(&self) -> Self {
241 PluginRegistry {
242 plugins: self.plugins.clone(),
243 }
244 }
245}
246
247pub fn context_for_plugin<S: Clone + 'static>(state: S) -> Context<S> {
254 Context::new(state)
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 struct Counter;
262 impl Plugin for Counter {
263 type State = Cell<u32>;
264 fn name(&self) -> &'static str {
265 "counter"
266 }
267 fn init(&self) -> Self::State {
268 Cell::new(0)
269 }
270 fn on_render<A: 'static>(&self, ctx: &PluginCtx<Self::State, A>) {
271 ctx.state.set(ctx.state.get() + 1);
272 }
273 }
274
275 struct Named {
276 name: &'static str,
277 }
278 impl Plugin for Named {
279 type State = ();
280 fn name(&self) -> &'static str {
281 self.name
282 }
283 }
284
285 #[derive(Debug, PartialEq)]
286 struct Theme {
287 dark: bool,
288 }
289
290 struct ThemePlugin;
291 impl Plugin for ThemePlugin {
292 type State = Theme;
293 fn name(&self) -> &'static str {
294 "theme"
295 }
296 fn init(&self) -> Self::State {
297 Theme { dark: false }
298 }
299 }
300
301 #[test]
302 fn registers_and_runs_render_hooks() {
303 let mut reg = PluginRegistry::<()>::new();
304 reg.register(Counter);
305 assert_eq!(reg.len(), 1);
306
307 reg.run_render_hooks(None);
308 reg.bump_render_count();
309 reg.run_render_hooks(None);
310 reg.bump_render_count();
311
312 assert_eq!(reg.render_count(), 2);
314 }
315
316 #[test]
317 fn distinct_named_plugins_register_independently() {
318 let mut reg = PluginRegistry::<()>::new();
319 reg.register(Named { name: "a" });
320 reg.register(Named { name: "b" });
321 assert_eq!(reg.len(), 2);
322 }
323
324 #[test]
325 fn plugin_with_state_registers() {
326 let mut reg = PluginRegistry::<()>::new();
327 reg.register_with_state(ThemePlugin, Theme { dark: false });
328 assert!(!reg.is_empty());
329 }
330
331 #[test]
332 #[should_panic(expected = "duplicate plugin name")]
333 fn duplicate_names_panic() {
334 let mut reg = PluginRegistry::<()>::new();
335 reg.register(Named { name: "dup" });
336 reg.register(Named { name: "dup" });
337 }
338}