diff --git a/README.md b/README.md index 351b6ac..0568380 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,33 @@ # rcui Simple TUI framework in Rust inspired by Qt. + +## Example + +```rust +use rcui::*; + +fn main() { + rcui::exec(Proxy::wrap( + |root, event| match event { + Event::KeyStroke(key) => match *key as u8 as char { + 'q' => rcui::quit(), + 's' => root.down(), + 'w' => root.up(), + _ => {} + }, + }, + ItemList::new(vec![ + "foo", "bar", "baz" + ]), + )); + println!("Quiting gracefully uwu"); +} +``` + +## Quick Start + +```console +$ cargo run --example grid +$ cargo run --example item_list +``` diff --git a/examples/grid.rs b/examples/grid.rs index 6d18b46..438ec15 100644 --- a/examples/grid.rs +++ b/examples/grid.rs @@ -4,8 +4,20 @@ struct MyText { text: Text, } +impl MyText { + fn new(text: &str) -> Self { + Self { + text: Text::new(text), + } + } + + fn wrap(text: &str) -> Box { + Box::new(Self::new(text)) + } +} + impl Widget for MyText { - fn render(&self, rect: &rcui::Rect) { + fn render(&mut self, rect: &rcui::Rect) { self.text.render(rect) } @@ -63,36 +75,39 @@ impl Widget for MyText { } } -pub fn my_text(text: &str) -> Box { - Box::new(MyText { - text: Text { - text: text.to_string(), - halign: HAlign::Left, - valign: VAlign::Top, - }, - }) -} - fn main() { rcui::exec(Proxy::wrap( - |event| match event { - Event::KeyStroke(key) => { - if *key as u8 as char == 't' { - rcui::quit(); + |root, event| { + match event { + Event::KeyStroke(key) => { + if *key as u8 as char == 't' { + rcui::quit(); + } } } + root.handle_event(event); }, - vbox(vec![ - hbox(vec![ - Box::new(ItemList { - items: vec!["item1", "item2", "item3"], - }), - my_text("hello"), - my_text("hello"), + VBox::new(vec![ + HBox::wrap(vec![ + MyText::wrap("hello"), + MyText::wrap("hello"), + MyText::wrap("hello"), + ]), + HBox::wrap(vec![ + MyText::wrap("world"), + MyText::wrap("world"), + MyText::wrap("world"), + ]), + HBox::wrap(vec![ + MyText::wrap("foo"), + MyText::wrap("foo"), + MyText::wrap("foo"), + ]), + HBox::wrap(vec![ + MyText::wrap("bar"), + MyText::wrap("bar"), + MyText::wrap("bar"), ]), - hbox(vec![my_text("world"), my_text("world"), my_text("world")]), - hbox(vec![my_text("foo"), my_text("foo"), my_text("foo")]), - hbox(vec![my_text("bar"), my_text("bar"), my_text("bar")]), ]), )); diff --git a/examples/item_list.rs b/examples/item_list.rs new file mode 100644 index 0000000..0d75b93 --- /dev/null +++ b/examples/item_list.rs @@ -0,0 +1,18 @@ +use rcui::*; + +fn main() { + rcui::exec(Proxy::wrap( + |root, event| match event { + Event::KeyStroke(key) => match *key as u8 as char { + 'q' => rcui::quit(), + 's' => root.down(), + 'w' => root.up(), + _ => {} + }, + }, + ItemList::new(vec![ + "foo", "bar", "baz", "test", "hello", "world", "dfsdjf", "sdfjksdf", + ]), + )); + println!("Quiting gracefully uwu"); +} diff --git a/src/lib.rs b/src/lib.rs index 48d81ec..4c66104 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +pub mod style; + +use ncurses::CURSOR_VISIBILITY::*; use ncurses::*; use std::panic::{set_hook, take_hook}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -14,7 +17,7 @@ pub enum Event { } pub trait Widget { - fn render(&self, rect: &Rect); + fn render(&mut self, rect: &Rect); fn handle_event(&mut self, event: &Event); } @@ -22,8 +25,18 @@ pub struct HBox { pub widgets: Vec>, } +impl HBox { + pub fn new(widgets: Vec>) -> Self { + Self { widgets } + } + + pub fn wrap(widgets: Vec>) -> Box { + Box::new(Self::new(widgets)) + } +} + impl Widget for HBox { - fn render(&self, rect: &Rect) { + fn render(&mut self, rect: &Rect) { let n = self.widgets.len(); let widget_w = rect.w / n as f32; for i in 0..n { @@ -47,8 +60,18 @@ pub struct VBox { pub widgets: Vec>, } +impl VBox { + pub fn new(widgets: Vec>) -> Self { + Self { widgets } + } + + pub fn wrap(widgets: Vec>) -> Box { + Box::new(Self::new(widgets)) + } +} + impl Widget for VBox { - fn render(&self, rect: &Rect) { + fn render(&mut self, rect: &Rect) { let n = self.widgets.len(); let widget_h = rect.h / n as f32; for i in 0..n { @@ -88,8 +111,22 @@ pub struct Text { pub valign: VAlign, } +impl Text { + pub fn new(text: &str) -> Self { + Self { + text: text.to_string(), + halign: HAlign::Left, + valign: VAlign::Top, + } + } + + pub fn wrap(text: &str) -> Box { + Box::new(Self::new(text)) + } +} + impl Widget for Text { - fn render(&self, rect: &Rect) { + fn render(&mut self, rect: &Rect) { let s = self .text .get(..rect.w.floor() as usize) @@ -138,28 +175,76 @@ impl Widget for Text { fn handle_event(&mut self, _event: &Event) {} } -// TODO(#4): ItemList is not finished pub struct ItemList { pub items: Vec, + pub cursor: usize, + pub scroll: usize, } -impl Widget for ItemList { - fn render(&self, rect: &Rect) { - for (i, item) in self.items.iter().enumerate() { - let text = Text { - text: item.to_string(), - halign: HAlign::Left, - valign: VAlign::Top, - }; - text.render(&Rect { - x: rect.x, - y: rect.y + i as f32, - w: rect.w, - h: 1.0, - }); +impl ItemList { + pub fn new(items: Vec) -> Self { + Self { + items, + cursor: 0, + scroll: 0, + } + } - if i as f32 >= rect.h { - break; + pub fn up(&mut self) { + if self.cursor > 0 { + self.cursor -= 1; + } + } + + pub fn down(&mut self) { + if self.cursor < self.items.len() - 1 { + self.cursor += 1; + } + } + + pub fn sync_scroll(&mut self, h: usize) { + if self.cursor >= self.scroll + h { + self.scroll = self.cursor - h + 1; + } else if self.cursor < self.scroll { + self.scroll = self.cursor; + } + } + + // TODO(#8): Operations to insert new items into the ItemList + // TODO(#9): Operations to remove items from ItemList +} + +// TODO(#10): EditField is not implemented + +impl Widget for ItemList { + fn render(&mut self, rect: &Rect) { + let h = rect.h.floor() as usize; + if h > 0 { + self.sync_scroll(h); + for i in 0..h { + if self.scroll + i < self.items.len() { + let mut text = Text { + text: self.items[i + self.scroll].to_string(), + halign: HAlign::Left, + valign: VAlign::Top, + }; + + let selected = i + self.scroll == self.cursor; + let color_pair = if selected { + style::CURSOR_PAIR + } else { + style::REGULAR_PAIR + }; + + attron(COLOR_PAIR(color_pair)); + text.render(&Rect { + x: rect.x, + y: rect.y + i as f32, + w: rect.w, + h: 1.0, + }); + attroff(COLOR_PAIR(color_pair)); + } } } } @@ -179,22 +264,6 @@ pub fn screen_rect() -> Rect { } } -pub fn text(text: &str) -> Box { - Box::new(Text { - text: text.to_string(), - halign: HAlign::Left, - valign: VAlign::Top, - }) -} - -pub fn hbox(widgets: Vec>) -> Box { - Box::new(HBox { widgets }) -} - -pub fn vbox(widgets: Vec>) -> Box { - Box::new(VBox { widgets }) -} - static QUIT: AtomicBool = AtomicBool::new(false); pub fn quit() { @@ -204,6 +273,13 @@ pub fn quit() { pub fn exec(mut ui: Box) { initscr(); + start_color(); + init_pair(style::REGULAR_PAIR, COLOR_WHITE, COLOR_BLACK); + init_pair(style::CURSOR_PAIR, COLOR_BLACK, COLOR_WHITE); + init_pair(style::UNFOCUSED_CURSOR_PAIR, COLOR_BLACK, COLOR_CYAN); + + curs_set(CURSOR_INVISIBLE); + set_hook(Box::new({ let default_hook = take_hook(); move |payload| { @@ -222,25 +298,24 @@ pub fn exec(mut ui: Box) { endwin(); } -pub struct Proxy { - pub root: Box, - pub handler: fn(&Event), +pub struct Proxy { + pub root: T, + pub handler: fn(&mut T, &Event), } -impl Proxy { - pub fn wrap(handler: fn(&Event), root: Box) -> Box { +impl Proxy { + pub fn wrap(handler: fn(&mut T, &Event), root: T) -> Box { Box::new(Self { root, handler }) } } -impl Widget for Proxy { - fn render(&self, rect: &Rect) { - self.root.render(rect) +impl Widget for Proxy { + fn render(&mut self, rect: &Rect) { + self.root.render(rect); } fn handle_event(&mut self, event: &Event) { - (self.handler)(event); - self.root.handle_event(event); + (self.handler)(&mut self.root, event); } } diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 0000000..a29c489 --- /dev/null +++ b/src/style.rs @@ -0,0 +1,12 @@ +use ncurses::*; + +pub const REGULAR_PAIR: i16 = 1; +pub const CURSOR_PAIR: i16 = 2; +pub const UNFOCUSED_CURSOR_PAIR: i16 = 3; + +pub fn init_style() { + start_color(); + init_pair(REGULAR_PAIR, COLOR_WHITE, COLOR_BLACK); + init_pair(CURSOR_PAIR, COLOR_BLACK, COLOR_WHITE); + init_pair(UNFOCUSED_CURSOR_PAIR, COLOR_BLACK, COLOR_CYAN); +}