1#![forbid(unsafe_code)]
2#![doc = "Backend traits for FrankenTUI: platform abstraction for input, presentation, and time."]
3#![doc = ""]
4#![doc = "This crate defines the boundary between the ftui runtime and platform-specific"]
5#![doc = "implementations (native terminal via `ftui-tty`, WASM via `ftui-web`)."]
6#![doc = ""]
7#![doc = "See ADR-008 for the design rationale."]
8
9use core::time::Duration;
10
11use ftui_core::event::Event;
12use ftui_core::terminal_capabilities::TerminalCapabilities;
13use ftui_render::buffer::Buffer;
14use ftui_render::diff::BufferDiff;
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub struct BackendFeatures {
22 pub mouse_capture: bool,
24 pub bracketed_paste: bool,
26 pub focus_events: bool,
28 pub kitty_keyboard: bool,
30}
31
32pub trait BackendClock {
37 fn now_mono(&self) -> Duration;
39}
40
41pub trait BackendEventSource {
47 type Error: core::fmt::Debug + core::fmt::Display;
49
50 fn size(&self) -> Result<(u16, u16), Self::Error>;
52
53 fn set_features(&mut self, features: BackendFeatures) -> Result<(), Self::Error>;
57
58 fn poll_event(&mut self, timeout: Duration) -> Result<bool, Self::Error>;
62
63 fn read_event(&mut self) -> Result<Option<Event>, Self::Error>;
67}
68
69pub trait BackendPresenter {
75 type Error: core::fmt::Debug + core::fmt::Display;
77
78 fn capabilities(&self) -> &TerminalCapabilities;
80
81 fn write_log(&mut self, text: &str) -> Result<(), Self::Error>;
83
84 fn present_ui(
90 &mut self,
91 buf: &Buffer,
92 diff: Option<&BufferDiff>,
93 full_repaint_hint: bool,
94 ) -> Result<(), Self::Error>;
95
96 fn gc(&mut self) {}
98}
99
100pub trait Backend {
106 type Error: core::fmt::Debug + core::fmt::Display;
108
109 type Clock: BackendClock;
111
112 type Events: BackendEventSource<Error = Self::Error>;
114
115 type Presenter: BackendPresenter<Error = Self::Error>;
117
118 fn clock(&self) -> &Self::Clock;
120
121 fn events(&mut self) -> &mut Self::Events;
123
124 fn presenter(&mut self) -> &mut Self::Presenter;
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use core::fmt;
132 use ftui_core::terminal_capabilities::TerminalCapabilities;
133
134 #[test]
139 fn backend_features_default_all_false() {
140 let f = BackendFeatures::default();
141 assert!(!f.mouse_capture);
142 assert!(!f.bracketed_paste);
143 assert!(!f.focus_events);
144 assert!(!f.kitty_keyboard);
145 }
146
147 #[test]
148 fn backend_features_equality() {
149 let a = BackendFeatures {
150 mouse_capture: true,
151 bracketed_paste: false,
152 focus_events: true,
153 kitty_keyboard: false,
154 };
155 let b = BackendFeatures {
156 mouse_capture: true,
157 bracketed_paste: false,
158 focus_events: true,
159 kitty_keyboard: false,
160 };
161 assert_eq!(a, b);
162 }
163
164 #[test]
165 fn backend_features_inequality() {
166 let a = BackendFeatures::default();
167 let b = BackendFeatures {
168 mouse_capture: true,
169 ..BackendFeatures::default()
170 };
171 assert_ne!(a, b);
172 }
173
174 #[test]
175 fn backend_features_clone() {
176 let a = BackendFeatures {
177 mouse_capture: true,
178 bracketed_paste: true,
179 focus_events: true,
180 kitty_keyboard: true,
181 };
182 let b = a;
183 assert_eq!(a, b);
184 }
185
186 #[test]
187 fn backend_features_debug() {
188 let f = BackendFeatures::default();
189 let debug = format!("{f:?}");
190 assert!(debug.contains("BackendFeatures"));
191 assert!(debug.contains("mouse_capture"));
192 }
193
194 struct TestClock {
199 elapsed: Duration,
200 }
201
202 impl BackendClock for TestClock {
203 fn now_mono(&self) -> Duration {
204 self.elapsed
205 }
206 }
207
208 #[derive(Debug)]
209 struct TestError(String);
210
211 impl fmt::Display for TestError {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 write!(f, "TestError: {}", self.0)
214 }
215 }
216
217 struct TestEventSource {
218 features: BackendFeatures,
219 events: Vec<Event>,
220 }
221
222 impl BackendEventSource for TestEventSource {
223 type Error = TestError;
224
225 fn size(&self) -> Result<(u16, u16), Self::Error> {
226 Ok((80, 24))
227 }
228
229 fn set_features(&mut self, features: BackendFeatures) -> Result<(), Self::Error> {
230 self.features = features;
231 Ok(())
232 }
233
234 fn poll_event(&mut self, _timeout: Duration) -> Result<bool, Self::Error> {
235 Ok(!self.events.is_empty())
236 }
237
238 fn read_event(&mut self) -> Result<Option<Event>, Self::Error> {
239 Ok(if self.events.is_empty() {
240 None
241 } else {
242 Some(self.events.remove(0))
243 })
244 }
245 }
246
247 struct TestPresenter {
248 caps: TerminalCapabilities,
249 logs: Vec<String>,
250 present_count: usize,
251 gc_count: usize,
252 }
253
254 impl BackendPresenter for TestPresenter {
255 type Error = TestError;
256
257 fn capabilities(&self) -> &TerminalCapabilities {
258 &self.caps
259 }
260
261 fn write_log(&mut self, text: &str) -> Result<(), Self::Error> {
262 self.logs.push(text.to_owned());
263 Ok(())
264 }
265
266 fn present_ui(
267 &mut self,
268 _buf: &Buffer,
269 _diff: Option<&BufferDiff>,
270 _full_repaint_hint: bool,
271 ) -> Result<(), Self::Error> {
272 self.present_count += 1;
273 Ok(())
274 }
275
276 fn gc(&mut self) {
277 self.gc_count += 1;
278 }
279 }
280
281 struct TestBackend {
282 clock: TestClock,
283 events: TestEventSource,
284 presenter: TestPresenter,
285 }
286
287 impl Backend for TestBackend {
288 type Error = TestError;
289 type Clock = TestClock;
290 type Events = TestEventSource;
291 type Presenter = TestPresenter;
292
293 fn clock(&self) -> &Self::Clock {
294 &self.clock
295 }
296
297 fn events(&mut self) -> &mut Self::Events {
298 &mut self.events
299 }
300
301 fn presenter(&mut self) -> &mut Self::Presenter {
302 &mut self.presenter
303 }
304 }
305
306 fn make_test_backend() -> TestBackend {
307 TestBackend {
308 clock: TestClock {
309 elapsed: Duration::from_millis(42),
310 },
311 events: TestEventSource {
312 features: BackendFeatures::default(),
313 events: Vec::new(),
314 },
315 presenter: TestPresenter {
316 caps: TerminalCapabilities::default(),
317 logs: Vec::new(),
318 present_count: 0,
319 gc_count: 0,
320 },
321 }
322 }
323
324 #[test]
329 fn clock_returns_elapsed() {
330 let clock = TestClock {
331 elapsed: Duration::from_secs(5),
332 };
333 assert_eq!(clock.now_mono(), Duration::from_secs(5));
334 }
335
336 #[test]
337 fn clock_zero_duration() {
338 let clock = TestClock {
339 elapsed: Duration::ZERO,
340 };
341 assert_eq!(clock.now_mono(), Duration::ZERO);
342 }
343
344 #[test]
349 fn event_source_size() {
350 let src = TestEventSource {
351 features: BackendFeatures::default(),
352 events: Vec::new(),
353 };
354 assert_eq!(src.size().unwrap(), (80, 24));
355 }
356
357 #[test]
358 fn event_source_set_features() {
359 let mut src = TestEventSource {
360 features: BackendFeatures::default(),
361 events: Vec::new(),
362 };
363 let features = BackendFeatures {
364 mouse_capture: true,
365 bracketed_paste: true,
366 focus_events: false,
367 kitty_keyboard: false,
368 };
369 src.set_features(features).unwrap();
370 assert!(src.features.mouse_capture);
371 assert!(src.features.bracketed_paste);
372 }
373
374 #[test]
375 fn event_source_poll_empty() {
376 let mut src = TestEventSource {
377 features: BackendFeatures::default(),
378 events: Vec::new(),
379 };
380 assert!(!src.poll_event(Duration::from_millis(10)).unwrap());
381 }
382
383 #[test]
384 fn event_source_read_none_when_empty() {
385 let mut src = TestEventSource {
386 features: BackendFeatures::default(),
387 events: Vec::new(),
388 };
389 assert!(src.read_event().unwrap().is_none());
390 }
391
392 #[test]
393 fn event_source_poll_with_events() {
394 let mut src = TestEventSource {
395 features: BackendFeatures::default(),
396 events: vec![Event::Focus(true)],
397 };
398 assert!(src.poll_event(Duration::from_millis(10)).unwrap());
399 }
400
401 #[test]
402 fn event_source_read_drains_events() {
403 let mut src = TestEventSource {
404 features: BackendFeatures::default(),
405 events: vec![Event::Focus(true), Event::Focus(false)],
406 };
407 let e1 = src.read_event().unwrap();
408 assert!(e1.is_some());
409 let e2 = src.read_event().unwrap();
410 assert!(e2.is_some());
411 let e3 = src.read_event().unwrap();
412 assert!(e3.is_none());
413 }
414
415 #[test]
420 fn presenter_capabilities() {
421 let p = TestPresenter {
422 caps: TerminalCapabilities::default(),
423 logs: Vec::new(),
424 present_count: 0,
425 gc_count: 0,
426 };
427 let _caps = p.capabilities();
428 }
429
430 #[test]
431 fn presenter_write_log() {
432 let mut p = TestPresenter {
433 caps: TerminalCapabilities::default(),
434 logs: Vec::new(),
435 present_count: 0,
436 gc_count: 0,
437 };
438 p.write_log("hello").unwrap();
439 p.write_log("world").unwrap();
440 assert_eq!(p.logs.len(), 2);
441 assert_eq!(p.logs[0], "hello");
442 assert_eq!(p.logs[1], "world");
443 }
444
445 #[test]
446 fn presenter_present_ui() {
447 let mut p = TestPresenter {
448 caps: TerminalCapabilities::default(),
449 logs: Vec::new(),
450 present_count: 0,
451 gc_count: 0,
452 };
453 let buf = Buffer::new(10, 5);
454 p.present_ui(&buf, None, false).unwrap();
455 p.present_ui(&buf, None, true).unwrap();
456 assert_eq!(p.present_count, 2);
457 }
458
459 #[test]
460 fn presenter_gc() {
461 let mut p = TestPresenter {
462 caps: TerminalCapabilities::default(),
463 logs: Vec::new(),
464 present_count: 0,
465 gc_count: 0,
466 };
467 p.gc();
468 p.gc();
469 assert_eq!(p.gc_count, 2);
470 }
471
472 #[test]
477 fn backend_clock_access() {
478 let backend = make_test_backend();
479 assert_eq!(backend.clock().now_mono(), Duration::from_millis(42));
480 }
481
482 #[test]
483 fn backend_events_access() {
484 let mut backend = make_test_backend();
485 let size = backend.events().size().unwrap();
486 assert_eq!(size, (80, 24));
487 }
488
489 #[test]
490 fn backend_presenter_access() {
491 let mut backend = make_test_backend();
492 let buf = Buffer::new(10, 5);
493 backend.presenter().present_ui(&buf, None, false).unwrap();
494 assert_eq!(backend.presenter.present_count, 1);
495 }
496
497 #[test]
498 fn backend_full_cycle() {
499 let mut backend = make_test_backend();
500
501 let _now = backend.clock().now_mono();
503
504 backend
506 .events()
507 .set_features(BackendFeatures {
508 mouse_capture: true,
509 ..BackendFeatures::default()
510 })
511 .unwrap();
512 assert!(backend.events.features.mouse_capture);
513
514 let buf = Buffer::new(80, 24);
516 backend.presenter().write_log("frame start").unwrap();
517 backend.presenter().present_ui(&buf, None, false).unwrap();
518 backend.presenter().gc();
519
520 assert_eq!(backend.presenter.logs.len(), 1);
521 assert_eq!(backend.presenter.present_count, 1);
522 assert_eq!(backend.presenter.gc_count, 1);
523 }
524
525 #[test]
530 fn test_error_display() {
531 let err = TestError("something failed".into());
532 assert_eq!(format!("{err}"), "TestError: something failed");
533 }
534
535 #[test]
536 fn test_error_debug() {
537 let err = TestError("oops".into());
538 let debug = format!("{err:?}");
539 assert!(debug.contains("oops"));
540 }
541}