textual 1.0.0-dev

A reactive TUI framework inspired by the Python Textual library
Documentation
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use rich_rs::{Console, ConsoleOptions, Segment, Segments};
use textual::message::MessageEvent;
use textual::prelude::*;

#[derive(Clone)]
struct ProbeHandles {
    layout_calls: Arc<AtomicUsize>,
    message_calls: Arc<AtomicUsize>,
    scroll_calls: Arc<AtomicUsize>,
    last_width: Arc<AtomicUsize>,
    last_height: Arc<AtomicUsize>,
}

impl ProbeHandles {
    fn new() -> Self {
        Self {
            layout_calls: Arc::new(AtomicUsize::new(0)),
            message_calls: Arc::new(AtomicUsize::new(0)),
            scroll_calls: Arc::new(AtomicUsize::new(0)),
            last_width: Arc::new(AtomicUsize::new(0)),
            last_height: Arc::new(AtomicUsize::new(0)),
        }
    }
}

struct ProbeWidget {
    handles: ProbeHandles,
}

impl ProbeWidget {
    fn new(handles: ProbeHandles) -> Self {
        Self { handles }
    }
}

impl Widget for ProbeWidget {
    fn render(&self, _console: &Console, options: &ConsoleOptions) -> Segments {
        let mut out = Segments::new();
        out.push(Segment::new(" ".repeat(options.size.0.max(1))));
        out
    }

    fn on_layout(&mut self, width: u16, height: u16) {
        self.handles.layout_calls.fetch_add(1, Ordering::Relaxed);
        self.handles
            .last_width
            .store(width as usize, Ordering::Relaxed);
        self.handles
            .last_height
            .store(height as usize, Ordering::Relaxed);
    }

    fn on_message(&mut self, _message: &MessageEvent, _ctx: &mut EventCtx) {
        self.handles.message_calls.fetch_add(1, Ordering::Relaxed);
    }

    fn on_mouse_scroll(&mut self, _delta_x: i32, _delta_y: i32, _ctx: &mut EventCtx) {
        self.handles.scroll_calls.fetch_add(1, Ordering::Relaxed);
    }
}

#[test]
fn panel_forwards_layout_and_messages() {
    let handles = ProbeHandles::new();
    let mut panel = Panel::new(ProbeWidget::new(handles.clone()))
        .padding(1)
        .border(true);

    panel.on_layout(20, 10);
    assert_eq!(handles.layout_calls.load(Ordering::Relaxed), 1);
    assert_eq!(handles.last_width.load(Ordering::Relaxed), 16);
    assert_eq!(handles.last_height.load(Ordering::Relaxed), 6);

    let message = MessageEvent {
        sender: NodeId::default(),
        message: Message::ClearRequested(ClearRequested),
        control: None,
    };
    let mut ctx = EventCtx::default();
    panel.on_message(&message, &mut ctx);
    assert_eq!(handles.message_calls.load(Ordering::Relaxed), 1);
}

#[test]
fn frame_forwards_layout_messages_and_scroll() {
    let handles = ProbeHandles::new();
    let mut frame = Frame::new(ProbeWidget::new(handles.clone()))
        .padding(1)
        .border(true);

    frame.on_layout(20, 10);
    assert_eq!(handles.layout_calls.load(Ordering::Relaxed), 1);
    assert_eq!(handles.last_width.load(Ordering::Relaxed), 16);
    assert_eq!(handles.last_height.load(Ordering::Relaxed), 6);

    let message = MessageEvent {
        sender: NodeId::default(),
        message: Message::ClearRequested(ClearRequested),
        control: None,
    };
    let mut ctx = EventCtx::default();
    frame.on_message(&message, &mut ctx);
    assert_eq!(handles.message_calls.load(Ordering::Relaxed), 1);

    frame.on_mouse_scroll(0, 1, &mut ctx);
    assert_eq!(handles.scroll_calls.load(Ordering::Relaxed), 1);
}