Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@

---

## next

Unreleased

- Add `PropPayload::Any` variant using `dyn Any`, allowing arbitrary data via `MockComponent::query` and `MockComponent::attr`.

## 3.2.0

Released on 10/11/2025
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ tokio-util = { version = "0.7", features = [
], default-features = false, optional = true }
futures-util = { version = "0.3", default-features = false, optional = true }
tuirealm_derive = { version = "2", optional = true }
dyn-clone = "1.0"

[dev-dependencies]
pretty_assertions = "^1"
Expand Down Expand Up @@ -69,6 +70,11 @@ name = "user-events"
path = "examples/user_events/user_events.rs"
required-features = ["crossterm"]

[[example]]
name = "arbitrary-data"
path = "examples/arbitrary_data.rs"
required-features = ["crossterm"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
290 changes: 290 additions & 0 deletions examples/arbitrary_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
//! This Example showcases the use of [`AnyProp`](tuirealm::props::AnyProp) via [`PropPayload`] for use in [`Props`], [`query`](MockComponent::query) and [`attr`](MockComponent::attr).
//!
//! The data structs used in this example are very simple and could be done via other values in [`PropPayload`] / [`AttrValue`],
//! but imagine this for outside sources like [`tuirealm-tree-view`](https://github.com/veeso/tui-realm-treeview)'s Tree data.
//!
//! The main section in this Example is [`StdLabel`] and [`OurLabel`].

use std::time::Duration;

use tuirealm::command::{Cmd, CmdResult};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{
Alignment, Color, PropBound, PropBoundExt, PropPayload, Style, TextModifiers,
};
use tuirealm::ratatui::layout::{Constraint, Direction, Layout, Rect};
use tuirealm::ratatui::widgets::Paragraph;
use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalAdapter, TerminalBridge};
use tuirealm::{
Application, AttrValue, Attribute, Component, Event, EventListenerCfg, Frame, MockComponent,
NoUserEvent, PollStrategy, Props, State, Update,
};

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);

// subscribe component to clause
app.mount(Id::Label, Box::new(OurLabel::default()), vec![])?;

app.active(&Id::Label).expect("failed to active");

let mut model = Model::new(app, CrosstermTerminalAdapter::new()?);
// Main loop
// NOTE: loop until quit; quit is set in update if AppClose is received from counter
while !model.quit {
// Tick
match model.app.tick(PollStrategy::Once) {
Err(err) => {
panic!("application error {err}");
}
Ok(messages) if !messages.is_empty() => {
// NOTE: redraw if at least one msg has been processed
model.redraw = true;
for msg in messages {
let mut msg = Some(msg);
while msg.is_some() {
msg = model.update(msg);
}
}
}
_ => {}
}
// Redraw
if model.redraw {
model.view();
model.redraw = false;
}
}

model.terminal.restore()?;

Ok(())
}

#[derive(Debug, PartialEq)]
pub enum Msg {
AppClose,
ForceRedraw,
}

// Let's define the component ids for our application
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Id {
Label,
}

pub struct Model<T>
where
T: TerminalAdapter,
{
/// Application
pub app: Application<Id, Msg, NoUserEvent>,
/// Indicates that the application must quit
pub quit: bool,
/// Tells whether to redraw interface
pub redraw: bool,
/// Used to draw to terminal
pub terminal: TerminalBridge<T>,
}

impl<T> Model<T>
where
T: TerminalAdapter,
{
pub fn new(app: Application<Id, Msg, NoUserEvent>, adapter: T) -> Self {
Self {
app,
quit: false,
redraw: true,
terminal: TerminalBridge::init(adapter).expect("Cannot initialize terminal"),
}
}

pub fn view(&mut self) {
assert!(
self.terminal
.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(3), // Label
]
.as_ref(),
)
.split(f.area());
self.app.view(&Id::Label, f, chunks[0]);
})
.is_ok()
);
}
}

// Let's implement Update for model

impl<T> Update<Msg> for Model<T>
where
T: TerminalAdapter,
{
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
if let Some(msg) = msg {
// Set redraw
self.redraw = true;
// Match message
match msg {
Msg::AppClose => {
self.quit = true; // Terminate
None
}
Msg::ForceRedraw => None,
}
} else {
None
}
}
}

/// This is our custom data we want to store on [`Props`] and communicate via [`query`](MockComponent::query) and [`attr`](MockComponent::attr).
#[derive(Debug, Clone, PartialEq)]
struct CustomState {
text: String,
}

impl Default for CustomState {
fn default() -> Self {
Self {
text: "Default text".to_string(),
}
}
}

/// Simple label component; just renders a text
/// NOTE: since I need just one label, I'm not going to use different object; I will directly implement Component for Label.
/// This is not ideal actually and in a real app you should differentiate Mock Components from Application Components.
#[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 MockComponent for StdLabel {
fn view(&mut self, frame: &mut Frame, area: Rect) {
// Check if visible
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
// Get properties
let text = self
.props
.get_ref(Attribute::Value)
.and_then(AttrValue::as_payload)
.and_then(PropPayload::as_any)
.map(PropBoundExt::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_or(Attribute::TextAlign, AttrValue::Alignment(Alignment::Left))
.unwrap_alignment();
let foreground = self
.props
.get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
.unwrap_color();
let background = self
.props
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
let modifiers = self
.props
.get_or(
Attribute::TextProps,
AttrValue::TextModifiers(TextModifiers::empty()),
)
.unwrap_text_modifiers();

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 parrsing <TAB>:"),
chunk1,
);

frame.render_widget(
Paragraph::new(text)
.style(
Style::default()
.fg(foreground)
.bg(background)
.add_modifier(modifiers),
)
.alignment(alignment),
chunk2,
);
}
}

fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.props.get(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) -> CmdResult {
CmdResult::None
}
}

#[derive(Debug, MockComponent, Default)]
struct OurLabel {
component: StdLabel,
}

impl Component<Msg, NoUserEvent> for OurLabel {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
// Does nothing
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).unwrap_or_else(|| {
AttrValue::Payload(PropPayload::Any(CustomState::default().to_any_prop()))
});
let tmp = existing_attr
.as_payload_mut()
.and_then(|v| v.as_any_mut())
.and_then(|v| v.as_any_mut()
.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::ForceRedraw)
}
_ => None,
}
}
}
Loading