Skip to content

Commit

Permalink
add fullscreen example
Browse files Browse the repository at this point in the history
  • Loading branch information
ccbrown committed Sep 21, 2024
1 parent b16368c commit 6c428fb
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 93 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ license = "MIT OR Apache-2.0"
iocraft = { path = "packages/iocraft" }
futures = "0.3.30"
smol = "2.0.1"
chrono = "0.4.38"

[dependencies]
unicode-width = "0.1.13"
65 changes: 65 additions & 0 deletions examples/fullscreen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use chrono::Local;
use iocraft::prelude::*;
use std::time::Duration;

#[component]
fn Example(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let (width, height) = hooks.use_terminal_size();
let mut system = hooks.use_context_mut::<SystemContext>();
let time = hooks.use_state(|| Local::now());
let should_exit = hooks.use_state(|| false);

hooks.use_future(async move {
loop {
smol::Timer::after(Duration::from_secs(1)).await;
time.set(Local::now());
}
});

hooks.use_terminal_events({
move |event| match event {
TerminalEvent::Key(KeyEvent { code, kind, .. }) if kind != KeyEventKind::Release => {
match code {
KeyCode::Char('q') => should_exit.set(true),
_ => {}
}
}
_ => {}
}
});

if should_exit.get() {
system.exit();
}

element! {
Box(
// subtract one in case there's a scrollbar
width: width - 1,
height,
background_color: Color::DarkGrey,
border_style: BorderStyle::Double,
border_color: Color::Blue,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
) {
Box(
border_style: BorderStyle::Round,
border_color: Color::Blue,
margin_bottom: 2,
padding_top: 2,
padding_bottom: 2,
padding_left: 8,
padding_right: 8,
) {
Text(content: format!("Current Time: {}", time.get().format("%r")))
}
Text(content: "Press \"q\" to quit.")
}
}
}

fn main() {
smol::block_on(element!(Example).fullscreen()).unwrap();
}
3 changes: 0 additions & 3 deletions packages/iocraft/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,3 @@ generational-box = "0.5.6"

[dev-dependencies]
indoc = "2"
smol = "2.0.1"
smol-macros = "0.1.1"
macro_rules_attribute = "0.2.0"
30 changes: 22 additions & 8 deletions packages/iocraft/src/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,21 @@ impl Canvas {
}
}

