use std::time::Duration;
use tuirealm::application::{Application, PollStrategy};
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::listener::EventListenerCfg;
use tuirealm::props::{
AttrValue, Attribute, Color, HorizontalAlignment, PropBound, PropPayload, Props, QueryResult,
Style,
};
use tuirealm::ratatui::Frame;
use tuirealm::ratatui::layout::{Constraint, Direction, Layout, Rect};
use tuirealm::ratatui::widgets::Paragraph;
use tuirealm::state::State;
use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalAdapter, TerminalResult};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let event_listener =
EventListenerCfg::default().crossterm_input_listener(Duration::from_millis(10), 10);
let mut app: Application<Id, Msg, NoUserEvent> = Application::init(event_listener);
app.mount(Id::Label, Box::new(OurLabel::default()), vec![])?;
app.active(&Id::Label).expect("failed to active");
let mut model = Model::new(app)?;
while !model.quit {
match model
.app
.tick(PollStrategy::Once(Duration::from_millis(10)))
{
Err(err) => {
panic!("application error {err}");
}
Ok(messages) if !messages.is_empty() => {
model.redraw = true;
for msg in messages {
model.update(msg);
}
}
_ => {}
}
if model.redraw {
model.view();
model.redraw = false;
}
}
model.terminal.restore()?;
Ok(())
}
#[derive(Debug, PartialEq)]
pub enum Msg {
AppClose,
Redraw,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Id {
Label,
}
pub struct Model {
pub app: Application<Id, Msg, NoUserEvent>,
pub quit: bool,
pub redraw: bool,
pub terminal: CrosstermTerminalAdapter,
}
impl Model {
fn init_adapter() -> TerminalResult<CrosstermTerminalAdapter> {
let mut adapter = CrosstermTerminalAdapter::new()?;
adapter.enable_raw_mode()?;
adapter.enter_alternate_screen()?;
Ok(adapter)
}
pub fn new(app: Application<Id, Msg, NoUserEvent>) -> TerminalResult<Self> {
Ok(Self {
app,
quit: false,
redraw: true,
terminal: Self::init_adapter()?,
})
}
pub fn view(&mut self) {
assert!(
self.terminal
.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(3), ]
.as_ref(),
)
.split(f.area());
self.app.view(&Id::Label, f, chunks[0]);
})
.is_ok()
);
}
}
impl Model {
fn update(&mut self, msg: Msg) {
self.redraw = true;
match msg {
Msg::AppClose => {
self.quit = true; }
Msg::Redraw => (),
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct CustomState {
text: String,
}
impl Default for CustomState {
fn default() -> Self {
Self {
text: "Default text".to_string(),
}
}
}
#[derive(Debug)]
pub struct StdLabel {
props: Props,
}
impl Default for StdLabel {
fn default() -> Self {
let mut props = Props::default();
props.set(
Attribute::Value,
AttrValue::Payload(PropPayload::Any(CustomState::default().to_any_prop())),
);
Self { props }
}
}
impl Component for StdLabel {
fn view(&mut self, frame: &mut Frame, area: Rect) {
if matches!(
self.props.get(Attribute::Display),
Some(AttrValue::Flag(false))
) {
return;
}
let text = self
.props
.get(Attribute::Value)
.and_then(AttrValue::as_payload)
.and_then(PropPayload::as_any)
.and_then(|v| v.downcast_ref::<CustomState>())
.map(|v| v.text.as_str())
.unwrap_or("Unavailable; this is a bug");
let alignment = self
.props
.get(Attribute::TextAlign)
.and_then(AttrValue::as_alignment_horizontal)
.unwrap_or(HorizontalAlignment::Left);
let foreground = self
.props
.get(Attribute::Foreground)
.and_then(AttrValue::as_color)
.unwrap_or(Color::Reset);
let background = self
.props
.get(Attribute::Background)
.and_then(AttrValue::as_color)
.unwrap_or(Color::Reset);
let modifiers = self
.props
.get(Attribute::TextProps)
.and_then(AttrValue::as_text_modifiers)
.unwrap_or_default();
let [chunk1, chunk2] = Layout::new(
Direction::Vertical,
[Constraint::Length(1), Constraint::Min(1)],
)
.areas(area);
frame.render_widget(
Paragraph::new("The following text should be changing when pressing <TAB>:"),
chunk1,
);
frame.render_widget(
Paragraph::new(text)
.style(
Style::default()
.fg(foreground)
.bg(background)
.add_modifier(modifiers),
)
.alignment(alignment),
chunk2,
);
}
fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
self.props.get_for_query(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.props.set(attr, value);
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
CmdResult::Invalid(cmd)
}
}
#[derive(Debug, Component, Default)]
struct OurLabel {
component: StdLabel,
}
impl AppComponent<Msg, NoUserEvent> for OurLabel {
fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::AppClose),
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
let mut existing_attr = self.query(Attribute::Value).map_or_else(
|| AttrValue::Payload(PropPayload::Any(CustomState::default().to_any_prop())),
QueryResult::into_attr,
);
let tmp = existing_attr
.as_payload_mut()
.and_then(|v| v.as_any_mut())
.and_then(|v| v
.downcast_mut::<CustomState>())
.expect("Unexpected type in Attribute::Value! Expected PropPayload::Any + CustomState!");
tmp.text = match tmp.text.as_str() {
"Default text" => "Some other text".to_string(),
_ => CustomState::default().text,
};
self.attr(Attribute::Value, existing_attr);
Some(Msg::Redraw)
}
_ => None,
}
}
}