-
-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
323 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |