use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use crate::zone::{ZoneId, ZoneRequest};
#[derive(Debug, Clone)]
pub struct RenderContext {
pub base_style: Style,
pub focused: bool,
pub terminal_width: u16,
pub terminal_height: u16,
pub tick: usize,
}
impl RenderContext {
#[must_use]
pub fn new(base_style: Style, terminal_width: u16, terminal_height: u16) -> Self {
Self {
base_style,
focused: false,
terminal_width,
terminal_height,
tick: 0,
}
}
#[must_use]
pub fn with_focus(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
#[must_use]
pub fn with_tick(mut self, tick: usize) -> Self {
self.tick = tick;
self
}
}
pub trait ZonePlugin: Send + Sync {
fn id(&self) -> &str;
fn zones(&self) -> Vec<ZoneRequest> {
vec![]
}
fn on_register(&self, _zone_id: ZoneId) {}
fn render(&self, zone_id: ZoneId, ctx: &RenderContext, area: Rect, buf: &mut Buffer) -> bool;
fn on_event(&self, _zone_id: ZoneId, _event: &ZoneEvent) -> bool {
false
}
}
#[derive(Debug, Clone)]
pub enum ZoneEvent {
Key {
code: ratatui::crossterm::event::KeyCode,
modifiers: ratatui::crossterm::event::KeyModifiers,
},
Click {
x: u16,
y: u16,
},
Scroll {
delta: i8,
},
FocusGained,
FocusLost,
Resize {
width: u16,
height: u16,
},
}
#[cfg(test)]
#[allow(clippy::unnecessary_literal_bound)]
mod tests {
use super::*;
struct TestPlugin;
impl ZonePlugin for TestPlugin {
fn id(&self) -> &str {
"test"
}
fn render(&self, _: ZoneId, ctx: &RenderContext, area: Rect, buf: &mut Buffer) -> bool {
use ratatui::widgets::{Paragraph, Widget};
let text = if ctx.focused { "FOCUSED" } else { "normal" };
Paragraph::new(text).style(ctx.base_style).render(area, buf);
true
}
}
#[test]
fn plugin_id_is_accessible() {
let p = TestPlugin;
assert_eq!(p.id(), "test");
}
#[test]
fn default_zones_is_empty() {
let p = TestPlugin;
assert!(p.zones().is_empty());
}
#[test]
fn render_context_builder() {
let ctx = RenderContext::new(Style::default(), 120, 40)
.with_focus(true)
.with_tick(42);
assert!(ctx.focused);
assert_eq!(ctx.tick, 42);
assert_eq!(ctx.terminal_width, 120);
}
#[test]
fn plugin_renders_into_buffer() {
let p = TestPlugin;
let area = Rect::new(0, 0, 20, 1);
let mut buf = Buffer::empty(area);
let ctx = RenderContext::new(Style::default(), 80, 24);
let rendered = p.render(ZoneId::new(1), &ctx, area, &mut buf);
assert!(rendered);
let content: String = buf
.content()
.iter()
.map(|c| c.symbol().to_string())
.collect();
assert!(content.contains("normal"));
}
#[test]
fn plugin_renders_focused() {
let p = TestPlugin;
let area = Rect::new(0, 0, 20, 1);
let mut buf = Buffer::empty(area);
let ctx = RenderContext::new(Style::default(), 80, 24).with_focus(true);
p.render(ZoneId::new(1), &ctx, area, &mut buf);
let content: String = buf
.content()
.iter()
.map(|c| c.symbol().to_string())
.collect();
assert!(content.contains("FOCUSED"));
}
#[test]
fn default_on_event_returns_false() {
let p = TestPlugin;
let handled = p.on_event(
ZoneId::new(1),
&ZoneEvent::Key {
code: ratatui::crossterm::event::KeyCode::Char('a'),
modifiers: ratatui::crossterm::event::KeyModifiers::NONE,
},
);
assert!(!handled);
}
}