├── .envrc ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── assets ├── demo.png ├── hstack.png ├── section.png ├── vstack.png └── watch.png ├── changelog.md ├── examples ├── Cargo.toml ├── examples │ └── watch.rs ├── readme.md └── src │ └── lib.rs ├── intuitive ├── Cargo.toml ├── notes.md ├── readme.md └── src │ ├── components │ ├── any.rs │ ├── centered.rs │ ├── children.rs │ ├── embed.rs │ ├── empty.rs │ ├── experimental_components │ │ ├── input.rs │ │ ├── mod.rs │ │ ├── modal │ │ │ ├── hook.rs │ │ │ └── mod.rs │ │ ├── scroll │ │ │ └── mod.rs │ │ └── table │ │ │ ├── alignment.rs │ │ │ ├── mod.rs │ │ │ └── widget.rs │ ├── mod.rs │ ├── section.rs │ ├── stack │ │ ├── flex.rs │ │ ├── horizontal.rs │ │ ├── mod.rs │ │ └── vertical.rs │ └── text │ │ └── mod.rs │ ├── element.rs │ ├── error.rs │ ├── event │ ├── channel.rs │ ├── handler.rs │ └── mod.rs │ ├── lib.rs │ ├── state │ ├── hook.rs │ ├── manager.rs │ ├── mod.rs │ └── state.rs │ ├── style.rs │ ├── terminal.rs │ └── text.rs ├── macros ├── Cargo.toml ├── readme.md └── src │ ├── component.rs │ ├── lib.rs │ ├── on_key.rs │ ├── render.rs │ └── utils.rs ├── readme.md └── shell.nix /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv 2 | 3 | target/ 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Crate" 3 | max_width = 140 4 | reorder_imports = true 5 | tab_spaces = 2 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "intuitive", 5 | "macros", 6 | "examples" 7 | ] 8 | -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enricozb/intuitive/c7428c7c137f45ad152dc2f81823272361675deb/assets/demo.png -------------------------------------------------------------------------------- /assets/hstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enricozb/intuitive/c7428c7c137f45ad152dc2f81823272361675deb/assets/hstack.png -------------------------------------------------------------------------------- /assets/section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enricozb/intuitive/c7428c7c137f45ad152dc2f81823272361675deb/assets/section.png -------------------------------------------------------------------------------- /assets/vstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enricozb/intuitive/c7428c7c137f45ad152dc2f81823272361675deb/assets/vstack.png -------------------------------------------------------------------------------- /assets/watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enricozb/intuitive/c7428c7c137f45ad152dc2f81823272361675deb/assets/watch.png -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 0.6.2 2 | - better `KeyHandler` docs 3 | - bring `Propagate` into scope in `on_key!` macro 4 | 5 | # 0.6.1 6 | - fix relative links in docs 7 | - add docs for generics in `#[component]` 8 | 9 | # 0.6.0 10 | - remove unstable feature 11 | - add experimental `Scroll` and `Input` components 12 | - add `Span`, `Spans`, and `Lines` for text styling 13 | - add generics to `#[component]` attribute macro 14 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "intuitive_examples" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | chrono = "0.4.22" 9 | clap = { version = "3.2.20", features = ["derive"] } 10 | intuitive = { path = "../intuitive", features = ["experimental"] } 11 | -------------------------------------------------------------------------------- /examples/examples/watch.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | process::{Command, Output}, 3 | thread, 4 | time::Duration, 5 | }; 6 | 7 | use chrono::Local; 8 | use clap::Parser; 9 | use intuitive::{ 10 | component, 11 | components::{stack::Flex::*, HStack, Section, Text, VStack}, 12 | error::Result, 13 | on_key, render, 14 | state::State, 15 | style::Color, 16 | terminal::Terminal, 17 | text::Span, 18 | }; 19 | 20 | #[component(Top)] 21 | fn render(interval: u64, command: String) { 22 | let date = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); 23 | 24 | render! { 25 | HStack(flex: [Block(10), Grow(1), Block(21)]) { 26 | Section(title: "Every", border: Color::DarkGray) { 27 | Text(text: format!("{}s", interval)) 28 | } 29 | Section(title: "Command", border: Color::DarkGray) { 30 | Text(text: command) 31 | } 32 | Section(title: "Time", border: Color::DarkGray) { 33 | Text(text: date) 34 | } 35 | } 36 | } 37 | } 38 | 39 | #[component(CommandOutput)] 40 | fn render(output: State>) { 41 | let text = output.inspect(|output| match output { 42 | Some(output) => { 43 | if output.status.success() { 44 | Span::new(String::from_utf8_lossy(&output.stdout), Color::White) 45 | } else { 46 | Span::new(String::from_utf8_lossy(&output.stderr), Color::Red) 47 | } 48 | } 49 | 50 | None => Span::new(".. waiting for command to finish ..", Color::Gray), 51 | }); 52 | 53 | render! { 54 | Text(text) 55 | } 56 | } 57 | 58 | #[component(Root)] 59 | fn render(args: Args, output: State>) { 60 | let on_key = on_key! { 61 | KeyEvent { code: Char('q'), .. } => event::quit() 62 | }; 63 | 64 | render! { 65 | VStack(flex: [Block(3), Grow(1)], on_key) { 66 | Top(interval: args.interval, command: args.command.clone()) 67 | CommandOutput(output: output.clone()) 68 | } 69 | } 70 | } 71 | 72 | #[derive(Parser, Debug, Default, Clone)] 73 | #[clap(author, version, about, long_about = None)] 74 | struct Args { 75 | #[clap(short = 'n', long)] 76 | interval: u64, 77 | 78 | #[clap(value_parser)] 79 | command: String, 80 | } 81 | 82 | fn main() -> Result<()> { 83 | let args = Args::parse(); 84 | let output = State::>::default(); 85 | 86 | { 87 | let output = output.clone(); 88 | let args = args.clone(); 89 | 90 | thread::spawn(move || { 91 | let mut cmd = Command::new("sh"); 92 | let cmd = cmd.args(["-c".to_string(), args.command]); 93 | 94 | let interval = Duration::from_secs(args.interval); 95 | 96 | loop { 97 | output.set(Some(cmd.output().expect("cmd::output panicked"))); 98 | 99 | thread::sleep(interval); 100 | } 101 | }); 102 | } 103 | 104 | Terminal::new(Root::new(args, output))?.run() 105 | } 106 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # [Intuitive] Examples 2 | 3 | This crate is a set of examples of basic TUI applications using Intuitive. 4 | They are run using `cargo run --example -- [args]`. 5 | 6 | ## Examples 7 | - [Watch] 8 | 9 | ### Watch 10 | Runs a program periodically, showing its output. The TUI is inspired by [viddy]. 11 | Press `q` to quit. Example: 12 | ```bash 13 | cargo run --example watch -- -n 1 'systemctl status network-interfaces.target' 14 | ``` 15 | ![watch](../assets/watch.png) 16 | 17 | [Intuitive]: https://github.com/enricozb/intuitive 18 | [viddy]: https://github.com/sachaos/viddy 19 | [Watch]: #watch 20 | -------------------------------------------------------------------------------- /examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An empty crate holding examples for the `intuitive` crate. 2 | -------------------------------------------------------------------------------- /intuitive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "intuitive" 3 | version = "0.6.3" 4 | description = "a library for building declarative text-based user interfaces" 5 | edition = "2021" 6 | license = "CC0-1.0" 7 | repository = "https://github.com/enricozb/intuitive" 8 | readme = "readme.md" 9 | 10 | [dependencies] 11 | crossterm = "0.25.0" 12 | ctrlc = { version = "3.2.3", features = [ "termination" ] } 13 | doc-cfg = "0.1" 14 | intuitive_macros = { path = "../macros", version = "0.6.2" } 15 | lazy_static = "1.4.0" 16 | parking_lot = "0.12.1" 17 | serial_test = "0.9" 18 | thiserror = "1.0.32" 19 | tui = "0.19.0" 20 | 21 | [features] 22 | unstable-doc-cfg = ["experimental"] 23 | experimental = [] 24 | 25 | [package.metadata.docs.rs] 26 | features = ["unstable-doc-cfg"] 27 | -------------------------------------------------------------------------------- /intuitive/notes.md: -------------------------------------------------------------------------------- 1 | # invariants 2 | - `State` cannot be a parameter to a struct implementing `Component` 3 | - this might only be true of the root component 4 | - `use_state` (and other hooks) can only be called in `render` 5 | - if you take children in your custom component you must call `render` on them when being rendered 6 | - frozen / rendred structs should only take in `AnyElement` not `AnyComponent` 7 | - all components should take in a `on_key` parameter 8 | - custom components must implement `Default` 9 | 10 | # todo 11 | - add a conditional component, or `if` syntax to `render!` 12 | - consider giving `KeyHandler` an additional parameter, specifically a struct that 13 | holds state that the `KeyHandler` can inspect. this could handle `onEnter` use 14 | case for input text boxes. 15 | - create a `use_memo` hook, as it 16 | 1. is probably useful 17 | 2. could serve as the primitive the `use_state` 18 | -------------------------------------------------------------------------------- /intuitive/readme.md: -------------------------------------------------------------------------------- 1 | ../readme.md -------------------------------------------------------------------------------- /intuitive/src/components/any.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Deref, sync::Arc}; 2 | 3 | use super::{Component, Empty}; 4 | 5 | /// An opaque type holding a struct that implements [`Component`]. 6 | /// 7 | /// `Any` is rarely used directly, even when implementing [`Component`]. 8 | /// It is typically indirectly used when a component receives children, 9 | /// through [`Children`]. 10 | /// 11 | /// An example of a component that requires `Any` is [`Modal`]. This is 12 | /// because it provides a function, [`Funcs::show`], that receives a component 13 | /// and presents it. 14 | /// 15 | /// [`Children`]: children/struct.Children.html 16 | /// [`Component`]: trait.Component.html 17 | /// [`Funcs::show`]: experimental/modal/struct.Funcs.html#method.show 18 | /// [`Modal`]: experimental/modal/struct.Modal.html 19 | #[derive(Clone)] 20 | pub struct Any(Arc); 21 | 22 | impl Any { 23 | fn new(component: C) -> Self { 24 | Any(Arc::new(component)) 25 | } 26 | } 27 | 28 | impl Default for Any { 29 | fn default() -> Self { 30 | Empty::new() 31 | } 32 | } 33 | 34 | impl Deref for Any { 35 | type Target = Arc; 36 | 37 | fn deref(&self) -> &Self::Target { 38 | &self.0 39 | } 40 | } 41 | 42 | impl From for Any { 43 | fn from(component: C) -> Self { 44 | Self::new(component) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /intuitive/src/components/centered.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | component, 3 | components::{children::Children, Embed, Empty, HStack, VStack}, 4 | event::KeyHandler, 5 | on_key, render, 6 | }; 7 | 8 | /// A component for centering its contents. 9 | /// 10 | /// For example, 11 | /// ```rust 12 | /// # use intuitive::{component, components::{Centered, Section}, render}; 13 | /// # 14 | /// #[component(Root)] 15 | /// fn render() { 16 | /// render! { 17 | /// Centered() { 18 | /// Section(title: "I'm centered") 19 | /// } 20 | /// } 21 | /// } 22 | /// ``` 23 | #[component(Centered)] 24 | pub fn render(children: Children<1>, on_key: KeyHandler) { 25 | let child = children[0].render(); 26 | 27 | let on_key = on_key! { [child, on_key] 28 | event => on_key.handle_or(event, |event| child.on_key(event)) 29 | }; 30 | 31 | render! { 32 | VStack(on_key) { 33 | Empty() 34 | HStack() { 35 | Empty() 36 | Embed(content: child) 37 | Empty() 38 | } 39 | Empty() 40 | } 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | use crate::{components::Text, element::Any as AnyElement, state::State}; 48 | 49 | #[test] 50 | fn centered_forwards_keys() { 51 | pub use crate::event::{KeyCode::*, KeyEvent, KeyModifiers}; 52 | 53 | let called = State::new(false); 54 | let on_key_called = called.clone(); 55 | 56 | let on_key = on_key! { 57 | KeyEvent { code: Esc, .. } => on_key_called.set(true), 58 | }; 59 | 60 | let centered: AnyElement = render! { 61 | Centered() { 62 | Text(on_key) 63 | } 64 | }; 65 | 66 | centered.on_key(KeyEvent::new(Esc, KeyModifiers::NONE)); 67 | 68 | assert_eq!(called.get(), true); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /intuitive/src/components/children.rs: -------------------------------------------------------------------------------- 1 | //! Structures for dealing with child components. 2 | 3 | use std::ops::Deref; 4 | 5 | use super::Any as AnyComponent; 6 | use crate::element::Any as AnyElement; 7 | 8 | /// An array of child components. 9 | /// 10 | /// The main purpose of `Children` is to provide a [`render`] method, 11 | /// and to implement [`Default`]. 12 | /// 13 | /// [`render`]: #method.render 14 | /// [`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html 15 | #[derive(Clone)] 16 | pub struct Children([AnyComponent; N]); 17 | 18 | impl Children { 19 | /// Render the components in the array. 20 | pub fn render(&self) -> [AnyElement; N] { 21 | let mut components = [(); N].map(|()| AnyElement::default()); 22 | 23 | for (i, component) in components.iter_mut().enumerate() { 24 | *component = self.0[i].render(); 25 | } 26 | 27 | components 28 | } 29 | } 30 | 31 | impl Default for Children { 32 | fn default() -> Self { 33 | Self([(); N].map(|_| AnyComponent::default())) 34 | } 35 | } 36 | 37 | impl Deref for Children { 38 | type Target = [AnyComponent; N]; 39 | 40 | fn deref(&self) -> &Self::Target { 41 | &self.0 42 | } 43 | } 44 | 45 | impl From<[AnyComponent; N]> for Children { 46 | fn from(children: [AnyComponent; N]) -> Self { 47 | Self(children) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /intuitive/src/components/embed.rs: -------------------------------------------------------------------------------- 1 | use crate::{component, components::Any as AnyComponent, element::Any as AnyElement}; 2 | 3 | pub enum Content { 4 | Component(AnyComponent), 5 | Element(AnyElement), 6 | } 7 | 8 | impl From for Content { 9 | fn from(component: AnyComponent) -> Self { 10 | Self::Component(component) 11 | } 12 | } 13 | 14 | impl From for Content { 15 | fn from(element: AnyElement) -> Self { 16 | Self::Element(element) 17 | } 18 | } 19 | 20 | impl Default for Content { 21 | fn default() -> Self { 22 | Self::Element(AnyElement::default()) 23 | } 24 | } 25 | 26 | /// A component that renders an [`element::Any`] or a [`component::Any`]. 27 | /// 28 | /// This is often needed when rendering children. More generally, `Embed` is 29 | /// useful when you have a variable that contains an [`element::Any`] or 30 | /// a [`component::Any`] and you want to insert it into a [`render!`] call. 31 | /// 32 | /// For example, 33 | /// ```rust 34 | /// # use intuitive::{component, components::{children::Children, HStack, VStack, Empty, Embed}, render}; 35 | /// # 36 | /// #[component(Centered)] 37 | /// pub fn render(children: Children<1>) { 38 | /// render! { 39 | /// VStack() { 40 | /// Empty() 41 | /// HStack() { 42 | /// Empty() 43 | /// Embed(content: children[0].clone()) 44 | /// Empty() 45 | /// } 46 | /// Empty() 47 | /// } 48 | /// } 49 | /// } 50 | /// ``` 51 | /// 52 | /// [`component::Any`]: struct.Any.html 53 | /// [`element::Any`]: ../element/struct.Any.html 54 | /// [`render!`]: ../macro.render.html 55 | #[component(Embed)] 56 | pub fn render(content: Content) -> AnyElement { 57 | match content { 58 | Content::Component(component) => component.render(), 59 | Content::Element(element) => Clone::clone(element), 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /intuitive/src/components/empty.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | component, 3 | element::{Any as AnyElement, Element}, 4 | }; 5 | 6 | /// A component that renders nothing. 7 | #[component(Empty)] 8 | pub fn render() -> AnyElement { 9 | AnyElement::new(Self {}) 10 | } 11 | 12 | impl Element for Empty {} 13 | -------------------------------------------------------------------------------- /intuitive/src/components/experimental_components/input.rs: -------------------------------------------------------------------------------- 1 | //! A module containing the `Input` component. 2 | 3 | use std::cmp; 4 | 5 | use tui::{text::Spans as TuiSpans, widgets::Paragraph}; 6 | 7 | use crate::{ 8 | component, 9 | components::{stack::Flex::*, Empty, Section, VStack}, 10 | element::{Any as AnyElement, Element}, 11 | event::{KeyHandler, MouseHandler}, 12 | on_key, render, 13 | state::use_state, 14 | style::Style, 15 | terminal::{Frame, Rect}, 16 | text::Spans, 17 | }; 18 | 19 | /// A single-line input component with cursor controls. 20 | /// 21 | /// This is a smart input box with the following features: 22 | /// - rendering a cursor 23 | /// - scrolling on overflow 24 | /// - supports navigating with arrow keys 25 | /// - supports navigating with `ctrl+a` and `ctrl+e` 26 | /// - has a fixed single-line height of 3 rows 27 | /// 28 | /// ## Vertical Alignment 29 | /// 30 | /// Since this component always renders as three blocks high, 31 | /// when more space is available it vertically centers itself. 32 | /// In order to align this element to the top, you will need 33 | /// to use [`VStack`] along with [`Flex::Block`] and properly 34 | /// route the key events: 35 | /// 36 | /// ```rust 37 | /// # use intuitive::{ 38 | /// # component, 39 | /// # components::{stack::Flex::*, Embed, Empty, experimental::input::Input, VStack}, 40 | /// # element, 41 | /// # error::Result, 42 | /// # on_key, render, 43 | /// # terminal::Terminal, 44 | /// # }; 45 | /// # 46 | /// #[component(Root)] 47 | /// fn render() { 48 | /// let input: element::Any = render! { 49 | /// Input(title: "Input Box") 50 | /// }; 51 | /// 52 | /// let on_key = on_key! { [input] 53 | /// KeyEvent { code: Esc, .. } => event::quit(), 54 | /// event => input.on_key(event), 55 | /// }; 56 | /// 57 | /// render! { 58 | /// VStack(flex: [Block(3), Grow(1)], on_key) { 59 | /// Embed(content: input) 60 | /// Empty() 61 | /// } 62 | /// } 63 | /// } 64 | /// ``` 65 | /// 66 | /// [`VStack`]: ../../struct.VStack.html 67 | /// [`Flex::Block`]: ../../stack/enum.Flex.html#variant.Block 68 | #[component(Input)] 69 | pub fn render(title: Spans, border: Style, on_key: KeyHandler, on_mouse: MouseHandler) { 70 | let cursor = use_state(|| 0usize); 71 | let text = use_state(|| String::new()); 72 | 73 | let on_key = on_key.then(on_key! { [cursor, text] 74 | KeyEvent { code: Char('a'), modifiers: KeyModifiers::CONTROL, .. } => cursor.set(0), 75 | KeyEvent { code: Char('e'), modifiers: KeyModifiers::CONTROL, .. } => cursor.set(text.get().len()), 76 | 77 | KeyEvent { code: Left, .. } => { 78 | cursor.update(|cursor| cursor.saturating_sub(1)); 79 | }, 80 | 81 | KeyEvent { code: Right, .. } => { 82 | cursor.update(|cursor| cmp::min(cursor + 1, text.get().len())); 83 | }, 84 | 85 | KeyEvent { code: Char(c), .. } => { 86 | text.mutate(|text| text.insert(cursor.get(), c)); 87 | cursor.update(|cursor| cursor + 1); 88 | }, 89 | 90 | KeyEvent { code: Backspace, .. } => { 91 | if cursor.get() > 0 && text.get().len() > 0 { 92 | text.mutate(|text| text.remove(cursor.get() - 1)); 93 | cursor.update(|cursor| cursor - 1); 94 | } 95 | }, 96 | }); 97 | 98 | // TODO(enricozb): consider adding (Vertical)Alignment as a property, that will 99 | // align the input box to the top/middle/bottom. 100 | render! { 101 | VStack(flex: [Grow(1), Block(3), Grow(1)], on_key) { 102 | Empty() 103 | Section(title, border, on_mouse) { 104 | Inner(cursor: cursor.get(), text: text.get()) 105 | } 106 | Empty() 107 | } 108 | } 109 | } 110 | 111 | #[component(Inner)] 112 | fn render(cursor: usize, text: String) { 113 | AnyElement::new(Frozen { 114 | cursor: *cursor as u16, 115 | text: text.clone(), 116 | }) 117 | } 118 | 119 | struct Frozen { 120 | cursor: u16, 121 | text: String, 122 | } 123 | 124 | impl Element for Frozen { 125 | fn draw(&self, rect: Rect, frame: &mut Frame) { 126 | let (text, cursor) = if self.cursor < rect.width { 127 | (self.text.clone().into(), rect.x + self.cursor) 128 | } else { 129 | let offset = (self.cursor - rect.width) as usize + 1; 130 | (self.text[offset..].into(), rect.right() - 1) 131 | }; 132 | 133 | let widget = Paragraph::new::(text); 134 | frame.render_widget(widget, rect); 135 | frame.set_cursor(cursor, rect.y); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /intuitive/src/components/experimental_components/mod.rs: -------------------------------------------------------------------------------- 1 | //! An experimental collection of components not subject to semver. 2 | //! 3 | //! Components in this crate are subject to change without guarantees from semver. 4 | //! This is a staging ground for potential future components. Furthermore, components 5 | //! here may or may not have any accompanying documentation. 6 | 7 | pub mod input; 8 | pub mod modal; 9 | pub mod scroll; 10 | pub mod table; 11 | -------------------------------------------------------------------------------- /intuitive/src/components/experimental_components/modal/hook.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use parking_lot::Mutex; 4 | 5 | use crate::{components::Any as AnyComponent, state::State}; 6 | 7 | static FUNCS: Mutex> = Mutex::new(None); 8 | 9 | /// A structure returned by [`use_modal`] that controls the hiding/showing of a modal. 10 | /// 11 | /// [`use_modal`]: fn.use_modal.html 12 | #[derive(Clone)] 13 | pub struct Funcs { 14 | modal: State>, 15 | 16 | show: Arc, 17 | hide: Arc, 18 | } 19 | 20 | impl Funcs { 21 | pub(crate) fn new(modal: State>) -> Self { 22 | let show_modal = modal.clone(); 23 | let hide_modal = modal.clone(); 24 | 25 | Self { 26 | modal, 27 | show: Arc::new(move |component| show_modal.set(Some(component))), 28 | hide: Arc::new(move || hide_modal.set(None)), 29 | } 30 | } 31 | 32 | /// Return whether if the modal is shown. 33 | pub fn is_shown(&self) -> bool { 34 | self.modal.inspect(Option::is_some) 35 | } 36 | 37 | /// Set `component` to be shown by the modal. 38 | pub fn show(&self, component: AnyComponent) { 39 | (self.show)(component); 40 | } 41 | 42 | /// Hide the showing modal, if any. 43 | pub fn hide(&self) { 44 | (self.hide)(); 45 | } 46 | } 47 | 48 | /// A hook that can control the hiding/showing of a modal. 49 | /// 50 | /// Like [`use_state`], calls to `use_modal` may only be within a call to 51 | /// [`Component::render`]. Unlike [`use_state`], calls to `use_modal` may only be within 52 | /// a component that is a child component of some [`Modal`]. The [`Funcs`] returned by 53 | /// `use_modal` will then refer to the nearest ancestor [`Modal`]. For example, if we 54 | /// have the following layout: 55 | /// ```rust 56 | /// # use intuitive::{render, component, components::{Empty, experimental::modal::{use_modal, Modal}}}; 57 | /// # 58 | /// #[component(MyComponent)] 59 | /// fn render() { 60 | /// let modal = use_modal(); 61 | /// 62 | /// render! { 63 | /// Empty() 64 | /// } 65 | /// } 66 | /// 67 | /// #[component(Root)] 68 | /// fn render() { 69 | /// render! { 70 | /// Modal() { // modal 1 71 | /// Modal() { // modal 2 72 | /// Modal() { // modal 3 73 | /// MyComponent() 74 | /// } 75 | /// } 76 | /// } 77 | /// } 78 | /// } 79 | /// ``` 80 | /// and `use_modal` is called within `MyComponent`, then it will return a [`Funcs`] struct 81 | /// that acts on `modal 3`. The other two ancestor modals are inaccessible. 82 | /// 83 | /// [`Component::render`]: trait.Component.html#tymethod.render 84 | /// [`Modal`]: struct.Modal.html 85 | /// [`Funcs`]: struct.Funcs.html 86 | /// [`use_state`]: ../../state/fn.use_state.html 87 | pub fn use_modal() -> Funcs { 88 | FUNCS 89 | .lock() 90 | .clone() 91 | .expect("use modal called outside of a Modal or outside of render") 92 | } 93 | 94 | pub fn set_modal_funcs(funcs: Funcs) { 95 | *FUNCS.lock() = Some(funcs); 96 | } 97 | -------------------------------------------------------------------------------- /intuitive/src/components/experimental_components/modal/mod.rs: -------------------------------------------------------------------------------- 1 | //! A module containing the `Modal` component and related hooks. 2 | //! 3 | //! The [`Modal`] component allows a user to present a [`Component`] on top of 4 | //! the children of the modal, from anywhere inside it. This is useful for popups such 5 | //! as input boxes, or error messages. See the [`Modal`] structure ocumentation for details. 6 | //! 7 | //! [`Modal`]: struct.Modal.html 8 | 9 | mod hook; 10 | 11 | pub use self::hook::{use_modal, Funcs}; 12 | use crate::{ 13 | components::{children::Children, Component}, 14 | element::{Any as AnyElement, Element}, 15 | event::{KeyEvent, KeyHandler}, 16 | state::use_state, 17 | terminal::{Frame, Rect}, 18 | }; 19 | 20 | /// A component supporting modal-style overlays. 21 | /// 22 | /// The [`Modal`] component allows a user to present a [`Component`] on top of 23 | /// the children of the modal, from anywhere inside it. This is useful for popups such 24 | /// as input boxes, or error messages. 25 | /// 26 | /// For example, if we wanted to show an error message 27 | /// whenever the `Enter` key is pressed, we can do something like this: 28 | /// ```rust 29 | /// # use intuitive::{component, components::{Centered, Section, Text, experimental::modal::{use_modal, Modal}}, style::Color, render, on_key}; 30 | /// # 31 | /// #[component(MyComponent)] 32 | /// fn render() { 33 | /// let modal = use_modal(); 34 | /// 35 | /// let on_key = on_key! { 36 | /// KeyEvent { code: Enter, .. } => modal.show(render! { 37 | /// Centered() { 38 | /// Section(title: "Error", border: Color::Red) { 39 | /// Text(text: "Enter was pressed!") 40 | /// } 41 | /// } 42 | /// }), 43 | /// 44 | /// KeyEvent { code: Esc, .. } if modal.is_shown() => modal.hide(), 45 | /// KeyEvent { code: Esc, .. } => event::quit(), 46 | /// }; 47 | /// 48 | /// render! { 49 | /// Section(title: "Some Example UI", on_key) 50 | /// } 51 | /// } 52 | /// 53 | /// #[component(Root)] 54 | /// fn render() { 55 | /// render! { 56 | /// Modal() { 57 | /// MyComponent() 58 | /// } 59 | /// } 60 | /// } 61 | /// 62 | /// ``` 63 | /// In order to overlay an error message on top of `MyComponent`, we render it 64 | /// as a child of a [`Modal`]. Then, in any descendant of this [`Modal`], we can call 65 | /// [`use_modal`] to mutate the state of that [`Modal`]. 66 | /// 67 | /// # Internals 68 | /// The [`Modal`] is somewhat special in that it does not (yet) use the built-in 69 | /// [`use_state`] hooks, but instead has its own internal `static` hook manager. 70 | /// 71 | /// [`Modal`]: struct.Modal.html 72 | /// [`Component`]: trait.Component.html 73 | /// [`use_state`]: ../../state/fn.use_state.html 74 | /// [`use_modal`]: fn.use_modal.html 75 | #[derive(Default)] 76 | pub struct Modal { 77 | pub children: Children<1>, 78 | pub on_key: KeyHandler, 79 | } 80 | 81 | impl Component for Modal { 82 | fn render(&self) -> AnyElement { 83 | let modal = use_state(|| None); 84 | let funcs = use_state(|| Funcs::new(modal.clone())); 85 | 86 | hook::set_modal_funcs(funcs.get()); 87 | 88 | AnyElement::new(Frozen { 89 | modal: modal.get().map(|modal| modal.render()), 90 | 91 | content: self.children[0].render(), 92 | on_key: self.on_key.clone(), 93 | }) 94 | } 95 | } 96 | 97 | struct Frozen { 98 | modal: Option, 99 | 100 | content: AnyElement, 101 | on_key: KeyHandler, 102 | } 103 | 104 | impl Element for Frozen { 105 | fn on_key(&self, event: KeyEvent) { 106 | self.on_key.handle_or(event, |event| self.content.on_key(event)); 107 | } 108 | 109 | fn draw(&self, rect: Rect, frame: &mut Frame) { 110 | self.content.draw(rect, frame); 111 | 112 | if let Some(modal) = &self.modal { 113 | modal.draw(rect, frame); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /intuitive/src/components/experimental_components/scroll/mod.rs: -------------------------------------------------------------------------------- 1 | //! A module containing the `Scroll` component. 2 | 3 | use std::cmp; 4 | 5 | use tui::{ 6 | buffer::Buffer, 7 | text::Spans as TuiSpans, 8 | widgets::{Block, Borders, Paragraph, Widget}, 9 | }; 10 | 11 | use crate::{ 12 | components::Component, 13 | element::{Any as AnyElement, Element}, 14 | event::{KeyEvent, KeyHandler, MouseEvent, MouseEventKind}, 15 | state::{use_state, State}, 16 | style::Style, 17 | terminal::{Frame, Rect}, 18 | text::{Lines, Spans}, 19 | }; 20 | 21 | /// A component that displays text along with a scrollbar. 22 | /// 23 | /// `Scroll` renders the `Spans` passed into it along with a scrollbar. 24 | /// 25 | /// [`Section`]: ../../struct.Section.html 26 | #[derive(Default)] 27 | pub struct Scroll { 28 | pub title: Spans, 29 | pub border: Style, 30 | pub text: Spans, 31 | 32 | pub on_key: KeyHandler, 33 | } 34 | 35 | impl Component for Scroll { 36 | fn render(&self) -> AnyElement { 37 | let buffer_offset = use_state(|| 0); 38 | 39 | AnyElement::new(Frozen { 40 | title: self.title.clone(), 41 | border: self.border, 42 | lines: self.text.clone().into(), 43 | on_key: self.on_key.clone(), 44 | 45 | buffer_offset, 46 | }) 47 | } 48 | } 49 | 50 | struct Frozen { 51 | title: Spans, 52 | lines: Lines, 53 | border: Style, 54 | on_key: KeyHandler, 55 | 56 | buffer_offset: State, 57 | } 58 | 59 | impl Frozen { 60 | fn scroll_height(&self, rect: Rect) -> u16 { 61 | let num_lines = self.lines.0.len(); 62 | let height = rect.height.saturating_sub(2) as usize; 63 | 64 | cmp::min(height, height * height / num_lines) as u16 65 | } 66 | 67 | fn max_buffer_offset(&self, rect: Rect) -> u16 { 68 | self.lines.0.len().saturating_sub(rect.height.saturating_sub(2) as usize) as u16 69 | } 70 | 71 | fn max_scroll_offset(&self, rect: Rect) -> u16 { 72 | rect.height.saturating_sub(2) - self.scroll_height(rect) 73 | } 74 | } 75 | 76 | impl Element for Frozen { 77 | fn on_key(&self, event: KeyEvent) { 78 | self.on_key.handle(event); 79 | } 80 | 81 | fn on_mouse(&self, rect: Rect, event: MouseEvent) { 82 | match event.kind { 83 | MouseEventKind::ScrollDown => self 84 | .buffer_offset 85 | .update(|offset| cmp::min(self.max_buffer_offset(rect), offset + 1)), 86 | MouseEventKind::ScrollUp => self.buffer_offset.update(|offset| offset.saturating_sub(1)), 87 | 88 | _ => (), 89 | } 90 | } 91 | 92 | fn draw(&self, rect: Rect, frame: &mut Frame) { 93 | frame.render_widget(self, rect); 94 | } 95 | } 96 | 97 | impl Widget for &Frozen { 98 | fn render(self, rect: Rect, buf: &mut Buffer) { 99 | let block = Block::default() 100 | .title::((&self.title).into()) 101 | .borders(Borders::ALL) 102 | .border_style(self.border.into()); 103 | 104 | let buffer_offset = self.buffer_offset.get(); 105 | 106 | let lines = self 107 | .lines 108 | .0 109 | .iter() 110 | .skip(buffer_offset as usize) 111 | .cloned() 112 | .map(TuiSpans::from) 113 | .collect(); 114 | 115 | // render text 116 | let paragraph = Paragraph::new::>(lines).block(block); 117 | Widget::render(paragraph, rect, buf); 118 | 119 | // render border & scroll-bar 120 | buf.set_string(rect.right() - 1, rect.top(), "▲", self.border.into()); 121 | buf.set_string(rect.right() - 1, rect.bottom() - 1, "▼", self.border.into()); 122 | 123 | let scroll_offset = self.max_scroll_offset(rect) * buffer_offset / self.max_buffer_offset(rect); 124 | 125 | for y in 1..=self.scroll_height(rect) { 126 | buf.set_string(rect.x + rect.width - 1, rect.y + y + scroll_offset, "█", self.border.into()); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /intuitive/src/components/experimental_components/table/alignment.rs: -------------------------------------------------------------------------------- 1 | /// The alignment of a column within a table. 2 | #[derive(Clone, Copy, PartialEq, Eq)] 3 | pub enum Alignment { 4 | Left, 5 | Right, 6 | } 7 | 8 | impl Default for Alignment { 9 | fn default() -> Self { 10 | Self::Left 11 | } 12 | } 13 | 14 | #[derive(Clone, Copy)] 15 | pub struct Array { 16 | alignments: [Alignment; N], 17 | } 18 | 19 | impl Default for Array { 20 | fn default() -> Self { 21 | Self { 22 | alignments: [(); N].map(|_| Alignment::default()), 23 | } 24 | } 25 | } 26 | 27 | impl From> for [Alignment; N] { 28 | fn from(array: Array) -> Self { 29 | array.alignments 30 | } 31 | } 32 | 33 | impl From<[Alignment; N]> for Array { 34 | fn from(alignments: [Alignment; N]) -> Self { 35 | Self { alignments } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /intuitive/src/components/experimental_components/table/mod.rs: -------------------------------------------------------------------------------- 1 | //! A module containing the `Table` component and related structures. 2 | 3 | mod alignment; 4 | mod widget; 5 | 6 | use tui::text::Spans as TuiSpans; 7 | 8 | pub use self::alignment::{Alignment, Array as AlignmentArray}; 9 | use self::widget::Table as TableWidget; 10 | use crate::{ 11 | components::Component, 12 | element::{Any as AnyElement, Element}, 13 | event::{KeyEvent, KeyHandler}, 14 | on_key, 15 | state::{use_state, State}, 16 | terminal::{Frame, Rect}, 17 | text::Spans, 18 | }; 19 | 20 | /// A component to render tabular data. 21 | #[derive(Default)] 22 | pub struct Table { 23 | pub alignments: AlignmentArray, 24 | pub rows: Vec<[Spans; N]>, 25 | 26 | pub on_key: KeyHandler, 27 | } 28 | 29 | impl Component for Table { 30 | fn render(&self) -> AnyElement { 31 | let index = use_state(|| 0); 32 | 33 | AnyElement::new(Frozen { 34 | alignments: self.alignments, 35 | rows: self.rows.clone(), 36 | index, 37 | 38 | on_key: self.on_key.clone(), 39 | }) 40 | } 41 | } 42 | 43 | struct Frozen { 44 | alignments: AlignmentArray, 45 | rows: Vec<[Spans; N]>, 46 | index: State, 47 | 48 | on_key: KeyHandler, 49 | } 50 | 51 | impl Element for Frozen { 52 | fn on_key(&self, event: KeyEvent) { 53 | self.on_key.handle_or( 54 | event, 55 | on_key! { 56 | KeyEvent { code: Char('j'), .. } => self.index.update(|i| if i + 1 < self.rows.len() { i + 1 } else { *i }), 57 | KeyEvent { code: Char('k'), .. } => self.index.update(|i| i.saturating_sub(1)), 58 | }, 59 | ); 60 | } 61 | 62 | fn draw(&self, rect: Rect, frame: &mut Frame) { 63 | let rows = self.rows.iter().cloned().map(|row| row.map(TuiSpans::from)).collect(); 64 | let widget = TableWidget::new(rows, self.alignments.into()); 65 | 66 | frame.render_widget(widget, rect); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /intuitive/src/components/experimental_components/table/widget.rs: -------------------------------------------------------------------------------- 1 | use tui::{ 2 | buffer::Buffer, 3 | layout::{Constraint, Rect}, 4 | style::{Modifier, Style}, 5 | text::{Span, Spans}, 6 | widgets::{Cell as TuiCell, Row, Table as TuiTable, Widget}, 7 | }; 8 | 9 | use super::alignment::Alignment; 10 | 11 | type Rows<'a, const N: usize> = Vec<[Spans<'a>; N]>; 12 | 13 | pub struct Table<'a, const N: usize> { 14 | pub rows: Rows<'a, N>, 15 | pub alignments: [Alignment; N], 16 | } 17 | 18 | impl<'a, const N: usize> Table<'a, N> { 19 | pub fn new(rows: Rows<'a, N>, alignments: [Alignment; N]) -> Self { 20 | Self { rows, alignments } 21 | } 22 | 23 | fn lengths(&self) -> Vec<[usize; N]> { 24 | self 25 | .rows 26 | .iter() 27 | .map(|row| { 28 | let mut lengths = [0; N]; 29 | for (i, cell) in row.iter().enumerate() { 30 | lengths[i] = Spans::width(cell); 31 | } 32 | 33 | lengths 34 | }) 35 | .collect() 36 | } 37 | 38 | #[allow(clippy::cast_possible_truncation)] 39 | fn constraints(&self) -> [Constraint; N] { 40 | let lengths: Vec<[usize; N]> = self.lengths(); 41 | let mut constraints = [0; N]; 42 | for i in 0..N { 43 | constraints[i] = lengths.iter().map(|l| l[i]).max().unwrap_or_default() as u16; 44 | } 45 | 46 | constraints.map(Constraint::Length) 47 | } 48 | 49 | fn aligned_rows(rows: Rows<'a, N>, alignments: &[Alignment; N], constraints: &[Constraint; N]) -> Vec> { 50 | rows 51 | .into_iter() 52 | .map(|mut row| { 53 | for i in 0..N { 54 | let (spans, alignment, constraint) = (&mut row[i], &alignments[i], constraints[i]); 55 | if *alignment == Alignment::Right { 56 | if let Constraint::Length(width) = constraint { 57 | spans.0.insert(0, Span::raw(" ".repeat((width as usize) - spans.width()))); 58 | } 59 | } 60 | } 61 | 62 | Row::new(row.into_iter().map(TuiCell::from).collect::>()) 63 | }) 64 | .collect() 65 | } 66 | 67 | fn widget(self, constraints: &'a [Constraint; N]) -> TuiTable<'a> { 68 | TuiTable::new(Self::aligned_rows(self.rows, &self.alignments, constraints)) 69 | .highlight_style(Style::default().add_modifier(Modifier::BOLD)) 70 | .widths(constraints) 71 | } 72 | } 73 | 74 | impl<'a, const N: usize> Widget for Table<'a, N> { 75 | fn render(self, area: Rect, buf: &mut Buffer) { 76 | let constraints = self.constraints(); 77 | 78 | Widget::render(self.widget(&constraints), area, buf); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /intuitive/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | //! A collection of basic components. 2 | //! 3 | //! This module contains two main things: 4 | //! - A [collection of commonly used components] 5 | //! - The [`Component`] trait 6 | //! 7 | //! # Components 8 | //! Components are the main building blocks of Intuitive TUIs. Most components 9 | //! can be built using the [`component` attribute macro]. For more complex components, 10 | //! such as those that require special handling when drawing, consider implementing 11 | //! the [`Component`] trait directly. 12 | //! 13 | //! [`Component`]: trait.Component.html 14 | //! [`component` attribute macro]: ../attr.component.html 15 | //! 16 | //! # Recipes 17 | //! The examples below are a few recipes for commonly constructed components. Also be 18 | //! sure to refer to the [examples] directory in the repository. These recipes exclude 19 | //! the `use` statements in order to shorten the code samples. 20 | //! - [Input Box] -- An input box 21 | //! - [Input Box With Cursor] -- An input box with a cursor 22 | //! - [Focus] -- How to focus on different sections 23 | //! 24 | //! ## Input Box 25 | //! An input box with state can easily be created with a functional component: 26 | //! ```rust 27 | //! # use intuitive::{component, components::{Section, Text}, on_key, state::use_state, render}; 28 | //! # 29 | //! #[component(Input)] 30 | //! fn render(title: String) { 31 | //! let text = use_state(|| String::new()); 32 | //! let on_key = on_key! { [text] 33 | //! KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 34 | //! KeyEvent { code: Backspace, .. } => text.mutate(|text| text.pop()), 35 | //! }; 36 | //! 37 | //! render! { 38 | //! Section(title) { 39 | //! Text(text: text.get(), on_key) 40 | //! } 41 | //! } 42 | //! } 43 | //! ``` 44 | //! 45 | //! ## Input Box With Cursor 46 | //! Drawing a cursor requires us to implement a custom [`Element`], 47 | //! specifically so we can control the drawing of the cursor. Notice that 48 | //! we use a functional component to return a custom [`element::Any`], instead 49 | //! of returning a [`render!`] invocation. 50 | //! ```rust 51 | //! # use intuitive::{ 52 | //! # component, 53 | //! # components::{Section, Text}, 54 | //! # element::{Any as AnyElement, Element}, 55 | //! # event::KeyEvent, 56 | //! # on_key, render, 57 | //! # state::use_state, 58 | //! # terminal::{Rect, Frame}, 59 | //! # }; 60 | //! # 61 | //! #[component(Input)] 62 | //! fn render(title: String) { 63 | //! let text = use_state(|| String::new()); 64 | //! 65 | //! let on_key = on_key! { [text] 66 | //! KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 67 | //! KeyEvent { code: Backspace, .. } => text.mutate(|text| text.pop()), 68 | //! }; 69 | //! 70 | //! AnyElement::new(Frozen { 71 | //! cursor: text.get().len() as u16, 72 | //! content: render! { 73 | //! Section(title: title.clone(), on_key) { 74 | //! Text(text: text.get()) 75 | //! } 76 | //! }, 77 | //! }) 78 | //! } 79 | //! 80 | //! struct Frozen { 81 | //! cursor: u16, 82 | //! content: AnyElement, 83 | //! } 84 | //! 85 | //! impl Element for Frozen { 86 | //! fn on_key(&self, event: KeyEvent) { 87 | //! self.content.on_key(event); 88 | //! } 89 | //! 90 | //! fn draw(&self, rect: Rect, frame: &mut Frame) { 91 | //! self.content.draw(rect, frame); 92 | //! frame.set_cursor(rect.x + self.cursor + 1, rect.y + 1); 93 | //! } 94 | //! } 95 | //! ``` 96 | //! 97 | //! ## Focus 98 | //! In order to implement focusing on specific sections, we need to construct the components 99 | //! to be focused on, specifically the three `Input`s manually, when rendering our `Root` component. 100 | //! Notice that we also call [`Component::render`] on those `Input`s, because we want to be 101 | //! able to delegate key events to them, depending on which is focused. Lastly, we use [`Embed`] 102 | //! in order to make use of a rendered component inside of the [`render!`] macro. 103 | //! 104 | //! ```rust 105 | //! # use intuitive::{ 106 | //! # component, 107 | //! # components::{Section, Text, VStack, Embed}, 108 | //! # element::{Any as AnyElement, Element}, 109 | //! # event::KeyEvent, 110 | //! # on_key, render, 111 | //! # state::use_state, 112 | //! # style::Color, 113 | //! # terminal::{Rect, Frame}, 114 | //! # }; 115 | //! # 116 | //! #[derive(Clone, Copy, PartialEq, Eq, Debug)] 117 | //! enum Focus { 118 | //! A, 119 | //! B, 120 | //! C, 121 | //! } 122 | //! 123 | //! #[component(Input)] 124 | //! fn render(title: String, focused: bool) { 125 | //! let text = use_state(|| String::new()); 126 | //! 127 | //! let color = if *focused { Color::Blue } else { Color::Gray }; 128 | //! 129 | //! let on_key = on_key! { [text] 130 | //! KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 131 | //! KeyEvent { code: Backspace, .. } => text.mutate(|text| text.pop()), 132 | //! }; 133 | //! 134 | //! render! { 135 | //! Section(title, border: color) { 136 | //! Text(text: text.get(), on_key) 137 | //! } 138 | //! } 139 | //! } 140 | //! 141 | //! #[component(Root)] 142 | //! fn render() { 143 | //! let focus = use_state(|| Focus::A); 144 | //! 145 | //! let input_a = Input::new("A".to_string(), focus.get() == Focus::A).render(); 146 | //! let input_b = Input::new("B".to_string(), focus.get() == Focus::B).render(); 147 | //! let input_c = Input::new("C".to_string(), focus.get() == Focus::C).render(); 148 | //! 149 | //! let on_key = on_key! { [focus, input_a, input_b, input_c] 150 | //! KeyEvent { code: Tab, .. } => focus.update(|focus| match focus { 151 | //! Focus::A => Focus::B, 152 | //! Focus::B => Focus::C, 153 | //! Focus::C => Focus::A, 154 | //! }), 155 | //! 156 | //! event if focus.get() == Focus::A => input_a.on_key(event), 157 | //! event if focus.get() == Focus::B => input_b.on_key(event), 158 | //! event if focus.get() == Focus::C => input_c.on_key(event), 159 | //! 160 | //! KeyEvent { code: Esc, .. } => event::quit(), 161 | //! }; 162 | //! 163 | //! render! { 164 | //! VStack(on_key) { 165 | //! Embed(content: input_a) 166 | //! Embed(content: input_b) 167 | //! Embed(content: input_c) 168 | //! } 169 | //! } 170 | //! } 171 | //! ``` 172 | //! 173 | //! [collection of commonly used components]: #structs 174 | //! [examples]: https://github.com/enricozb/intuitive/tree/main/examples 175 | //! [`Component`]: trait.Component.html 176 | //! [`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html 177 | //! [`element::Any`]: ../element/struct.Any.html 178 | //! [`Element`]: ../element/trait.Element.html 179 | //! [`Embed`]: struct.Embed.html 180 | //! [Focus]: #focus 181 | //! [Input Box]: #input-box 182 | //! [Input Box With Cursor]: #input-box-with-cursor 183 | //! [`render!`]: ../macro.render.html 184 | //! [`Component::render`]: #tymethod.render 185 | 186 | pub mod children; 187 | pub mod stack; 188 | 189 | mod experimental_components; 190 | #[doc_cfg::doc_cfg(feature = "experimental")] 191 | pub mod experimental { 192 | //! An experimental collection of components not subject to semver. 193 | //! 194 | //! Components in this crate are subject to change without guarantees from semver. 195 | //! This is a staging ground for potential future components. Furthermore, components 196 | //! here may or may not have any accompanying documentation. 197 | 198 | pub use super::experimental_components::*; 199 | } 200 | 201 | mod any; 202 | mod centered; 203 | mod embed; 204 | mod empty; 205 | mod section; 206 | mod text; 207 | 208 | pub use self::{ 209 | any::Any, 210 | centered::Centered, 211 | embed::Embed, 212 | empty::Empty, 213 | section::Section, 214 | stack::{horizontal::Stack as HStack, vertical::Stack as VStack}, 215 | text::Text, 216 | }; 217 | use crate::element::Any as AnyElement; 218 | 219 | /// A trait describing structures that can be rendered to an [`Element`]. 220 | /// 221 | /// Before implementing the `Component` trait directly, make sure that what you are trying 222 | /// to do can't be done through the [`component` attribute macro], as there are 223 | /// a few nuances and implicit requirements when implementing `Component`. 224 | /// 225 | /// # Implementing `Component` 226 | /// The general idea behind the `Component` trait is that it orchestrates the construction 227 | /// of [`Element`]s. [`Element`]s know how to be drawn and how to handle keys. 228 | /// 229 | /// Before implementing component, it's important to understand the implicit 230 | /// expectations that Intuitive makes when rendering components. 231 | /// 232 | /// ## Invariants & Expectations 233 | /// 1. When rendering a frame, [`Component::render`] must be called on every `Component`, even if it 234 | /// is not being drawn this frame. 235 | /// - This is to ensure that hooks, such as [`use_state`], are always called in the 236 | /// same order. 237 | /// - This can typically be guaranteed by always calling [`Component::render`] 238 | /// on your component's children. 239 | /// 2. [`Component::render`] must never be called outside of [`Component::render`]. This is to 240 | /// continue the assurances made in the previous point. 241 | /// 3. Structures implementing `Component`, must also implement `Default`. 242 | /// 4. Structures implementing `Component` must have all of their fields public. 243 | /// 5. Structures implementing `Component` _should_ have an `on_key` parameter if they also 244 | /// take in `children`. This `on_key` parameter should be of type [`KeyHandler`] and 245 | /// should default to forwarding the key events to the children. 246 | /// 247 | /// Refer to the [`Section` component source] as an example component that 248 | /// adheres to these invariants. 249 | /// 250 | /// ## Custom Appearance 251 | /// In order to customize how a component is drawn by the [`Terminal`], you must 252 | /// create a struct that implements [`Element`]. This is typically done by 253 | /// creating two structs, one that implements `Component`, and a "frozen" struct 254 | /// that implements [`Element`], and the one implementing `Component` returns the 255 | /// custom [`Element`] on [`Component::render`]. 256 | /// 257 | /// Typically, when a component accepts [`Children`] and returns a custom [`Element`], 258 | /// the "frozen" structure that is constructed takes in `[AnyElement; N]` as its 259 | /// children, because [`Component::render`] was called on the [`Children`]. Again, 260 | /// refer to the [`Section` component source] that also returns a custom [`Element`]. 261 | /// 262 | /// [`component` attribute macro]: ../attr.component.html 263 | /// [`Terminal`]: ../terminal/struct.Terminal.html 264 | /// [`Component::render`]: #tymethod.render 265 | /// [`Element`]: ../element/trait.Element.html 266 | /// [`KeyHandler`]: ../event/struct.KeyHandler.html 267 | /// [`use_state`]: ../state/fn.use_state.html 268 | /// [`Section` component source]: ../../src/intuitive/components/section.rs.html 269 | /// [`Children`]: children/struct.Children.html 270 | pub trait Component { 271 | fn render(&self) -> AnyElement; 272 | } 273 | -------------------------------------------------------------------------------- /intuitive/src/components/section.rs: -------------------------------------------------------------------------------- 1 | use tui::{ 2 | text::Spans as TuiSpans, 3 | widgets::{Block, Borders}, 4 | }; 5 | 6 | use crate::{ 7 | component, 8 | components::children::Children, 9 | element::{Any as AnyElement, Element}, 10 | event::{KeyEvent, KeyHandler, MouseEvent, MouseHandler}, 11 | style::Style, 12 | terminal::{Frame, Rect}, 13 | text::Spans, 14 | }; 15 | 16 | /// A component with a border and a title. 17 | /// 18 | /// `Section` is used to wrap a component with a border and a title. 19 | /// For example, 20 | /// ```rust 21 | /// # use intuitive::{component, components::{Section, Text}, render}; 22 | /// # 23 | /// #[component(Root)] 24 | /// fn render() { 25 | /// render! { 26 | /// Section(title: "Input Box") { 27 | /// Text(text: "Hi there!") 28 | /// } 29 | /// } 30 | /// } 31 | /// ``` 32 | /// Will render the following: 33 | /// 34 | /// ![section](https://raw.githubusercontent.com/enricozb/intuitive/main/assets/section.png) 35 | /// 36 | /// `Section` also accepts a border [`Style`]. This style will merge with any style applied to 37 | /// the title. 38 | /// 39 | /// [`Style`]: ../style/struct.Style.html 40 | #[component(Section)] 41 | pub fn render(title: Spans, border: Style, children: Children<1>, on_key: KeyHandler, on_mouse: MouseHandler) { 42 | AnyElement::new(Frozen { 43 | title: title.clone(), 44 | border: *border, 45 | 46 | content: children[0].render(), 47 | on_key: on_key.clone(), 48 | on_mouse: on_mouse.clone(), 49 | }) 50 | } 51 | 52 | struct Frozen { 53 | title: Spans, 54 | border: Style, 55 | 56 | content: AnyElement, 57 | on_key: KeyHandler, 58 | on_mouse: MouseHandler, 59 | } 60 | 61 | impl Element for Frozen { 62 | fn on_key(&self, event: KeyEvent) { 63 | self.on_key.handle_or(event, |event| self.content.on_key(event)); 64 | } 65 | 66 | fn on_mouse(&self, rect: Rect, event: MouseEvent) { 67 | self.on_mouse.handle_or(event, |event| { 68 | self.content.on_mouse( 69 | Rect { 70 | x: rect.x + 1, 71 | y: rect.y - 1, 72 | width: rect.width - 1, 73 | height: rect.height - 1, 74 | }, 75 | event, 76 | ); 77 | }); 78 | } 79 | 80 | fn draw(&self, rect: Rect, frame: &mut Frame) { 81 | let block = Block::default() 82 | .title::((&self.title).into()) 83 | .borders(Borders::ALL) 84 | .border_style(self.border.into()); 85 | 86 | self.content.draw(block.inner(rect), frame); 87 | frame.render_widget(block, rect); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /intuitive/src/components/stack/flex.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | /// Control how much each child of a stack component grows. 4 | /// 5 | /// For example: 6 | /// ```rust 7 | /// # use intuitive::{component, components::{Section, VStack}, render}; 8 | /// # 9 | /// #[component(Root)] 10 | /// fn render() { 11 | /// render! { 12 | /// VStack(flex: [1, 2, 3]) { 13 | /// Section(title: "small") 14 | /// Section(title: "medium") 15 | /// Section(title: "large") 16 | /// } 17 | /// } 18 | /// } 19 | /// ``` 20 | /// will render a vertical stack of three [`Section`] components. The bottom one 21 | /// will be 3 times the height of the top one, and the middle one will be 2 times the height 22 | /// of the top one, as shown in the [`VStack`] docs. 23 | /// 24 | /// When using the `flex` parameter to [`VStack`] and [`HStack`], providing a value 25 | /// of type `[u16; N]`, will assume that [`Flex::Grow`] is intended, therefore making 26 | /// all dimensions relative. In order to have absolute height or width for a child, provide a 27 | /// value of type `[Flex; N]` to the `flex` parameter. For example, 28 | /// ```rust 29 | /// # use intuitive::{component, components::{Section, VStack, stack::Flex::*}, render}; 30 | /// # 31 | /// #[component(Root)] 32 | /// fn render() { 33 | /// render! { 34 | /// VStack(flex: [Block(3), Grow(1), Block(3)]) { 35 | /// Section(title: "absolute") 36 | /// Section(title: "relative") 37 | /// Section(title: "absolute") 38 | /// } 39 | /// } 40 | /// } 41 | /// ``` 42 | /// 43 | /// [`HStack`]: ../struct.HStack.html 44 | /// [`Section`]: ../struct.Section.html 45 | /// [`VStack`]: ../struct.VStack.html 46 | /// [`Flex::Grow`]: #variant.Grow 47 | #[derive(Clone, Copy)] 48 | pub enum Flex { 49 | /// An absolute amount of height or width. 50 | Block(u16), 51 | /// A relative amount of height or width. 52 | Grow(u16), 53 | } 54 | 55 | /// An array of [`Flex`] values. 56 | /// 57 | /// This struct exists in order to implement `From<[Flex; N]>` and 58 | /// `From<[u16; N]>`. 59 | /// 60 | /// [`Flex`]: enum.Flex.html 61 | #[derive(Clone, Copy)] 62 | pub struct Array { 63 | flex: [Flex; N], 64 | } 65 | 66 | impl From<[u16; N]> for Array { 67 | fn from(flex: [u16; N]) -> Self { 68 | Self { 69 | flex: flex.map(Flex::Grow), 70 | } 71 | } 72 | } 73 | 74 | impl From<[Flex; N]> for Array { 75 | fn from(flex: [Flex; N]) -> Self { 76 | Self { flex } 77 | } 78 | } 79 | 80 | impl Deref for Array { 81 | type Target = [Flex; N]; 82 | 83 | fn deref(&self) -> &Self::Target { 84 | &self.flex 85 | } 86 | } 87 | 88 | impl Default for Array { 89 | fn default() -> Self { 90 | Self { flex: [Flex::Grow(1); N] } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /intuitive/src/components/stack/horizontal.rs: -------------------------------------------------------------------------------- 1 | use tui::layout::{Constraint, Direction, Layout}; 2 | 3 | use super::{Flex, FlexArray}; 4 | use crate::{ 5 | component, 6 | components::children::Children, 7 | element::{Any as AnyElement, Element}, 8 | event::{self, KeyEvent, KeyHandler, MouseEvent, MouseHandler}, 9 | terminal::{Frame, Rect}, 10 | }; 11 | 12 | /// A component that for renders a horizontal stack of components. 13 | /// 14 | /// The `flex` argument specifies the amount of space allocated to each child, similar 15 | /// to the [`flex` css property]. See the [`FlexArray`] documentation for details. 16 | /// 17 | /// An example usage would be, 18 | /// ```rust 19 | /// # use intuitive::{component, components::{HStack, Section}, render}; 20 | /// # 21 | /// #[component(Root)] 22 | /// fn render() { 23 | /// render! { 24 | /// HStack(flex: [1, 2, 3]) { 25 | /// Section(title: "Left") 26 | /// Section(title: "Middle") 27 | /// Section(title: "Right") 28 | /// } 29 | /// } 30 | /// } 31 | /// ``` 32 | /// Will render the following: 33 | /// 34 | /// ![hstack](https://raw.githubusercontent.com/enricozb/intuitive/main/assets/hstack.png) 35 | /// 36 | /// [`flex` css property]: https://developer.mozilla.org/en-US/docs/Web/CSS/flex 37 | /// [`FlexArray`]: struct.FlexArray.html 38 | #[component(Stack)] 39 | pub fn render(flex: FlexArray, children: Children, on_key: KeyHandler, on_mouse: MouseHandler) { 40 | AnyElement::new(Frozen { 41 | flex: *flex, 42 | 43 | children: children.render(), 44 | on_key: on_key.clone(), 45 | on_mouse: on_mouse.clone(), 46 | }) 47 | } 48 | 49 | struct Frozen { 50 | flex: FlexArray, 51 | 52 | children: [AnyElement; N], 53 | on_key: KeyHandler, 54 | on_mouse: MouseHandler, 55 | } 56 | 57 | impl Frozen { 58 | fn layout(&self, rect: Rect) -> Vec { 59 | let total_grow: u16 = self 60 | .flex 61 | .iter() 62 | .map(|flex| match flex { 63 | Flex::Grow(grow) => *grow, 64 | Flex::Block(_) => 0, 65 | }) 66 | .sum(); 67 | 68 | let total_px: u16 = self 69 | .flex 70 | .iter() 71 | .map(|flex| match flex { 72 | Flex::Block(px) => *px, 73 | Flex::Grow(_) => 0, 74 | }) 75 | .sum(); 76 | 77 | let grow_px = rect.width - total_px; 78 | 79 | Layout::default() 80 | .direction(Direction::Horizontal) 81 | .constraints(self.flex.map(|flex| match flex { 82 | Flex::Block(px) => Constraint::Length(px), 83 | Flex::Grow(grow) => Constraint::Length(grow * grow_px / total_grow), 84 | })) 85 | .split(rect) 86 | } 87 | } 88 | 89 | impl Element for Frozen { 90 | fn on_key(&self, event: KeyEvent) { 91 | self.on_key.handle(event); 92 | } 93 | 94 | fn on_mouse(&self, rect: Rect, event: MouseEvent) { 95 | self.on_mouse.handle_or(event, |event| { 96 | for (i, rect) in self.layout(rect).into_iter().enumerate() { 97 | if event::is_within(&event, rect) { 98 | self.children[i].on_mouse(rect, event); 99 | 100 | break; 101 | } 102 | } 103 | }); 104 | } 105 | 106 | fn draw(&self, rect: Rect, frame: &mut Frame) { 107 | let layout = self.layout(rect); 108 | 109 | for (i, child) in self.children.iter().enumerate() { 110 | child.draw(*layout.get(i).expect("missing rect"), frame); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /intuitive/src/components/stack/mod.rs: -------------------------------------------------------------------------------- 1 | //! Structures relating to the `HStack` and `VStack` components. 2 | 3 | mod flex; 4 | pub(super) mod horizontal; 5 | pub(super) mod vertical; 6 | 7 | pub use self::flex::{Array as FlexArray, Flex}; 8 | -------------------------------------------------------------------------------- /intuitive/src/components/stack/vertical.rs: -------------------------------------------------------------------------------- 1 | use tui::layout::{Constraint, Direction, Layout}; 2 | 3 | use super::{Flex, FlexArray}; 4 | use crate::{ 5 | component, 6 | components::children::Children, 7 | element::{Any as AnyElement, Element}, 8 | event::{self, KeyEvent, KeyHandler, MouseEvent, MouseHandler}, 9 | terminal::{Frame, Rect}, 10 | }; 11 | 12 | /// A component that renders a vertical stack of components. 13 | /// 14 | /// The `flex` argument specifies the amount of space allocated to each child, similar 15 | /// to the [`flex` css property]. See the [`FlexArray`] documentation for details. 16 | /// 17 | /// An example usage would be, 18 | /// ```rust 19 | /// # use intuitive::{component, components::{VStack, Section}, render}; 20 | /// # 21 | /// #[component(Root)] 22 | /// fn render() { 23 | /// render! { 24 | /// VStack(flex: [1, 2, 3]) { 25 | /// Section(title: "Top") 26 | /// Section(title: "Middle") 27 | /// Section(title: "Bottom") 28 | /// } 29 | /// } 30 | /// } 31 | /// ``` 32 | /// Will render the following: 33 | /// 34 | /// ![vstack](https://raw.githubusercontent.com/enricozb/intuitive/main/assets/vstack.png) 35 | /// 36 | /// [`flex` css property]: https://developer.mozilla.org/en-US/docs/Web/CSS/flex 37 | /// [`FlexArray`]: struct.FlexArray.html 38 | #[component(Stack)] 39 | pub fn render(flex: FlexArray, children: Children, on_key: KeyHandler, on_mouse: MouseHandler) { 40 | AnyElement::new(Frozen { 41 | flex: *flex, 42 | 43 | children: children.render(), 44 | on_key: on_key.clone(), 45 | on_mouse: on_mouse.clone(), 46 | }) 47 | } 48 | 49 | struct Frozen { 50 | flex: FlexArray, 51 | 52 | children: [AnyElement; N], 53 | on_key: KeyHandler, 54 | on_mouse: MouseHandler, 55 | } 56 | 57 | impl Frozen { 58 | fn layout(&self, rect: Rect) -> Vec { 59 | let total_grow: u16 = self 60 | .flex 61 | .iter() 62 | .map(|flex| match flex { 63 | Flex::Grow(grow) => *grow, 64 | Flex::Block(_) => 0, 65 | }) 66 | .sum(); 67 | 68 | let total_px: u16 = self 69 | .flex 70 | .iter() 71 | .map(|flex| match flex { 72 | Flex::Block(px) => *px, 73 | Flex::Grow(_) => 0, 74 | }) 75 | .sum(); 76 | 77 | let grow_px = rect.height - total_px; 78 | 79 | Layout::default() 80 | .direction(Direction::Vertical) 81 | .constraints(self.flex.map(|flex| match flex { 82 | Flex::Block(px) => Constraint::Length(px), 83 | Flex::Grow(grow) => Constraint::Length(grow * grow_px / total_grow), 84 | })) 85 | .split(rect) 86 | } 87 | } 88 | 89 | impl Element for Frozen { 90 | fn on_key(&self, event: KeyEvent) { 91 | self.on_key.handle(event); 92 | } 93 | 94 | fn on_mouse(&self, rect: Rect, event: MouseEvent) { 95 | self.on_mouse.handle_or(event, |event| { 96 | for (i, rect) in self.layout(rect).into_iter().enumerate() { 97 | if event::is_within(&event, rect) { 98 | self.children[i].on_mouse(rect, event); 99 | 100 | break; 101 | } 102 | } 103 | }); 104 | } 105 | 106 | fn draw(&self, rect: Rect, frame: &mut Frame) { 107 | let layout = self.layout(rect); 108 | 109 | for (i, child) in self.children.iter().enumerate() { 110 | child.draw(*layout.get(i).expect("missing rect"), frame); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /intuitive/src/components/text/mod.rs: -------------------------------------------------------------------------------- 1 | use tui::{text::Spans as TuiSpans, widgets::Paragraph}; 2 | 3 | use crate::{ 4 | component, 5 | element::{Any as AnyElement, Element}, 6 | event::{KeyEvent, KeyHandler, MouseEvent, MouseHandler}, 7 | terminal::{Frame, Rect}, 8 | text::Lines, 9 | }; 10 | 11 | /// A component that displays text. 12 | /// 13 | /// `Text` renders the [`Lines`] passed into it. 14 | /// 15 | /// [`Lines`]: ../text/struct.Lines.html 16 | #[component(Text)] 17 | pub fn render(text: Lines, on_key: KeyHandler, on_mouse: MouseHandler) { 18 | AnyElement::new(Frozen { 19 | lines: text.clone(), 20 | on_key: on_key.clone(), 21 | on_mouse: on_mouse.clone(), 22 | }) 23 | } 24 | 25 | struct Frozen { 26 | lines: Lines, 27 | on_key: KeyHandler, 28 | on_mouse: MouseHandler, 29 | } 30 | 31 | impl Element for Frozen { 32 | fn on_key(&self, event: KeyEvent) { 33 | self.on_key.handle(event); 34 | } 35 | 36 | fn on_mouse(&self, _rect: Rect, event: MouseEvent) { 37 | self.on_mouse.handle(event); 38 | } 39 | 40 | fn draw(&self, rect: Rect, frame: &mut Frame) { 41 | let widget = Paragraph::new::>(self.lines.0.iter().cloned().map(TuiSpans::from).collect()); 42 | 43 | frame.render_widget(widget, rect); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /intuitive/src/element.rs: -------------------------------------------------------------------------------- 1 | //! Types describing rendered components. 2 | 3 | use std::{ops::Deref, sync::Arc}; 4 | 5 | use crate::{ 6 | components::{Component, Empty}, 7 | event::{KeyEvent, MouseEvent}, 8 | terminal::{Frame, Rect}, 9 | }; 10 | 11 | /// An opaque type holding a struct that implements [`Element`]. 12 | /// 13 | /// [`Element`]: trait.Element.html 14 | #[derive(Clone)] 15 | pub struct Any(Arc); 16 | 17 | impl Any { 18 | pub fn new(element: C) -> Self { 19 | Self(Arc::new(element)) 20 | } 21 | } 22 | 23 | impl Default for Any { 24 | fn default() -> Self { 25 | Empty {}.into() 26 | } 27 | } 28 | 29 | impl Deref for Any { 30 | type Target = Arc; 31 | 32 | fn deref(&self) -> &Self::Target { 33 | &self.0 34 | } 35 | } 36 | 37 | impl From for Any { 38 | fn from(component: C) -> Self { 39 | component.render() 40 | } 41 | } 42 | 43 | /// A rendered component. 44 | /// 45 | /// Once a [`Component`] is rendered, it now can be drawn (through [`draw`]) or it 46 | /// can handle a key event (through [`on_key`]). 47 | /// 48 | /// # Drawing 49 | /// Intuitive internally uses [tui] in order to draw to the terminal. The [`Rect`] 50 | /// and [`Frame`] structures are re-exports from [tui]. 51 | /// 52 | /// # Handling Keys 53 | /// Typically, structures that implement `Element` do not have any [`State`]. 54 | /// Usually, an `Element` will contain an `on_key` field which has captured any 55 | /// state that could be mutated, and then the `Element` will delegate key events 56 | /// to its `on_key` field. See the [`Section` source] for an example of this. 57 | /// 58 | /// [`Component`]: ../components/trait.Component.html 59 | /// [`draw`]: #method.draw 60 | /// [`Frame`]: https://docs.rs/tui/latest/tui/terminal/struct.Frame.html 61 | /// [`on_key`]: #method.on_key 62 | /// [`Rect`]: https://docs.rs/tui/latest/tui/layout/struct.Rect.html 63 | /// [`Section` source]: ../../src/intuitive/components/section.rs.html 64 | /// [`State`]: ../state/struct.State.html 65 | /// [tui]: https://docs.rs/tui/latest/tui/ 66 | pub trait Element { 67 | fn draw(&self, _rect: Rect, _frame: &mut Frame) {} 68 | fn on_key(&self, _event: KeyEvent) {} 69 | fn on_mouse(&self, _rect: Rect, _event: MouseEvent) {} 70 | } 71 | -------------------------------------------------------------------------------- /intuitive/src/error.rs: -------------------------------------------------------------------------------- 1 | //! The crate's `Error` type. 2 | 3 | use std::{io, sync::mpsc::RecvError}; 4 | 5 | use thiserror::Error; 6 | 7 | pub type Result = std::result::Result; 8 | 9 | #[derive(Error, Debug)] 10 | pub enum Error { 11 | #[error("io: {0}")] 12 | Io(#[from] io::Error), 13 | 14 | #[error("recv: {0}")] 15 | Recv(#[from] RecvError), 16 | 17 | #[error("send: {0}")] 18 | Send(String), 19 | 20 | #[error("manager: {0}")] 21 | Manager(&'static str), 22 | 23 | #[error("use_state calls must be in the same order: {0}")] 24 | UseState(String), 25 | } 26 | -------------------------------------------------------------------------------- /intuitive/src/event/channel.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::{ 3 | mpsc::{self, Receiver, Sender}, 4 | Arc, 5 | }, 6 | thread, 7 | }; 8 | 9 | use crossterm::event::{self as crossterm_event, Event as CrosstermEvent}; 10 | use lazy_static::lazy_static; 11 | use parking_lot::Mutex; 12 | 13 | use super::Event; 14 | use crate::error::{Error, Result}; 15 | 16 | lazy_static! { 17 | static ref CHANNEL: Channel = Channel::new(); 18 | } 19 | 20 | pub(crate) fn read() -> Result { 21 | CHANNEL.recv() 22 | } 23 | 24 | fn send(event: Event) -> Result<()> { 25 | CHANNEL.send(event) 26 | } 27 | 28 | /// Triggers a re-render. 29 | pub fn re_render() -> Result<()> { 30 | send(Event::Render) 31 | } 32 | 33 | /// Quits the application. 34 | /// 35 | /// This is often used in [`KeyHandler`]s like so: 36 | /// ```rust 37 | /// # use intuitive::on_key; 38 | /// # 39 | /// let on_key = on_key! { 40 | /// KeyEvent { code: Char('q'), .. } => event::quit(), 41 | /// }; 42 | /// ``` 43 | /// 44 | /// [`KeyHandler`]: struct.KeyHandler.html 45 | pub fn quit() { 46 | send(Event::Quit).expect("quit"); 47 | } 48 | 49 | pub fn start_crossterm_events() { 50 | thread::spawn(move || loop { 51 | let event = match crossterm_event::read().expect("read") { 52 | CrosstermEvent::Key(event) => Event::Key(event), 53 | CrosstermEvent::Mouse(event) => Event::Mouse(event), 54 | CrosstermEvent::Resize(..) => Event::Render, 55 | 56 | _ => continue, 57 | }; 58 | 59 | send(event).expect("send"); 60 | }); 61 | } 62 | 63 | struct Channel { 64 | sender: Arc>>, 65 | receiver: Arc>>, 66 | } 67 | 68 | impl Channel { 69 | pub fn new() -> Self { 70 | let (sender, receiver) = mpsc::channel(); 71 | 72 | Self { 73 | sender: Arc::new(Mutex::new(sender)), 74 | receiver: Arc::new(Mutex::new(receiver)), 75 | } 76 | } 77 | 78 | pub fn recv(&self) -> Result { 79 | Ok(self.receiver.lock().recv()?) 80 | } 81 | 82 | pub fn send(&self, event: Event) -> Result<()> { 83 | self.sender.lock().send(event).map_err(|err| Error::Send(err.to_string())) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /intuitive/src/event/handler.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | /// Whether to propagate the event to the next handler. 4 | pub enum Propagate { 5 | /// Continues handler propagation. 6 | Next, 7 | /// Stops handler propagation. 8 | Stop, 9 | } 10 | 11 | /// A generic handler for mouse and keyboard events. 12 | pub struct Handler { 13 | handler: Arc Propagate + 'static + Send + Sync>, 14 | } 15 | 16 | impl Default for Handler { 17 | fn default() -> Self { 18 | Self { 19 | handler: Arc::new(|_| Propagate::Next), 20 | } 21 | } 22 | } 23 | 24 | impl Clone for Handler { 25 | fn clone(&self) -> Self { 26 | Self { 27 | handler: self.handler.clone(), 28 | } 29 | } 30 | } 31 | 32 | impl Handler { 33 | /// Call the handler on the event. 34 | pub fn handle(&self, event: T) { 35 | self.handle_or(event, |_| {}); 36 | } 37 | 38 | /// Call the handler on the event, defaulting to the alternative_handler. 39 | pub fn handle_or(&self, event: T, alternative_handler: F) 40 | where 41 | F: FnOnce(T) -> R, 42 | { 43 | match (self.handler)(event) { 44 | Propagate::Next => drop(alternative_handler(event)), 45 | Propagate::Stop => (), 46 | } 47 | } 48 | 49 | /// Create a new handler that propagates to `next_handler`. 50 | /// 51 | /// Propagation only occurs if this handler returns `Propagate::Next`. 52 | pub fn then(&self, next_handler: F) -> Self 53 | where 54 | F: Fn(T) -> Propagate + 'static + Send + Sync, 55 | { 56 | let handler = self.handler.clone(); 57 | 58 | Handler::from(move |event| match handler(event) { 59 | Propagate::Next => next_handler(event), 60 | Propagate::Stop => Propagate::Stop, 61 | }) 62 | } 63 | } 64 | 65 | impl From for Handler 66 | where 67 | F: Fn(T) -> Propagate + 'static + Send + Sync, 68 | { 69 | fn from(f: F) -> Self { 70 | Self { handler: Arc::new(f) } 71 | } 72 | } 73 | 74 | impl From<&Handler> for Handler { 75 | fn from(handler: &Handler) -> Self { 76 | handler.clone() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /intuitive/src/event/mod.rs: -------------------------------------------------------------------------------- 1 | //! Primitives for handling and sending events. 2 | 3 | mod channel; 4 | pub mod handler; 5 | 6 | pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; 7 | 8 | pub use self::channel::{quit, re_render}; 9 | pub(crate) use self::channel::{read, start_crossterm_events}; 10 | use self::handler::Handler; 11 | #[cfg(doc)] 12 | use self::handler::Propagate; 13 | use crate::terminal::Rect; 14 | 15 | pub(crate) enum Event { 16 | Mouse(MouseEvent), 17 | Key(KeyEvent), 18 | Render, 19 | Quit, 20 | } 21 | 22 | /// A handler for [`KeyEvent`]s. 23 | /// 24 | /// # Creating a `KeyHandler` 25 | /// 26 | /// A `KeyHandler` is used to manipulate closures or functions that take in a 27 | /// [`KeyEvent`] as a parameter, and return a [`Propagate`]. `KeyHandler`s are often 28 | /// created using the [`on_key!`] macro. For example, 29 | /// ```rust 30 | /// # use intuitive::{component, components::Text, render, on_key, state::use_state}; 31 | /// # 32 | /// #[component(Root)] 33 | /// fn render() { 34 | /// let text = use_state(|| String::new()); 35 | /// 36 | /// let on_key = on_key! { [text] 37 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 38 | /// }; 39 | /// 40 | /// render! { 41 | /// Text(text: format!("Hi There {}", text.get()), on_key) 42 | /// } 43 | /// } 44 | /// ``` 45 | /// 46 | /// # Using a `KeyHandler` 47 | /// 48 | /// A [`KeyEvent`] can be handled by a `KeyHandler` through the [`Handler::handle`] 49 | /// method. `KeyHandler` implements [`Default`], and the default handler ignores the 50 | /// `KeyEvent`, and always returns [`Propagate`]`::Next`. 51 | /// 52 | /// Typically, components want to take some default action when implementing 53 | /// `on_key`, but allow the user of this component to override this handler. This can 54 | /// be done using the [`KeyHandler::handle_or`] method: 55 | /// ```rust 56 | /// # use intuitive::{element::Element, event::{KeyHandler, KeyEvent}}; 57 | /// # 58 | /// struct Frozen { 59 | /// on_key: KeyHandler, 60 | /// } 61 | /// 62 | /// impl Element for Frozen { 63 | /// fn on_key(&self, event: KeyEvent) { 64 | /// self.on_key.handle_or(event, |event| { /* default functionality here */ }) 65 | /// } 66 | /// } 67 | /// ``` 68 | /// Here, `Frozen::on_key` calls the handler that was provided if one was. If no 69 | /// `KeyHandler` was provided, then `self.on_key` is the default handler, 70 | /// which always returns [`Propagate`]`::Next`. This causes the closure above to 71 | /// be executed. 72 | /// 73 | /// # Propagation 74 | /// 75 | /// A user of a component can control when the default key handler is run by 76 | /// returning one of [`Propagate`]`::{Next, Stop}`. For example, to create an 77 | /// input box that receives input keys, but quits on the escape key: 78 | /// ```rust 79 | /// # use intuitive::{component, components::experimental::input::Input, render, on_key, state::use_state}; 80 | /// # 81 | /// #[component(Root)] 82 | /// fn render() { 83 | /// let text = use_state(|| String::new); 84 | /// 85 | /// let on_key = on_key! { [text] 86 | /// KeyEvent { code: Esc, .. } => event::quit(), 87 | /// 88 | /// _ => return Propagate::Next, 89 | /// }; 90 | /// 91 | /// render! { 92 | /// Input(on_key) 93 | /// } 94 | /// } 95 | /// ``` 96 | /// This will cause all key events other than `Esc` to be handled by `Input`'s 97 | /// default key handler. 98 | /// 99 | /// [`on_key!`]: ../macro.on_key.html 100 | /// [`Handler::handle_or`]: handler/struct.Handler.html#method.handle_or 101 | /// [`State`]: ../state/struct.State.html 102 | /// [`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html 103 | pub type KeyHandler = Handler; 104 | 105 | /// A handler for [`MouseEvent`]s. 106 | pub type MouseHandler = Handler; 107 | 108 | /// Check if a mouse event is within a [`Rect`]. 109 | pub fn is_within(event: &MouseEvent, rect: Rect) -> bool { 110 | let (x, y) = (event.column, event.row); 111 | 112 | let x_within = rect.x <= x && x <= rect.x + rect.width; 113 | let y_within = rect.y <= y && y <= rect.y + rect.height; 114 | 115 | x_within && y_within 116 | } 117 | -------------------------------------------------------------------------------- /intuitive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "unstable-doc-cfg", feature(doc_cfg))] 2 | 3 | //! # Intuitive 4 | //! Intuitive is a component-based library for creating text-based user interfaces 5 | //! (TUIs) easily. 6 | //! 7 | //! It is heavily inspired by [React] and [SwiftUI], containing features that 8 | //! resemble functional components, hooks, and a (mostly) declarative DSL. 9 | //! 10 | //! Check out the [Getting Started] section below for a brief introduction to using Intuitive. 11 | //! 12 | //! # Design 13 | //! The main focus of Intuitive is to simplify the implementation of section-based TUIs, 14 | //! such as [lazygit](https://github.com/jesseduffield/lazygit)'s, even at the slight 15 | //! expense of performance. Intuitive attempts to make it easy to write reusable TUI 16 | //! components that 17 | //! - encapsulate logic around handling state and key events 18 | //! - have complex layouts 19 | //! - are easy to read 20 | //! 21 | //! For example, a complex layout with an input box: 22 | //! ```no_run 23 | //! # use intuitive::{ 24 | //! # component, 25 | //! # components::{stack::Flex::*, HStack, Section, Text, VStack}, 26 | //! # error::Result, 27 | //! # on_key, render, 28 | //! # state::use_state, 29 | //! # terminal::Terminal, 30 | //! # }; 31 | //! # 32 | //! #[component(Root)] 33 | //! fn render() { 34 | //! let text = use_state(|| String::new()); 35 | //! 36 | //! let on_key = on_key! { [text] 37 | //! KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 38 | //! KeyEvent { code: Backspace, .. } => text.mutate(|text| text.pop()), 39 | //! KeyEvent { code: Esc, .. } => event::quit(), 40 | //! }; 41 | //! 42 | //! render! { 43 | //! VStack(flex: [Block(3), Grow(1)], on_key) { 44 | //! Section(title: "Input") { 45 | //! Text(text: text.get()) 46 | //! } 47 | //! 48 | //! HStack(flex: [1, 2, 3]) { 49 | //! Section(title: "Column 1") 50 | //! Section(title: "Column 2") 51 | //! Section(title: "Column 3") 52 | //! } 53 | //! } 54 | //! } 55 | //! } 56 | //! 57 | //! fn main() -> Result<()> { 58 | //! Terminal::new(Root::new())?.run() 59 | //! } 60 | //! ``` 61 | //! And the output would look like this: 62 | //! 63 | //! ![demo](https://raw.githubusercontent.com/enricozb/intuitive/main/assets/demo.png) 64 | //! 65 | //! # Getting Started 66 | //! Similarly to [React], Intuitive is built around components that are composable. 67 | //! There is one root component, that is passed to [`Terminal::new()`], in order to 68 | //! run the TUI. 69 | //! 70 | //! There are two main ways to build components: 71 | //! - Functional components using the [`component` attribute macro] 72 | //! - Custom components by implementing [`Component`] and (potentially [`Element`]) 73 | //! 74 | //! Both of these are discussed in depth in the [`components`] module documentation. Other 75 | //! useful resources are: 76 | //! - The documentation for the [`render!`] and [`on_key!`] macros, as they are often used 77 | //! when writing components. 78 | //! - The [recipes] section of the [`components`] module documentation, describing ways to 79 | //! achieve common UI interactions. 80 | //! - The [examples] directory in the repository, which contains complete examples of simple 81 | //! applications. 82 | //! 83 | //! # Disclaimer 84 | //! Intuitive is closer to a proof-of-concept than to a crate that's ready for 85 | //! prime-time use. There may also be some bugs in the library of components, 86 | //! please [raise an issue] if you find any. Furthermore, since a large and 87 | //! complex application has yet to be built using Intuitive, it is not a 88 | //! guarantee that it does not have some major flaw making such development 89 | //! difficult. 90 | //! 91 | //! [raise an issue]: https://github.com/enricozb/intuitive/issues 92 | //! [`component` attribute macro]: attr.component.html 93 | //! [`render!`]: macro.render.html 94 | //! [`on_key!`]: macro.on_key.html 95 | //! [`Component`]: components/trait.Component.html 96 | //! [`components`]: components/index.html 97 | //! [`Element`]: element/trait.Element.html 98 | //! [examples]: https://github.com/enricozb/intuitive/tree/main/examples 99 | //! [Getting Started]: #getting-started 100 | //! [React]: https://reactjs.org/ 101 | //! [recipes]: components/index.html#recipes 102 | //! [SwiftUI]: https://developer.apple.com/xcode/swiftui/ 103 | //! [`Terminal::new()`]: terminal/struct.Terminal.html#method.new 104 | 105 | extern crate self as intuitive; 106 | 107 | pub mod components; 108 | pub mod element; 109 | pub mod error; 110 | pub mod event; 111 | pub mod state; 112 | pub mod style; 113 | pub mod terminal; 114 | pub mod text; 115 | 116 | /// Helper attribute macro for creating functional components. 117 | /// 118 | /// # Usage 119 | /// This macro is used when creating functional components, where the name of 120 | /// the generated component is the item in the attribute. For example, 121 | /// ```rust 122 | /// # use intuitive::{component, components::{Centered, Section, Text}, on_key, state::use_state, render}; 123 | /// # 124 | /// #[component(Root)] 125 | /// pub fn render(title: String) { 126 | /// let text = use_state(String::new); 127 | /// 128 | /// let on_key = on_key! { [text] 129 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 130 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.pop()), 131 | /// KeyEvent { code: Esc, .. } => event::quit(), 132 | /// }; 133 | /// 134 | /// render! { 135 | /// Centered() { 136 | /// Section(title) { 137 | /// Text(text: text.get(), on_key) 138 | /// } 139 | /// } 140 | /// } 141 | /// } 142 | /// ``` 143 | /// constructs a `Root` component, that can be used in a [`render!`] macro. 144 | /// 145 | /// # Parameters 146 | /// If the `render` function contains parameters, these will become parameters to the 147 | /// generated component. These parameters can later be supplied when using the generated 148 | /// component in a [`render!`] macro. The parameters' types **must** implement [`Default`], 149 | /// as the generated component derives [`Default`]. If you need more control over the 150 | /// default values of the parameters, consider implementing the [`Component`] trait instead 151 | /// of using the `#[component(..)]` attribute macro. 152 | /// 153 | /// # Managing State 154 | /// State in functional components is managed similarly to how they are in [React], 155 | /// using the [`use_state`] hook. Refer to the [`use_state`] documentation for details. 156 | /// 157 | /// # Handling Key Events 158 | /// In functional components, key events are sent to the component at the root of the 159 | /// returned [`render!`] macro invocation. This means that in the example above, the 160 | /// key event will be sent to an instance of the [`Centered`] component. However, 161 | /// most components forward their key events to their children (especially those that 162 | /// have only a single child), and therefore the `on_key` handler could have been 163 | /// provided to any of the [`Centered`], [`Section`], or [`Text`] components above. 164 | /// 165 | /// # Generics 166 | /// When requiring generics, for example when accepting a variable number of children, 167 | /// they can be added into the attribute and then used in the parameters. For example: 168 | /// ```rust 169 | /// # use intuitive::{component, components::{Centered, children::Children, Section, Text}, on_key, state::use_state, render}; 170 | /// # 171 | /// #[component(Root)] 172 | /// pub fn render(title: String, children: Children) { 173 | /// let text = use_state(String::new); 174 | /// 175 | /// let on_key = on_key! { [text] 176 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 177 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.pop()), 178 | /// KeyEvent { code: Esc, .. } => event::quit(), 179 | /// }; 180 | /// 181 | /// render! { 182 | /// Centered() { 183 | /// Section(title) { 184 | /// Text(text: text.get(), on_key) 185 | /// } 186 | /// } 187 | /// } 188 | /// } 189 | /// ``` 190 | /// 191 | /// # Generated Component 192 | /// The generated component is a structure that implements the [`Component`] trait. It 193 | /// also has a an associated function `new() -> component::Any` that is used to create the 194 | /// component when passing it to `Terminal::new()`. If the component has parameters, 195 | /// they will also be parameters to the associated function `new()`in the same order 196 | /// they were specified in the `render` function. 197 | /// 198 | /// # Nuances 199 | /// There are a couple of nuances with this macro: 200 | /// - The visibility of the generated component will be the same as that of the 201 | /// `render` function the `#[component(..)]` attribute is applied to. 202 | /// - The return type to `render` (and even the function name itself) are completely 203 | /// ignored. In order to keep things consistent, it's recommended that the function 204 | /// is called `render` and the return type is left empty. 205 | /// 206 | /// [`Centered`]: components/struct.Centered.html 207 | /// [`Component`]: components/trait.Component.html 208 | /// [`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html 209 | /// [React]: https://reactjs.org/ 210 | /// [`render!`]: macro.render.html 211 | /// [`Section`]: components/struct.Section.html 212 | /// [`Text`]: components/struct.Text.html 213 | /// [`use_state`]: state/fn.use_state.html 214 | pub use intuitive_macros::component; 215 | /// Helper macro for creating key handlers. 216 | /// 217 | /// # Details 218 | /// This macro is used to simplify a common pattern constructing a [`event::KeyHandler`] where: 219 | /// - [`event`], [`event::KeyEvent`], [`event::KeyCode`]`::*`, and [`event::handler::Propagate`] are brought into scope 220 | /// - [`state::State`]s need to be cloned before being moved into the key handler 221 | /// - The event is immediately `match`ed 222 | /// 223 | /// In addition to the above, this macro also: 224 | /// - implicitly introduces the `|event|` closure parameter 225 | /// - adds the catch-all `_ => ()` case to the `match` expression 226 | /// - returns [`event::handler::Propagate::Stop`] 227 | /// 228 | /// # Usage 229 | /// An example usage looks like the following: 230 | /// ```rust 231 | /// # use intuitive::{state::use_state, on_key}; 232 | /// # 233 | /// let text = use_state(String::new); 234 | /// 235 | /// let on_key = on_key! { [text] 236 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 237 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.pop()), 238 | /// }; 239 | /// ``` 240 | /// and expands to the following: 241 | /// ```rust 242 | /// # use intuitive::{state::use_state, on_key}; 243 | /// # 244 | /// let text = use_state(String::new); 245 | /// 246 | /// let on_key = { 247 | /// let text = text.clone(); 248 | /// 249 | /// move |event| { 250 | /// use intuitive::event::{self, KeyEvent, KeyCode::*}; 251 | /// 252 | /// match event { 253 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 254 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.pop()), 255 | /// _ => (), 256 | /// }; 257 | /// }; 258 | /// }; 259 | /// ``` 260 | pub use intuitive_macros::on_key; 261 | /// Macro for rendering components. 262 | /// 263 | /// # Usage 264 | /// This macro is meant to be used when returning from [`components::Component::render`], 265 | /// and uses a [SwiftUI](https://developer.apple.com/xcode/swiftui/)-like syntax. 266 | /// 267 | /// For example: 268 | /// ```rust 269 | /// # use intuitive::{components::{Any as AnyComponent, Section, Text, VStack}, render}; 270 | /// # 271 | /// let _: AnyComponent = render! { 272 | /// VStack() { 273 | /// Section(title: "Top Section") { 274 | /// Text(text: "Hello") 275 | /// } 276 | /// 277 | /// Section(title: "Bottom Section") { 278 | /// Text(text: "World") 279 | /// } 280 | /// } 281 | /// }; 282 | /// ``` 283 | /// is rendering a `VStack` (with default parameters), and two children. The 284 | /// child components are `Section`s, each with their own `Text` child components. 285 | /// 286 | /// # Parameters 287 | /// Parameters passed to components look like function arguments but are actually much 288 | /// closer to structure initialization. Like struct fields, they can be passed in any 289 | /// order, and they require the field name, unless the parameter and value are the same 290 | /// identifier. Unlike struct fields, you can omit parameters, as any omitted parameters 291 | /// are implicitly passed in with their default values. 292 | /// 293 | /// ## Automatic Parameter Conversion 294 | /// When passing parameters to components within a `render!` macro invocation, an implicit 295 | /// [`TryInto::try_into`] call is made for each parameter. This means that you can omit 296 | /// any `.into()` calls when passing parameters to components. This is very useful when 297 | /// working with [`Spans`] and [`Style`], as they implement [`From`] from a variety 298 | /// of types. 299 | /// 300 | /// # Children 301 | /// Children to a component come after the component surrounded by braces (`{ ... }`). 302 | /// Like parameters, children are optional, but are only valid for components that 303 | /// accept them (for example `Text` accepts no children, but `Section` does). 304 | /// 305 | /// Children are passed as arrays (`[AnyComponent; N]`), so components specify exactly 306 | /// how many children they take in. Some components, like `VStack` and `HStack` take 307 | /// in a variable number of children, while some, like `Section`, only accept a single 308 | /// child component. 309 | /// 310 | /// [`From`]: https://doc.rust-lang.org/std/convert/trait.From.html 311 | /// [`Spans`]: spans/struct.Spans.html 312 | /// [`Style`]: style/struct.Style.html 313 | /// [`TryInto::try_into`]: https://doc.rust-lang.org/std/convert/trait.TryInto.html#tymethod.try_into 314 | pub use intuitive_macros::render; 315 | -------------------------------------------------------------------------------- /intuitive/src/state/hook.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::Mutex; 2 | 3 | use super::manager::Manager; 4 | pub use super::State; 5 | use crate::error::Error; 6 | 7 | static MANAGER: Mutex = Mutex::new(Manager::new()); 8 | 9 | pub fn render_done() { 10 | MANAGER.lock().reset().map_err(|err| Error::UseState(err.to_string())).unwrap(); 11 | } 12 | 13 | /// A hook for managing state within a [`Component`] 14 | /// 15 | /// Similarly to [React Hooks], `use_state` lets you manager state without an explicit 16 | /// struct storing state. `use_state` returns a [`State`], which can be used to retrieve 17 | /// or update the value. For example, 18 | /// ```rust 19 | /// # use intuitive::{component, components::{Section, Text}, state::use_state, on_key, render}; 20 | /// # 21 | /// #[component(Input)] 22 | /// fn render(title: String) { 23 | /// let text = use_state(|| String::new()); 24 | /// 25 | /// let on_key = on_key! { [text] 26 | /// KeyEvent { code: Char(c), .. } => text.mutate(|text| text.push(c)), 27 | /// KeyEvent { code: Backspace, .. } => text.mutate(|text| text.pop()), 28 | /// }; 29 | /// 30 | /// render! { 31 | /// Section(title) { 32 | /// Text(text: text.get(), on_key) 33 | /// } 34 | /// } 35 | /// } 36 | /// ``` 37 | /// 38 | /// # Gotchas 39 | /// Any calls to `use_state` must always be called in the same order and in every render. 40 | /// This means that there can not be any conditional logic around the calling of `use_state`. 41 | /// If Intuitive detects such a violation, it will panic with an appropriate message. 42 | /// 43 | /// [`Component`]: ../components/trait.Component.html 44 | /// [`State`]: struct.State.html 45 | /// [React Hooks]: https://reactjs.org/docs/hooks-intro.html 46 | pub fn use_state(initializer: F) -> State 47 | where 48 | T: 'static + Send, 49 | F: FnOnce() -> T, 50 | { 51 | MANAGER 52 | .lock() 53 | .next(initializer) 54 | .map_err(|err| Error::UseState(err.to_string())) 55 | .unwrap() 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use serial_test::serial; 61 | 62 | use super::*; 63 | 64 | fn setup() { 65 | *MANAGER.lock() = Manager::new(); 66 | 67 | let _ = use_state(|| 1); 68 | let _ = use_state(|| 2); 69 | 70 | render_done(); 71 | } 72 | 73 | #[test] 74 | #[serial] 75 | fn use_state_no_panic() { 76 | setup(); 77 | 78 | let _ = use_state(|| 1); 79 | let _ = use_state(|| 2); 80 | 81 | render_done(); 82 | } 83 | 84 | #[test] 85 | #[serial] 86 | fn use_state_get() { 87 | setup(); 88 | 89 | let state_1 = use_state(|| 1); 90 | let state_2 = use_state(|| 2); 91 | 92 | assert_eq!(state_1.get(), 1); 93 | assert_eq!(state_2.get(), 2); 94 | 95 | render_done(); 96 | } 97 | 98 | #[test] 99 | #[serial] 100 | fn use_state_set_get() { 101 | setup(); 102 | 103 | let state_1 = use_state(|| 1); 104 | let state_2 = use_state(|| 2); 105 | 106 | state_1.set(3); 107 | state_2.set(4); 108 | 109 | assert_eq!(state_1.get(), 3); 110 | assert_eq!(state_2.get(), 4); 111 | 112 | render_done(); 113 | } 114 | 115 | #[test] 116 | #[serial] 117 | #[should_panic] 118 | fn use_state_wrong_type() { 119 | setup(); 120 | 121 | let _ = use_state(|| ()); 122 | 123 | render_done(); 124 | } 125 | 126 | #[test] 127 | #[serial] 128 | #[should_panic] 129 | fn use_state_too_few() { 130 | setup(); 131 | 132 | let _ = use_state(|| 1); 133 | 134 | render_done(); 135 | } 136 | 137 | #[test] 138 | #[serial] 139 | #[should_panic] 140 | fn use_state_too_many() { 141 | setup(); 142 | 143 | let _ = use_state(|| 1); 144 | let _ = use_state(|| 2); 145 | let _ = use_state(|| 3); 146 | 147 | render_done(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /intuitive/src/state/manager.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use super::State; 4 | use crate::error::{Error, Result}; 5 | 6 | pub struct Manager { 7 | states: Vec>, 8 | idx: usize, 9 | filling: bool, 10 | } 11 | 12 | impl Manager { 13 | pub const fn new() -> Self { 14 | Self { 15 | states: Vec::new(), 16 | idx: 0, 17 | filling: true, 18 | } 19 | } 20 | 21 | pub fn next(&mut self, initializer: F) -> Result> 22 | where 23 | T: 'static + Send, 24 | F: FnOnce() -> T, 25 | { 26 | if self.filling { 27 | let state = State::new(initializer()); 28 | self.states.push(Box::new(state.clone())); 29 | 30 | Ok(state) 31 | } else { 32 | let state = self.states.get(self.idx).ok_or(Error::Manager("invalid index"))?; 33 | 34 | self.idx += 1; 35 | 36 | Ok(state.downcast_ref::>().ok_or(Error::Manager("invalid type"))?.clone()) 37 | } 38 | } 39 | 40 | pub fn reset(&mut self) -> Result<()> { 41 | if self.filling { 42 | self.filling = false; 43 | } else if self.idx != self.states.len() { 44 | return Err(Error::Manager("insufficient calls")); 45 | } 46 | 47 | self.idx = 0; 48 | 49 | Ok(()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /intuitive/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | //! Primitives for handling state. 2 | 3 | mod hook; 4 | mod manager; 5 | 6 | use std::sync::Arc; 7 | 8 | use parking_lot::Mutex; 9 | 10 | pub(crate) use self::hook::render_done; 11 | pub use self::hook::use_state; 12 | use crate::event; 13 | 14 | /// A struct that triggers a re-render upon mutation. 15 | /// 16 | /// `State`'s have interior mutability, and can be cloned. `State`s cloned 17 | /// from one another share the inner reference to a `T`, and therefore mutating one of 18 | /// them will be reflected across all of the cloned states. For example, 19 | /// ```rust 20 | /// # use intuitive::state::use_state; 21 | /// # 22 | /// let count = use_state(|| 0); 23 | /// 24 | /// let other_count = count.clone(); 25 | /// other_count.set(1); 26 | /// 27 | /// // both `count` and `other_count` are `1` 28 | /// assert_eq!(count.get(), other_count.get()); 29 | /// ``` 30 | /// 31 | /// This is useful when receiving a `State` as a parameter from a parent component, 32 | /// as it must be cloned, and then may be mutated by both the child and parent components. 33 | #[derive(Default)] 34 | pub struct State { 35 | inner: Arc>, 36 | } 37 | 38 | impl State { 39 | pub(crate) fn new(inner: T) -> Self { 40 | Self { 41 | inner: Arc::new(Mutex::new(inner)), 42 | } 43 | } 44 | 45 | /// Sets a new value for the state and triggers a re-render. 46 | pub fn set(&self, new: T) { 47 | let mut inner = self.inner.lock(); 48 | *inner = new; 49 | 50 | event::re_render().expect("re_render"); 51 | } 52 | 53 | /// Calls a function on the inner value and returns its result. 54 | /// Does not trigger a re-render. 55 | pub fn inspect(&self, f: F) -> R 56 | where 57 | F: FnOnce(&T) -> R, 58 | { 59 | f(&self.inner.lock()) 60 | } 61 | 62 | /// Calls a function on a mutable reference of the inner value and triggers a re-render. 63 | pub fn mutate(&self, f: F) 64 | where 65 | F: FnOnce(&mut T) -> R, 66 | { 67 | let mut inner = self.inner.lock(); 68 | drop(f(&mut inner)); 69 | 70 | event::re_render().expect("re_render"); 71 | } 72 | 73 | /// Calls a function on the inner value, replaces it with the result, and triggers a re-render. 74 | pub fn update(&self, f: F) 75 | where 76 | F: FnOnce(&T) -> T, 77 | { 78 | let mut inner = self.inner.lock(); 79 | *inner = f(&inner); 80 | 81 | event::re_render().expect("re_render"); 82 | } 83 | } 84 | 85 | impl State { 86 | /// Returns a clone of the `State`'s inner value. 87 | pub fn get(&self) -> T { 88 | self.inner.lock().clone() 89 | } 90 | } 91 | 92 | impl Clone for State { 93 | fn clone(&self) -> Self { 94 | Self { inner: self.inner.clone() } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /intuitive/src/state/state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use parking_lot::Mutex; 4 | 5 | use crate::event; 6 | 7 | #[derive(Default)] 8 | pub struct State { 9 | inner: Arc>, 10 | } 11 | 12 | impl State { 13 | pub fn new(inner: T) -> Self { 14 | Self { 15 | inner: Arc::new(Mutex::new(inner)), 16 | } 17 | } 18 | 19 | pub fn set(&self, new: T) { 20 | let mut inner = self.inner.lock(); 21 | *inner = new; 22 | 23 | event::re_render().expect("re_render"); 24 | } 25 | 26 | pub fn mutate(&self, f: F) 27 | where 28 | F: FnOnce(&mut T) -> R, 29 | { 30 | let mut inner = self.inner.lock(); 31 | drop(f(&mut inner)); 32 | 33 | event::re_render().expect("re_render"); 34 | } 35 | 36 | pub fn update(&self, f: F) 37 | where 38 | F: FnOnce(&T) -> T, 39 | { 40 | let mut inner = self.inner.lock(); 41 | *inner = f(&inner); 42 | 43 | event::re_render().expect("re_render"); 44 | } 45 | } 46 | 47 | impl State { 48 | pub fn get(&self) -> T { 49 | self.inner.lock().clone() 50 | } 51 | } 52 | 53 | impl Clone for State { 54 | fn clone(&self) -> Self { 55 | Self { inner: self.inner.clone() } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /intuitive/src/style.rs: -------------------------------------------------------------------------------- 1 | //! Structures for working with colors and text modifiers. 2 | 3 | use tui::style::Style as TuiStyle; 4 | pub use tui::style::{Color, Modifier}; 5 | 6 | /// Styles that can apply to anything drawn on the screen. 7 | /// 8 | /// Styles are composed of foreground colors, background colors, 9 | /// and text modifiers. These fields are optional (modifiers have `Modifier::NONE`), 10 | /// and can be merged together when multiple styles are applied. 11 | /// 12 | /// `Style` also conveniently implements `From`, which creates a style 13 | /// with the provided color as the foreground color. 14 | #[derive(Default, Clone, Copy, PartialEq, Eq)] 15 | pub struct Style(TuiStyle); 16 | 17 | impl Style { 18 | pub fn new(fg: Option, bg: Option, modifier: Modifier) -> Self { 19 | Self(TuiStyle { 20 | fg, 21 | bg, 22 | add_modifier: modifier, 23 | sub_modifier: Modifier::empty(), 24 | }) 25 | } 26 | } 27 | 28 | /// `Convert` a color into a `Style` with a specific foreground color. 29 | impl From for Style { 30 | fn from(color: Color) -> Self { 31 | Self::new(Some(color), None, Modifier::empty()) 32 | } 33 | } 34 | 35 | impl From