fn write_impl<W: Write>(&self, mut w: W, ansi: bool) -> io::Result<()> {
fn write_impl<W: Write>(
&self,
mut w: W,
ansi: bool,
omit_final_newline: bool,
) -> io::Result<()> {
if ansi {
write!(w, csi!("0m"))?;
}

let mut background_color = None;
let mut text_style = CanvasTextStyle::default();

for row in &self.cells {
for y in 0..self.cells.len() {
let row = &self.cells[y];
let last_non_empty = row.iter().rposition(|cell| !cell.is_empty());
let row = &row[..last_non_empty.map_or(0, |i| i + 1)];
let mut col = 0;
Expand Down Expand Up @@ -196,10 +202,14 @@ impl Canvas {
}
// clear until end of line
write!(w, csi!("K"))?;
// add a carriage return in case we're in raw mode
w.write_all(b"\r\n")?;
} else {
w.write_all(b"\n")?;
}
if !omit_final_newline || y < self.cells.len() - 1 {
if ansi {
// add a carriage return in case we're in raw mode
w.write_all(b"\r\n")?;
} else {
w.write_all(b"\n")?;
}
}
}
if ansi {
Expand All @@ -211,12 +221,16 @@ impl Canvas {

/// Writes the canvas to the given writer with ANSI escape codes.
pub fn write_ansi<W: Write>(&self, w: W) -> io::Result<()> {
self.write_impl(w, true)
self.write_impl(w, true, false)
}

pub(crate) fn write_ansi_without_final_newline<W: Write>(&self, w: W) -> io::Result<()> {
self.write_impl(w, true, true)
}

/// Writes the canvas to the given writer as unstyled text, without ANSI escape codes.
pub fn write<W: Write>(&self, w: W) -> io::Result<()> {
self.write_impl(w, false)
self.write_impl(w, false, false)
}
}

Expand Down
29 changes: 24 additions & 5 deletions packages/iocraft/src/element.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
component::{Component, ComponentHelper, ComponentHelperExt},
props::AnyProps,
render, terminal_render_loop, Canvas,
render, terminal_render_loop, Canvas, Terminal,
};
use crossterm::{terminal, tty::IsTty};
use std::{
Expand Down Expand Up @@ -195,6 +195,9 @@ pub trait ElementExt: private::Sealed + Sized {

/// Renders the element in a loop, allowing it to be dynamic and interactive.
fn render_loop(&mut self) -> impl Future<Output = io::Result<()>>;

/// Renders the element as fullscreen in a loop, allowing it to be dynamic and interactive.
fn fullscreen(&mut self) -> impl Future<Output = io::Result<()>>;
}

impl<'a> ElementExt for AnyElement<'a> {
Expand All @@ -216,7 +219,11 @@ impl<'a> ElementExt for AnyElement<'a> {
}

async fn render_loop(&mut self) -> io::Result<()> {
terminal_render_loop(self, stdout()).await
terminal_render_loop(self, Terminal::new()?).await
}

async fn fullscreen(&mut self) -> io::Result<()> {
terminal_render_loop(self, Terminal::fullscreen()?).await
}
}

Expand All @@ -239,7 +246,11 @@ impl<'a> ElementExt for &mut AnyElement<'a> {
}

async fn render_loop(&mut self) -> io::Result<()> {
terminal_render_loop(&mut **self, stdout()).await
terminal_render_loop(&mut **self, Terminal::new()?).await
}

async fn fullscreen(&mut self) -> io::Result<()> {
terminal_render_loop(&mut **self, Terminal::fullscreen()?).await
}
}

Expand All @@ -265,7 +276,11 @@ where
}

async fn render_loop(&mut self) -> io::Result<()> {
terminal_render_loop(self, stdout()).await
terminal_render_loop(self, Terminal::new()?).await
}

async fn fullscreen(&mut self) -> io::Result<()> {
terminal_render_loop(self, Terminal::fullscreen()?).await
}
}

Expand All @@ -291,7 +306,11 @@ where
}

async fn render_loop(&mut self) -> io::Result<()> {
terminal_render_loop(&mut **self, stdout()).await
terminal_render_loop(&mut **self, Terminal::new()?).await
}

async fn fullscreen(&mut self) -> io::Result<()> {
terminal_render_loop(&mut **self, Terminal::fullscreen()?).await
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/iocraft/src/hooks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ mod use_state;
pub use use_state::*;
mod use_terminal_events;
pub use use_terminal_events::*;
mod use_terminal_size;
pub use use_terminal_size::*;
23 changes: 23 additions & 0 deletions packages/iocraft/src/hooks/use_terminal_size.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use crate::{
hooks::{UseState, UseTerminalEvents},
Hooks, TerminalEvent,
};
use crossterm::terminal;

/// `UseTerminalSize` is a hook that returns the current terminal size.
pub trait UseTerminalSize {
/// Returns the current terminal size as a tuple of `(width, height)`.
fn use_terminal_size(&mut self) -> (u16, u16);
}

impl UseTerminalSize for Hooks<'_, '_> {
fn use_terminal_size(&mut self) -> (u16, u16) {
let size = self.use_state(|| terminal::size().unwrap_or((0, 0)));
self.use_terminal_events(move |event| {
if let TerminalEvent::Resize(width, height) = event {
size.set((width, height));
}
});
size.get()
}
}
75 changes: 20 additions & 55 deletions packages/iocraft/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,38 +354,40 @@ impl<'a> Tree<'a> {
}
}

