Skip to content

Commit

Permalink
added example for user events
Browse files Browse the repository at this point in the history
  • Loading branch information
veeso committed Aug 16, 2023
1 parent 547728d commit 9a7046b
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 7 deletions.
18 changes: 11 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ repository = "https://github.com/veeso/tui-realm"
bitflags = "^1.3"
crossterm = { version = "^0.25", optional = true }
lazy-regex = "^2.3.0"
serde = { version = "^1", features = [ "derive" ], optional = true }
serde = { version = "^1", features = ["derive"], optional = true }
termion = { version = "^1.5", optional = true }
thiserror = "^1.0.0"
tui = { version = "0.19", default-features = false }
Expand All @@ -29,12 +29,16 @@ toml = "0.5.9"
tempfile = "3.2.0"

[features]
default = [ "derive", "with-crossterm" ]
derive = [ "tuirealm_derive" ]
serialize = [ "serde" ]
with-crossterm = [ "crossterm", "tui/crossterm" ]
with-termion = [ "termion", "tui/termion" ]
default = ["derive", "with-crossterm"]
derive = ["tuirealm_derive"]
serialize = ["serde"]
with-crossterm = ["crossterm", "tui/crossterm"]
with-termion = ["termion", "tui/termion"]

[[example]]
name = "demo"
path = "examples/demo.rs"
path = "examples/demo/demo.rs"

[[example]]
name = "user-events"
path = "examples/user_events/user_events.rs"
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
111 changes: 111 additions & 0 deletions examples/user_events/components/label.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! ## Label
//!
//! label component

use std::time::UNIX_EPOCH;

use super::{Msg, UserEvent};

use tuirealm::command::{Cmd, CmdResult};
use tuirealm::event::{Key, KeyEvent};
use tuirealm::props::{Alignment, Color, Style, TextModifiers};
use tuirealm::tui::{layout::Rect, widgets::Paragraph};
use tuirealm::{AttrValue, Attribute, Component, Event, Frame, MockComponent, Props, State};

/// 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.
pub struct Label {
props: Props,
}

impl Default for Label {
fn default() -> Self {
Self {
props: Props::default(),
}
}
}

impl MockComponent for Label {
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_or(Attribute::Text, AttrValue::String(String::default()))
.unwrap_string();
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();
frame.render_widget(
Paragraph::new(text)
.style(
Style::default()
.fg(foreground)
.bg(background)
.add_modifier(modifiers),
)
.alignment(alignment),
area,
);
}
}

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
}
}

impl Component<Msg, UserEvent> for Label {
fn on(&mut self, ev: Event<UserEvent>) -> Option<Msg> {
// Does nothing
match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::AppClose),
Event::User(UserEvent::GotData(time)) => {
// set text
self.attr(
Attribute::Text,
AttrValue::String(
time.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string(),
),
);

Some(Msg::None)
}
_ => None,
}
}
}
10 changes: 10 additions & 0 deletions examples/user_events/components/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! ## Components
//!
//! demo example components

use super::{Msg, UserEvent};

mod label;

// -- export
pub use label::Label;
74 changes: 74 additions & 0 deletions examples/user_events/model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! ## Model
//!
//! app model

use super::{Id, Msg, UserEvent};

use tuirealm::terminal::TerminalBridge;
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::{Application, Update};

pub struct Model {
/// Application
pub app: Application<Id, Msg, UserEvent>,
/// 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,
}

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

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

// Let's implement Update for model

impl Update<Msg> for Model {
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::None => None,
}
} else {
None
}
}
}
117 changes: 117 additions & 0 deletions examples/user_events/user_events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
mod components;
mod model;

use std::time::{Duration, SystemTime};

use tuirealm::{
listener::{ListenerResult, Poll},
Application, Event, EventListenerCfg, PollStrategy, Sub, SubClause, SubEventClause, Update,
};

use components::Label;

use crate::model::Model;

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

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

#[derive(Debug, Eq, Clone, Copy, PartialOrd, Ord)]
pub enum UserEvent {
GotData(SystemTime),
None,
}

impl PartialEq for UserEvent {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(UserEvent::GotData(_), UserEvent::GotData(_)) => true,
_ => false,
}
}
}

#[derive(Default)]
struct UserDataPort;

impl Poll<UserEvent> for UserDataPort {
fn poll(&mut self) -> ListenerResult<Option<Event<UserEvent>>> {
ListenerResult::Ok(Some(Event::User(UserEvent::GotData(SystemTime::now()))))
}
}

fn main() {
let mut app: Application<Id, Msg, UserEvent> = Application::init(
EventListenerCfg::default()
.default_input_listener(Duration::from_millis(10))
.port(
Box::new(UserDataPort::default()),
Duration::from_millis(1000),
),
);

// subscribe component to clause
app.mount(
Id::Label,
Box::new(Label::default()),
vec![Sub::new(
SubEventClause::User(UserEvent::GotData(SystemTime::UNIX_EPOCH)),
SubClause::Always,
)],
)
.expect("failed to mount");
app.mount(
Id::Other,
Box::new(Label::default()),
vec![Sub::new(
SubEventClause::User(UserEvent::GotData(SystemTime::UNIX_EPOCH)),
SubClause::Always,
)],
)
.expect("failed to mount");

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

let mut model = Model::new(app);
let _ = model.terminal.enter_alternate_screen();
let _ = model.terminal.enable_raw_mode();
// 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.len() > 0 => {
// NOTE: redraw if at least one msg has been processed
model.redraw = true;
for msg in messages.into_iter() {
let mut msg = Some(msg);
while msg.is_some() {
msg = model.update(msg);
}
}
}
_ => {}
}
// Redraw
if model.redraw {
model.view();
model.redraw = false;
}
}
// Terminate terminal
let _ = model.terminal.leave_alternate_screen();
let _ = model.terminal.disable_raw_mode();
let _ = model.terminal.clear_screen();
}

0 comments on commit 9a7046b

Please sign in to comment.