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