async fn terminal_render_loop<W>(&mut self, mut w: W) -> io::Result<()>
where
W: Write,
{
let mut terminal = Terminal::new()?;
async fn terminal_render_loop(&mut self, mut term: Terminal) -> io::Result<()> {
let mut prev_canvas: Option<Canvas> = None;
loop {
let width = terminal.width().ok().map(|w| w as usize);
execute!(w, terminal::BeginSynchronizedUpdate,)?;
let lines_to_rewind_to_clear = prev_canvas.as_ref().map_or(0, |c| c.height());
let output = self.render(width, Some(&mut terminal), lines_to_rewind_to_clear);
let width = term.width().ok().map(|w| w as usize);
execute!(term, terminal::BeginSynchronizedUpdate,)?;
let lines_to_rewind_to_clear = prev_canvas
.as_ref()
.map_or(0, |c| c.height() - if term.is_fullscreen() { 1 } else { 0 });
let output = self.render(width, Some(&mut term), lines_to_rewind_to_clear);
if output.did_clear_terminal_output || prev_canvas.as_ref() != Some(&output.canvas) {
if !output.did_clear_terminal_output {
terminal.rewind_lines(lines_to_rewind_to_clear as _)?;
term.rewind_lines(lines_to_rewind_to_clear as _)?;
}
if term.is_fullscreen() {
output.canvas.write_ansi_without_final_newline(&mut term)?;
} else {
output.canvas.write_ansi(&mut term)?;
}
output.canvas.write_ansi(&mut w)?;
}
prev_canvas = Some(output.canvas);
execute!(w, terminal::EndSynchronizedUpdate)?;
if self.system_context.should_exit() || terminal.received_ctrl_c() {
execute!(term, terminal::EndSynchronizedUpdate)?;
if self.system_context.should_exit() || term.received_ctrl_c() {
break;
}
select(
self.root_component.wait().boxed_local(),
terminal.wait().boxed_local(),
term.wait().boxed_local(),
)
.await;
if terminal.received_ctrl_c() {
if term.received_ctrl_c() {
break;
}
}
mem::drop(terminal);
write!(term, "\r\n")?;
Ok(())
}
}
Expand All @@ -396,48 +398,11 @@ pub(crate) fn render<E: ElementExt>(mut e: E, max_width: Option<usize>) -> Canva
tree.render(max_width, None, 0).canvas
}

pub(crate) async fn terminal_render_loop<E, W>(mut e: E, dest: W) -> io::Result<()>
pub(crate) async fn terminal_render_loop<E>(mut e: E, term: Terminal) -> io::Result<()>
where
E: ElementExt,
W: Write,
{
let h = e.helper();
let mut tree = Tree::new(e.props_mut(), h);
tree.terminal_render_loop(dest).await
}

#[cfg(test)]
mod tests {
use crate::{hooks::UseFuture, prelude::*};
use macro_rules_attribute::apply;
use smol_macros::test;

#[component]
fn MyComponent(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let mut system = hooks.use_context_mut::<SystemContext>();
let mut counter = hooks.use_state(|| 0);

hooks.use_future(async move {
counter += 1;
});

if counter == 1 {
system.exit();
}

element! {
Text(content: format!("count: {}", counter))
}
}

#[apply(test!)]
async fn test_terminal_render_loop() {
let mut buf = Vec::new();
terminal_render_loop(element!(MyComponent), &mut buf)
.await
.unwrap();
let output = String::from_utf8_lossy(&buf);
assert!(output.contains("count: 0"));
assert!(output.contains("count: 1"));
}
tree.terminal_render_loop(term).await
}
15 changes: 15 additions & 0 deletions packages/iocraft/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ impl From<Padding> for LengthPercentage {

macro_rules! impl_from_length {
($name:ident) => {
impl From<i16> for $name {
fn from(l: i16) -> Self {
$name::Length(l as _)
}
}
impl From<i32> for $name {
fn from(l: i32) -> Self {
$name::Length(l as _)
}
}
impl From<u16> for $name {
fn from(l: u16) -> Self {
$name::Length(l as _)
}
}
impl From<u32> for $name {
fn from(l: u32) -> Self {
$name::Length(l)
Expand Down
Loading

0 comments on commit 6c428fb

Please sign in to comment.