├── .github └── workflows │ ├── mdbook.yml │ ├── mdbook22.yml │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── changes.md ├── examples ├── async1.rs ├── block.life ├── cheat.md ├── files.rs ├── life.life ├── life.rs ├── mdedit.md ├── mdedit.rs ├── mdedit_parts │ ├── dump.rs │ ├── format.rs │ ├── mod.rs │ ├── operations.rs │ ├── parser.rs │ ├── styles.rs │ └── test_markdown.rs ├── minimal.rs ├── queen_bee_shuttle.life ├── rat.life ├── theme_sample.rs ├── trap.life ├── tumbler.life ├── turbo.rs └── ultra.rs ├── files.gif ├── mdedit.gif ├── readme.md ├── rsbook ├── book.toml └── src │ ├── SUMMARY.md │ ├── appctx.md │ ├── chapter_1.md │ ├── concepts.md │ ├── events.md │ ├── events_control_flow.md │ ├── events_control_flow2.md │ ├── events_match.md │ ├── events_widget.md │ ├── events_widget2.md │ ├── examples.md │ ├── focus.md │ ├── focus_builder.md │ ├── focus_container.md │ ├── focus_deeper.md │ ├── focus_widget.md │ ├── intro.md │ ├── minimal.md │ ├── render_overlay.md │ ├── widget-extensions.md │ └── widgets.md └── src ├── control_queue.rs ├── framework.rs ├── lib.rs ├── poll.rs ├── poll_queue.rs ├── run_config.rs ├── terminal.rs ├── threadpool.rs ├── timer.rs └── tokio_tasks.rs /.github/workflows/mdbook.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a mdBook site to GitHub Pages 2 | # 3 | # To get started with mdBook see: https://rust-lang.github.io/mdBook/index.html 4 | # 5 | name: Deploy mdBook site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: [ $default-branch ] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: "pages" 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | env: 32 | MDBOOK_VERSION: 0.4.40 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Install mdBook 36 | run: | 37 | curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh 38 | rustup update 39 | cargo install --version ${MDBOOK_VERSION} mdbook 40 | - name: Setup Pages 41 | id: pages 42 | uses: actions/configure-pages@v5 43 | - name: Build with mdBook 44 | run: mdbook build rsbook 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: ./rsbook/book 49 | 50 | # Deployment job 51 | deploy: 52 | environment: 53 | name: github-pages 54 | url: ${{ steps.deployment.outputs.page_url }} 55 | runs-on: ubuntu-latest 56 | needs: build 57 | steps: 58 | - name: Deploy to GitHub Pages 59 | id: deployment 60 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/workflows/mdbook22.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a mdBook site to GitHub Pages 2 | # 3 | # To get started with mdBook see: https://rust-lang.github.io/mdBook/index.html 4 | # 5 | name: Deploy mdBook site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: ["master"] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: "pages" 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | env: 32 | MDBOOK_VERSION: 0.4.36 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Install mdBook 36 | run: | 37 | curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh 38 | rustup update 39 | cargo install --version ${MDBOOK_VERSION} mdbook 40 | - name: Setup Pages 41 | id: pages 42 | uses: actions/configure-pages@v5 43 | - name: Build with mdBook 44 | run: mdbook build rsbook 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: ./rsbook/book 49 | 50 | # Deployment job 51 | deploy: 52 | environment: 53 | name: github-pages 54 | url: ${{ steps.deployment.outputs.page_url }} 55 | runs-on: ubuntu-latest 56 | needs: build 57 | steps: 58 | - name: Deploy to GitHub Pages 59 | id: deployment 60 | uses: actions/deploy-pages@v4 61 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build 21 | - name: Run tests 22 | run: cargo test 23 | - name: Run test examples 24 | run: cargo build --features async --examples 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /docs -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rat-salsa" 3 | version = "0.30.0" 4 | authors = ["thscharler "] 5 | edition = "2021" 6 | description = "ratatui widgets and a crossterm event-loop" 7 | license = "MIT/Apache-2.0" 8 | repository = "https://github.com/thscharler/rat-salsa" 9 | readme = "readme.md" 10 | keywords = ["ratatui", "input", "event-loop"] 11 | categories = ["command-line-interface"] 12 | exclude = [".idea/*", ".gitignore", "files.gif", "mdedit.gif"] 13 | 14 | [features] 15 | default = [] 16 | async = ["dep:tokio"] 17 | 18 | [dependencies] 19 | crossbeam = "0.8" 20 | crossterm = "0.28" 21 | log = "0.4" 22 | ratatui = { version = "0.29" } 23 | tokio = { version = "1.42.0", features = ["rt", "rt-multi-thread", "sync", "time"], optional = true } 24 | 25 | rat-widget = { version = "0.33" } 26 | 27 | [dev-dependencies] 28 | fern = "0.7" 29 | humantime = "2.1" 30 | anyhow = "1.0" 31 | directories-next = "2.0.0" 32 | sysinfo = "0.32.0" 33 | pulldown-cmark = "0.12.0" 34 | ropey = "1.6.1" 35 | cli-clipboard = "0.4.0" 36 | unicode-segmentation = "1.11" 37 | textwrap = "0.16" 38 | configparser = { version = "3.1.0", features = ["indexmap"] } 39 | rand = "0.8.5" 40 | 41 | rat-theme = { version = "0.27" } -------------------------------------------------------------------------------- /changes.md: -------------------------------------------------------------------------------- 1 | # 0.30.0 2 | 3 | * feature: Add RenderedEvent. This can be activated by adding 4 | PollRendered and will send one event after each successful render. 5 | * feature: Add an optional async runtime. Currently, tokio. 6 | Start async tasks via the AppContext and get the result as event. 7 | 8 | # 0.29.0 9 | 10 | * major change: Replaced the current Message type variable with Event. 11 | 12 | This offloads the complete event-type to the application. 13 | 14 | * event distribution goes only through one channel now, 15 | the newly added `event()` method. This makes it easier not 16 | to miss some event-forwarding to different parts of the app. 17 | * crossterm goes from 'requirement' to 'supported'. 18 | There still is `PollCrossterm`, but it can be replaced with 19 | something else entirely. 20 | * A custom Pollxx can be added and distribute events via the standard `event()`. 21 | It can require support for its own events with trait bounds. 22 | Implementing one stays the same, and if configuration is needed 23 | it still has to go in the Global struct and has to be shared 24 | with the Pollxx. 25 | * Timers and ThreadPool can be deactivated completely. 26 | They are still kept in the AppContext and panic when they are 27 | accessed and not initialized but otherwise are simply absent. 28 | 29 | The change was surprisingly easy though. Most of the changes come from the optional 30 | timer and threading. The AppState trait is much cleaner now. It retains init() 31 | and gains the lost shutdown(). It is probably the best to keep those out of regular 32 | event-handling. The error() handling functions stays too, but that's unused for 33 | everything but the main AppState anyway. 34 | 35 | *** Upgrading *** 36 | Is copypasta more or less. Move the current crossterm(), message() and timer() 37 | functions out of the trait impl and call them from event(). Or inline their 38 | contents. 39 | 40 | * fix: Key Release events are not universal. Set a static flag during 41 | terminal init to allow widgets to query this behaviour. 42 | 43 | # 0.28.2 44 | 45 | * doc fixes 46 | 47 | # 0.28.1 48 | 49 | * remove unnecessary Debug bounds everywhere. 50 | 51 | # 0.28.0 52 | 53 | ** upgrade to ratatui 0.29 ** 54 | 55 | * examples changed due to upstream changes. 56 | 57 | # 0.27.2 58 | 59 | * fix: upstream changes 60 | 61 | # 0.27.1 62 | 63 | * docs: small fixes 64 | 65 | # 0.27.0 66 | 67 | * break: Make Control non_exhaustive. 68 | 69 | * feature: Change the sleep strategy. Longer idle sleeps and separate 70 | and faster backoff after changing to fast sleeps. 71 | 72 | # 0.26.0 73 | 74 | * break: final renames in rat-focus. 75 | 76 | # 0.25.6 77 | 78 | * fix: update some docs. 79 | 80 | # 0.25.5 81 | 82 | * fix: docs 83 | 84 | # 0.25.4 85 | 86 | * fix: changes in rat-menu affect the examples. 87 | 88 | # 0.25.2 and 0.25.3 89 | 90 | fixed some docs 91 | 92 | # 0.25.1 93 | 94 | * mention the book. 95 | 96 | # 0.25.0 97 | 98 | Sync version for beta. 99 | 100 | * feat: write rsbook. 101 | * feat: Replace all conversions OutcomeXX to Control with 102 | one `From>`. All OutcomeXX should be convertible to 103 | base Outcome anyway. 104 | * refactor: Cancel is now an Arc. 105 | * fix: Define Ord for Control without using Message. 106 | * example: Add life.rs 107 | * example: Add turbo.rs 108 | 109 | # 0.24.2 110 | 111 | * minor fixes for examples/mdedit 112 | * add gifs but don't publish them. 113 | 114 | # 0.24.1 115 | 116 | * extensive work at the mdedit example. might even publish this 117 | separately sometime. 118 | 119 | * cleanup minimal example. make it more minimal. 120 | 121 | # 0.24.0 122 | 123 | * update ratatui to 0.28 124 | 125 | # 0.23.0 126 | 127 | * Start example mdedit. 128 | * Update examples files. 129 | 130 | * break: remove timeout from AppContext and add to Terminal::render() instead. 131 | * break: rename AppEvents to AppState to be more in sync with ratatui. 132 | 133 | * feature: add replace_timer() to both contexts. 134 | * feature: addd set_screen_cursor() to RenderContext. 135 | * fix: Timer must use next from TimerDef if it exists. 136 | 137 | # 0.22.2 138 | 139 | * refactor: adaptations for new internal Scroll<'a> instead of Scrolled widget. 140 | 141 | # 0.22.1 142 | 143 | * add files.rs example 144 | * DarkTheme adds methods to create styles directly from scheme colors. 145 | 146 | # 0.22.0 147 | 148 | * Restart the loop once more: 149 | * Remove RepaintEvent. Move the TimeOut to RenderContext instead. 150 | * Add trait EventPoll. This abstracts away all event-handling, 151 | and allows adding custom event-sources. 152 | * Implement PollTimers, PollCrossterm, PollTasks this way, 153 | and just make those the default set. 154 | * Add trait Terminal. This encapsulates the ratatui::Terminal. 155 | * Make the terminal init sequences customizable. 156 | * Allows other Backends, while not adding to the type variables. 157 | * Extend RunConfig with 158 | * render - RenderUI impl 159 | * events - List of EventPoll. 160 | * Remove functions add_timer(), remove_timer(), spawn() and queue() from RenderContext. 161 | This is not needed while rendering. 162 | 163 | # 0.21.2 164 | 165 | * refactor: AppWidget::render() removes mut from self parameter. 166 | this matches better with ratatui. should be good enough usually. 167 | * add theme example 168 | 169 | # 0.21.1 170 | 171 | Fixed several future problems with ordering the events in the presence 172 | of AppContext::queue(). Changed to use a single queue for external events 173 | and results. External events are only polled again after all internal 174 | results have been processed. This way there is a well-defined order 175 | for the internal results and a guarantee that no external interference 176 | can occur between processing two internal results. Which probably 177 | would provide food for some headaches. 178 | 179 | # 0.21.0 180 | 181 | Moved everything from rat-salsa2 back to rat-salsa, now that it is no 182 | longer in use. 183 | 184 | # 0.20.2 185 | 186 | * complete refactor: 187 | * throw away TuiApp completely. It got fat&ugly lately. 188 | * Drastically reduce the number of used types, don't need 189 | Data and Theme, those can go into Global as an implementation detail. 190 | 191 | With everything down to three types Global, Action and Error use them directly. 192 | Everything is still tied together via AppContext and RenderContext. 193 | 194 | * refactor: hide timer in the context structs and add the necessary access 195 | functions, add and remove. 196 | * refactor: make Timers private and add a TimerHandle for deletion. 197 | * AppContext and RenderContext: queue and tasks need not be public. 198 | 199 | # 0.20.1 200 | 201 | * Extend tasks with cancellation support. 202 | * Add queue for extra result values from event-handling. 203 | Used for accurate repaint after focus changes. 204 | 205 | * fix missing conversion from ScrollOutcome. 206 | * fix missing conversions for DoubleClickOutcome. 207 | 208 | * simplified the internal machinery of event-handling a bit. 209 | Simpler type variables are a thing. 210 | 211 | # 0.15.1 212 | 213 | was the wrong crate committed 214 | 215 | # 0.20.0 216 | 217 | * Split AppWidgets into AppWidgets and AppEvents. One for the 218 | widget side for render, the other for the state side for all 219 | event handling. This better aligns with the split seen 220 | in ratatui stateful widgets. 221 | - The old mono design goes too much in the direction of a widget tree, 222 | which is not the intent. 223 | - It seems that AppWidget now very much mimics the StatefulWidget trait, 224 | event if that was not the initial goal. Curious. 225 | - I think I'm quite content with the tree-way split that now exists. 226 | - I had originally intended to use the rat-event::HandleEvent trait 227 | instead of some AppEvents, but that proved to limited. It still is 228 | very fine for creating widgets, that's why I don't want to change 229 | it anymore. Can live well with this current state. 230 | 231 | # 0.19.0 232 | 233 | First release that I consider as BETA ready. 234 | 235 | * reorg from rat-event down. built in some niceties there. 236 | 237 | # 0.18.0 238 | 239 | Start from scratch as rat-salsa2. The old rat-salsa now is 240 | mostly demolished and split up in 241 | 242 | * rat-event 243 | * rat-focus 244 | * rat-ftable 245 | * rat-input 246 | * rat-scrolled 247 | * rat-widget 248 | 249 | and the rest is not deemed worth it. 250 | -------------------------------------------------------------------------------- /examples/async1.rs: -------------------------------------------------------------------------------- 1 | use crate::config::MinimalConfig; 2 | use crate::event::Async1Event; 3 | use crate::global::GlobalState; 4 | use crate::scenery::{Scenery, SceneryState}; 5 | use anyhow::Error; 6 | use rat_salsa::poll::{PollCrossterm, PollRendered, PollTasks, PollTimers}; 7 | #[cfg(feature = "async")] 8 | use rat_salsa::PollTokio; 9 | use rat_salsa::{run_tui, RunConfig}; 10 | use rat_theme::dark_theme::DarkTheme; 11 | use rat_theme::scheme::IMPERIAL; 12 | use std::time::SystemTime; 13 | 14 | type AppContext<'a> = rat_salsa::AppContext<'a, GlobalState, Async1Event, Error>; 15 | type RenderContext<'a> = rat_salsa::RenderContext<'a, GlobalState>; 16 | 17 | fn main() -> Result<(), Error> { 18 | setup_logging()?; 19 | 20 | let rt = tokio::runtime::Runtime::new()?; 21 | 22 | let config = MinimalConfig::default(); 23 | let theme = DarkTheme::new("Imperial".into(), IMPERIAL); 24 | let mut global = GlobalState::new(config, theme); 25 | 26 | let app = Scenery; 27 | let mut state = SceneryState::default(); 28 | 29 | run_tui( 30 | app, 31 | &mut global, 32 | &mut state, 33 | RunConfig::default()? 34 | .poll(PollCrossterm) 35 | .poll(PollTimers) 36 | .poll(PollTasks) 37 | .poll(PollRendered) 38 | .poll(PollTokio::new(rt)), 39 | )?; 40 | 41 | Ok(()) 42 | } 43 | 44 | /// Globally accessible data/state. 45 | pub mod global { 46 | use crate::config::MinimalConfig; 47 | use rat_theme::dark_theme::DarkTheme; 48 | use rat_widget::msgdialog::MsgDialogState; 49 | use rat_widget::statusline::StatusLineState; 50 | 51 | #[derive(Debug)] 52 | pub struct GlobalState { 53 | pub cfg: MinimalConfig, 54 | pub theme: DarkTheme, 55 | pub status: StatusLineState, 56 | pub error_dlg: MsgDialogState, 57 | } 58 | 59 | impl GlobalState { 60 | pub fn new(cfg: MinimalConfig, theme: DarkTheme) -> Self { 61 | Self { 62 | cfg, 63 | theme, 64 | status: Default::default(), 65 | error_dlg: Default::default(), 66 | } 67 | } 68 | } 69 | } 70 | 71 | /// Configuration. 72 | pub mod config { 73 | #[derive(Debug, Default)] 74 | pub struct MinimalConfig {} 75 | } 76 | 77 | /// Application wide messages. 78 | pub mod event { 79 | use rat_salsa::timer::TimeOut; 80 | use rat_salsa::RenderedEvent; 81 | 82 | #[derive(Debug)] 83 | pub enum Async1Event { 84 | Timer(TimeOut), 85 | Event(crossterm::event::Event), 86 | Rendered, 87 | Message(String), 88 | FromAsync(String), 89 | AsyncTick(u32), 90 | } 91 | 92 | impl From for Async1Event { 93 | fn from(_: RenderedEvent) -> Self { 94 | Self::Rendered 95 | } 96 | } 97 | 98 | impl From for Async1Event { 99 | fn from(value: TimeOut) -> Self { 100 | Self::Timer(value) 101 | } 102 | } 103 | 104 | impl From for Async1Event { 105 | fn from(value: crossterm::event::Event) -> Self { 106 | Self::Event(value) 107 | } 108 | } 109 | } 110 | 111 | pub mod scenery { 112 | use crate::async1::{Async1, Async1State}; 113 | use crate::event::Async1Event; 114 | use crate::global::GlobalState; 115 | use crate::{AppContext, RenderContext}; 116 | use anyhow::Error; 117 | use rat_salsa::{AppState, AppWidget, Control}; 118 | use rat_widget::event::{ct_event, ConsumedEvent, Dialog, HandleEvent, Regular}; 119 | use rat_widget::focus::FocusBuilder; 120 | use rat_widget::layout::layout_middle; 121 | use rat_widget::msgdialog::MsgDialog; 122 | use rat_widget::statusline::StatusLine; 123 | use ratatui::buffer::Buffer; 124 | use ratatui::layout::{Constraint, Layout, Rect}; 125 | use ratatui::widgets::StatefulWidget; 126 | use std::time::{Duration, SystemTime}; 127 | 128 | #[derive(Debug)] 129 | pub struct Scenery; 130 | 131 | #[derive(Debug, Default)] 132 | pub struct SceneryState { 133 | pub async1: Async1State, 134 | } 135 | 136 | impl AppWidget for Scenery { 137 | type State = SceneryState; 138 | 139 | fn render( 140 | &self, 141 | area: Rect, 142 | buf: &mut Buffer, 143 | state: &mut Self::State, 144 | ctx: &mut RenderContext<'_>, 145 | ) -> Result<(), Error> { 146 | let t0 = SystemTime::now(); 147 | 148 | let layout = Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).split(area); 149 | 150 | Async1.render(area, buf, &mut state.async1, ctx)?; 151 | 152 | if ctx.g.error_dlg.active() { 153 | let err = MsgDialog::new().styles(ctx.g.theme.msg_dialog_style()); 154 | err.render( 155 | layout_middle( 156 | layout[0], 157 | Constraint::Percentage(20), 158 | Constraint::Percentage(20), 159 | Constraint::Length(1), 160 | Constraint::Length(1), 161 | ), 162 | buf, 163 | &mut ctx.g.error_dlg, 164 | ); 165 | } 166 | 167 | let el = t0.elapsed().unwrap_or(Duration::from_nanos(0)); 168 | ctx.g.status.status(1, format!("R {:.0?}", el).to_string()); 169 | 170 | let status_layout = 171 | Layout::horizontal([Constraint::Fill(61), Constraint::Fill(39)]).split(layout[1]); 172 | let status = StatusLine::new() 173 | .layout([ 174 | Constraint::Fill(1), 175 | Constraint::Length(8), 176 | Constraint::Length(8), 177 | ]) 178 | .styles(ctx.g.theme.statusline_style()); 179 | status.render(status_layout[1], buf, &mut ctx.g.status); 180 | 181 | Ok(()) 182 | } 183 | } 184 | 185 | impl AppState for SceneryState { 186 | fn init(&mut self, ctx: &mut AppContext<'_>) -> Result<(), Error> { 187 | ctx.focus = Some(FocusBuilder::for_container(&self.async1)); 188 | self.async1.init(ctx)?; 189 | Ok(()) 190 | } 191 | 192 | fn event( 193 | &mut self, 194 | event: &Async1Event, 195 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, Async1Event, Error>, 196 | ) -> Result, Error> { 197 | let t0 = SystemTime::now(); 198 | 199 | let mut r = match event { 200 | Async1Event::Event(event) => { 201 | let mut r = match &event { 202 | ct_event!(resized) => Control::Changed, 203 | ct_event!(key press CONTROL-'q') => Control::Quit, 204 | _ => Control::Continue, 205 | }; 206 | 207 | r = r.or_else(|| { 208 | if ctx.g.error_dlg.active() { 209 | ctx.g.error_dlg.handle(event, Dialog).into() 210 | } else { 211 | Control::Continue 212 | } 213 | }); 214 | 215 | let f = ctx.focus_mut().handle(event, Regular); 216 | ctx.queue(f); 217 | 218 | r 219 | } 220 | Async1Event::Rendered => { 221 | ctx.focus = Some(FocusBuilder::rebuild(&self.async1, ctx.focus.take())); 222 | Control::Continue 223 | } 224 | Async1Event::Message(s) => { 225 | ctx.g.status.status(0, &*s); 226 | Control::Changed 227 | } 228 | _ => Control::Continue, 229 | }; 230 | 231 | r = r.or_else_try(|| self.async1.event(event, ctx))?; 232 | 233 | let el = t0.elapsed()?; 234 | ctx.g.status.status(2, format!("E {:.0?}", el).to_string()); 235 | 236 | Ok(r) 237 | } 238 | 239 | fn error( 240 | &self, 241 | event: Error, 242 | ctx: &mut AppContext<'_>, 243 | ) -> Result, Error> { 244 | ctx.g.error_dlg.append(format!("{:?}", &*event).as_str()); 245 | Ok(Control::Changed) 246 | } 247 | } 248 | } 249 | 250 | pub mod async1 { 251 | use crate::{Async1Event, GlobalState, RenderContext}; 252 | use anyhow::Error; 253 | use rat_salsa::{AppState, AppWidget, Control}; 254 | use rat_widget::event::{HandleEvent, MenuOutcome, Regular}; 255 | use rat_widget::focus::{FocusBuilder, FocusContainer}; 256 | use rat_widget::menu::{MenuLine, MenuLineState}; 257 | use ratatui::buffer::Buffer; 258 | use ratatui::layout::{Constraint, Direction, Layout, Rect}; 259 | use ratatui::widgets::StatefulWidget; 260 | use std::time::Duration; 261 | 262 | #[derive(Debug)] 263 | pub(crate) struct Async1; 264 | 265 | #[derive(Debug, Default)] 266 | pub struct Async1State { 267 | pub menu: MenuLineState, 268 | } 269 | 270 | impl AppWidget for Async1 { 271 | type State = Async1State; 272 | 273 | fn render( 274 | &self, 275 | area: Rect, 276 | buf: &mut Buffer, 277 | state: &mut Self::State, 278 | ctx: &mut RenderContext<'_>, 279 | ) -> Result<(), Error> { 280 | // TODO: repaint_mask 281 | 282 | let r = Layout::new( 283 | Direction::Vertical, 284 | [ 285 | Constraint::Fill(1), // 286 | Constraint::Length(1), 287 | ], 288 | ) 289 | .split(area); 290 | 291 | let menu = MenuLine::new() 292 | .styles(ctx.g.theme.menu_style()) 293 | .item_parsed("_Simple Async") 294 | .item_parsed("_Long Running") 295 | .item_parsed("_Quit"); 296 | menu.render(r[1], buf, &mut state.menu); 297 | 298 | Ok(()) 299 | } 300 | } 301 | 302 | impl FocusContainer for Async1State { 303 | fn build(&self, builder: &mut FocusBuilder) { 304 | builder.widget(&self.menu); 305 | } 306 | } 307 | 308 | impl AppState for Async1State { 309 | fn init( 310 | &mut self, 311 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, Async1Event, Error>, 312 | ) -> Result<(), Error> { 313 | ctx.focus().first(); 314 | self.menu.select(Some(0)); 315 | Ok(()) 316 | } 317 | 318 | #[allow(unused_variables)] 319 | fn event( 320 | &mut self, 321 | event: &Async1Event, 322 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, Async1Event, Error>, 323 | ) -> Result, Error> { 324 | let r = match event { 325 | Async1Event::Event(event) => match self.menu.handle(event, Regular) { 326 | MenuOutcome::Activated(0) => { 327 | // spawn async task ... 328 | ctx.spawn_async(async { 329 | // to some awaiting 330 | tokio::time::sleep(Duration::from_secs(2)).await; 331 | 332 | Ok(Control::Message(Async1Event::FromAsync( 333 | "result of async computation".into(), 334 | ))) 335 | }); 336 | // that's it. 337 | Control::Continue 338 | } 339 | MenuOutcome::Activated(1) => { 340 | // spawn async task ... 341 | ctx.spawn_async_ext(|chan| async move { 342 | // to some awaiting 343 | let period = Duration::from_secs_f32(1.0 / 60.0); 344 | let mut interval = tokio::time::interval(period); 345 | 346 | for i in 0..1200 { 347 | interval.tick().await; 348 | _ = chan 349 | .send(Ok(Control::Message(Async1Event::AsyncTick(i)))) 350 | .await 351 | } 352 | 353 | Ok(Control::Message(Async1Event::AsyncTick(300))) 354 | }); 355 | // that's it. 356 | Control::Continue 357 | } 358 | MenuOutcome::Activated(2) => Control::Quit, 359 | v => v.into(), 360 | }, 361 | Async1Event::FromAsync(s) => { 362 | // receive result from async operation 363 | ctx.g.error_dlg.append(s); 364 | Control::Changed 365 | } 366 | Async1Event::AsyncTick(n) => { 367 | ctx.g.status.status(0, format!("--- {} ---", n)); 368 | Control::Changed 369 | } 370 | _ => Control::Continue, 371 | }; 372 | 373 | Ok(r) 374 | } 375 | } 376 | } 377 | 378 | fn setup_logging() -> Result<(), Error> { 379 | // _ = fs::remove_file("log.log"); 380 | fern::Dispatch::new() 381 | .format(|out, message, record| { 382 | out.finish(format_args!( 383 | "[{} {} {}]\n {}", 384 | humantime::format_rfc3339_seconds(SystemTime::now()), 385 | record.level(), 386 | record.target(), 387 | message 388 | )) 389 | }) 390 | .level(log::LevelFilter::Debug) 391 | .chain(fern::log_file("log.log")?) 392 | .apply()?; 393 | Ok(()) 394 | } 395 | -------------------------------------------------------------------------------- /examples/block.life: -------------------------------------------------------------------------------- 1 | 2 | [life] 3 | name = block 4 | rules = 23/3 5 | width = 4 6 | height = 4 7 | one.color = blue(1) 8 | 9 | [data] 10 | 0 = .... 11 | 1 = .XX. 12 | 2 = .XX. 13 | 3 = .... -------------------------------------------------------------------------------- /examples/cheat.md: -------------------------------------------------------------------------------- 1 | # Github Flavored Markup 2 | 3 | # General 4 | 5 | Up to three leading spaces still work as expected. The fourth 6 | makes everything a code block. 7 | 8 | # Thematic break {#fff .as .ddf fjfjj} 9 | 10 | At least 3 *, - or _ form a thematic break. Spaces inbetween 11 | are ok. 12 | 13 | # Headings I 14 | 15 | One to six # make a heading. 16 | 17 | # Headings II 18 | 19 | Underlining with = makes H1, underlining with - makes a H2. 20 | 21 | # Code blocks 22 | 23 | * Indent at least by 4. 24 | 25 | * Use a fence 26 | 27 | ``` 28 | CODE 29 | ``` 30 | 31 | or 32 | 33 | ~~~ 34 | CODE 35 | ~~~ 36 | 37 | Or inline `code`. 38 | 39 | CODE CODE 40 | CODE CODE 41 | 42 | # HTML blocks 43 | 44 | Use tags. 45 | 46 | # Links 47 | 48 | ## Links 49 | 50 | [link](/link_to "title") 51 | [link]( "title") 52 | 53 | 54 | 55 | ## Links to reference 56 | 57 | [use_ref][some] 58 | [some] 59 | 60 | ## Link references 61 | 62 | [some]: /links-somewhere 63 | 64 | ## Images 65 | 66 | Same as links with leading ! 67 | 68 | ![foo](/link_to_image) 69 | ... 70 | 71 | ## Footnotes 72 | 73 | [^1] 74 | 75 | [^1]: Footnote 76 | 77 | # Tables 78 | 79 | | header | :left | right: | :center: | 80 | |----------|------------|--------|----------| 81 | | text | text | text | text | 82 | | **bold** | _emphasis_ | | | 83 | 84 | # Block quote 85 | 86 | > Quote Quote Quote 87 | 88 | and also 89 | 90 | > [!NOTE] 91 | > [!TIP] 92 | > [!IMPORTANT] 93 | > [!WARNING] 94 | > [!CAUTION] 95 | 96 | # List 97 | 98 | * Bullet 99 | * Bullet 100 | 101 | + Bullet 102 | + Bullet 103 | 104 | - Bullet 105 | 106 | - [ ] Unchecked 107 | 108 | - [x] Checked 109 | 110 | 1) Numbered 111 | 2) Numbered 112 | 113 | 1. Indent 114 | 1. More Indent 115 | 1. Even more 116 | 2. Indent 117 | 3. Indent 118 | 119 | Definition 120 | : Define things here. 121 | -------------------------------------------------------------------------------- /examples/life.life: -------------------------------------------------------------------------------- 1 | [ life ] 2 | name = blink 3 | 4 | # Number of neighbours to stay alive / Number of neighbours to birth a new one. 5 | # (Each digit is a count) 6 | rules = 23/3 7 | 8 | # Ones. Default is 1,X,x; everything else is 0. 9 | one = 1Xx 10 | 11 | # Color of ones. 12 | # Numeric: RRGGBB in hex, rrr ggg bbb in dec. 13 | # Theme: black(n), white(n), gray(n), red(n), orange(n), yellow(n), 14 | # limegreen(n), green(n), bluegreen(n), cyan(n), blue(n), 15 | # deepblue(n), purple(n), magenta(n), redpink(n), primary(n), 16 | # secondary(n) 17 | # With 0<=n<=3. 18 | # Base: black, red, green, yellow, blue, magenta, cyan, gray, 19 | # dark gray, light red, light green, light yellow, light blue, 20 | # light magenta, light cyan, white 21 | # 22 | one.color = ffffff 23 | zero.color = 222222 24 | 25 | 26 | [ data ] 27 | 0 = ........ 28 | 1 = ..111... 29 | 2 = ........ 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/mdedit.md: -------------------------------------------------------------------------------- 1 | # MD-Edit 2 | 3 | ## Keyboard navigation 4 | 5 | * ESC - Jump to menu and back. 6 | * F5 - Jump to file-list and back. 7 | * F6 - Hide file-list. 8 | * F2 - Cheat sheet. 9 | * F1 - This document. 10 | 11 | ## Ctrl-W - Window navigation 12 | 13 | * Ctrl-W Left/Right - Jump between split windows. 14 | * Ctrl-W Tab/Backtab - Change focus. 15 | 16 | * Ctrl-W t - Jump to tabs. Use Left/Right to select a tab. 17 | * Ctrl-W t - The second time jumps back to the edit. Or just use Tab. 18 | 19 | * Ctrl-W s - Jump to the split. Use Left/Right to resize the split. 20 | Use Ctrl-Left/Right to move between multiple splits. 21 | 22 | * Ctrl-W c - Close the current window. 23 | * Ctrl-W x - Close the current window. 24 | 25 | * Ctrl-W d - Divide view, split horizontally. 26 | * Ctrl-W + - Divide view, split horizontally. 27 | 28 | ## Files 29 | 30 | * Ctrl+O - Open file. 31 | * Ctrl+N - New file. 32 | * Ctrl+S - Save file. (Autosaved when terminal-window looses focus.) 33 | 34 | ## Editing 35 | 36 | * Ctrl+C / Ctrl+X / Ctrl+V - Clipboard 37 | * Ctrl+Z / Ctrl+Shift+Z - Undo/Redo. 38 | * Ctrl+D - Duplicate line. 39 | * Ctrl+Y - Remove line. 40 | * Ctrl+Backspace, Ctrl+Delete - Remove word 41 | * Alt+Backspace, Alt+Delete - Remove word 42 | * Tab/BackTab - Indent/Dedent selection. Insert tab otherwise. 43 | 44 | ## Table 45 | 46 | * Enter - Line break within the table. At the end creates an empty 47 | duplicate of the last row. If you have just the header enter will 48 | create the necessary underlines. 49 | * Tab/Backtab - Navigate between cells. 50 | 51 | 52 | ## Formatting 53 | 54 | * Alt-F - Formats the item at the cursor position, or everything 55 | selected. 56 | * Alt+Shift+F - Alternate format. Currently, formats a table to 57 | equal column widths. 58 | * Alt+1..=6 - Make header. 59 | 60 | -------------------------------------------------------------------------------- /examples/mdedit_parts/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::mdedit_parts::dump::{md_dump, md_dump_styles}; 2 | use crate::mdedit_parts::format::md_format; 3 | use crate::mdedit_parts::operations::{md_backtab, md_line_break, md_make_header, md_tab}; 4 | use rat_widget::event::{ct_event, flow, HandleEvent, Regular, TextOutcome}; 5 | use rat_widget::focus::HasFocus; 6 | use rat_widget::text::upos_type; 7 | use rat_widget::textarea::TextAreaState; 8 | use unicode_segmentation::UnicodeSegmentation; 9 | 10 | pub mod dump; 11 | pub mod format; 12 | pub mod operations; 13 | pub mod parser; 14 | pub mod styles; 15 | pub mod test_markdown; 16 | 17 | // qualifier for markdown-editing. 18 | #[derive(Debug)] 19 | pub struct MarkDown; 20 | 21 | impl HandleEvent for TextAreaState { 22 | fn handle(&mut self, event: &crossterm::event::Event, qualifier: MarkDown) -> TextOutcome { 23 | if self.is_focused() { 24 | flow!(match event { 25 | ct_event!(key press ALT-'f') => md_format(self, false), 26 | ct_event!(key press ALT_SHIFT-'F') => md_format(self, true), 27 | ct_event!(key press ALT-'d') => md_dump(self), 28 | ct_event!(key press ALT-'s') => md_dump_styles(self), 29 | 30 | ct_event!(key press ALT-'1') => md_make_header(self, 1), 31 | ct_event!(key press ALT-'2') => md_make_header(self, 2), 32 | ct_event!(key press ALT-'3') => md_make_header(self, 3), 33 | ct_event!(key press ALT-'4') => md_make_header(self, 4), 34 | ct_event!(key press ALT-'5') => md_make_header(self, 5), 35 | ct_event!(key press ALT-'6') => md_make_header(self, 6), 36 | 37 | ct_event!(keycode press Enter) => md_line_break(self), 38 | ct_event!(keycode press Tab) => md_tab(self), 39 | ct_event!(keycode press SHIFT-BackTab) => md_backtab(self), 40 | _ => TextOutcome::Continue, 41 | }); 42 | } 43 | 44 | self.handle(event, Regular) 45 | } 46 | } 47 | 48 | /// Length as grapheme count, excluding line breaks. 49 | fn str_line_len(s: &str) -> upos_type { 50 | let it = s.graphemes(true); 51 | it.filter(|c| *c != "\n" && *c != "\r\n").count() as upos_type 52 | } 53 | 54 | /// Length as grapheme count. 55 | fn str_len(s: &str) -> upos_type { 56 | let it = s.graphemes(true); 57 | it.count() as upos_type 58 | } 59 | -------------------------------------------------------------------------------- /examples/mdedit_parts/operations.rs: -------------------------------------------------------------------------------- 1 | use crate::mdedit_parts::parser::{parse_md_header, parse_md_item, parse_md_row}; 2 | use crate::mdedit_parts::str_line_len; 3 | use crate::mdedit_parts::styles::MDStyle; 4 | use rat_widget::event::TextOutcome; 5 | use rat_widget::text::{upos_type, TextPosition, TextRange}; 6 | use rat_widget::textarea::TextAreaState; 7 | use std::ops::Range; 8 | use unicode_segmentation::UnicodeSegmentation; 9 | 10 | pub fn md_make_header(state: &mut TextAreaState, header: u8) -> TextOutcome { 11 | if let Some(_) = md_paragraph(state) { 12 | let cursor = state.cursor(); 13 | let pos = TextPosition::new(0, cursor.y); 14 | 15 | let insert_txt = format!("{} ", "#".repeat(header as usize)); 16 | 17 | state.value.insert_str(pos, &insert_txt).expect("valid_pos"); 18 | 19 | TextOutcome::TextChanged 20 | } else if let Some((header_byte, header_range)) = md_header(state) { 21 | let cursor = state.cursor(); 22 | 23 | let txt = state.str_slice_byte(header_byte.clone()); 24 | let md_header = parse_md_header(header_byte.start, txt.as_ref()).expect("md header"); 25 | 26 | let (new_txt, new_cursor) = if md_header.header != header { 27 | ( 28 | format!("{} {}", "#".repeat(header as usize), md_header.text), 29 | TextPosition::new( 30 | cursor.x - md_header.header as upos_type + header as upos_type, 31 | cursor.y, 32 | ), 33 | ) 34 | } else { 35 | ( 36 | format!("{}", md_header.text), 37 | TextPosition::new(cursor.x - md_header.header as upos_type, cursor.y), 38 | ) 39 | }; 40 | 41 | state.begin_undo_seq(); 42 | state 43 | .value 44 | .remove_str_range(header_range) 45 | .expect("valid_range"); 46 | state 47 | .value 48 | .insert_str(header_range.start, &new_txt) 49 | .expect("valid_pos"); 50 | state.set_cursor(new_cursor, false); 51 | state.end_undo_seq(); 52 | 53 | TextOutcome::TextChanged 54 | } else { 55 | TextOutcome::Unchanged 56 | } 57 | } 58 | 59 | pub fn md_tab(state: &mut TextAreaState) -> TextOutcome { 60 | if is_md_table(state) { 61 | let cursor = state.cursor(); 62 | let row = state.line_at(cursor.y); 63 | let x = next_tab_md_row(row.as_ref(), cursor.x); 64 | state.set_cursor((x, cursor.y), false); 65 | state.set_move_col(Some(x)); 66 | 67 | TextOutcome::TextChanged 68 | } else if is_md_item(state) { 69 | if state.has_selection() { 70 | return TextOutcome::Continue; 71 | } 72 | 73 | let cursor = state.cursor(); 74 | 75 | let (item_byte, item_range) = md_item(state).expect("md item"); 76 | let indent_x = if item_range.start.y < cursor.y { 77 | let item_str = state.str_slice_byte(item_byte.clone()); 78 | let item = parse_md_item(item_byte.start, item_str.as_ref()).expect("md item"); 79 | state.byte_pos(item.text_bytes.start).x 80 | } else if let Some((prev_byte, prev_range)) = md_prev_item(state) { 81 | let prev_str = state.str_slice_byte(prev_byte.clone()); 82 | let prev_item = parse_md_item(prev_byte.start, prev_str.as_ref()).expect("md item"); 83 | state.byte_pos(prev_item.text_bytes.start).x 84 | } else { 85 | 0 86 | }; 87 | 88 | if cursor.x < indent_x { 89 | state 90 | .value 91 | .insert_str(cursor, &(" ".repeat((indent_x - cursor.x) as usize))) 92 | .expect("fine"); 93 | TextOutcome::TextChanged 94 | } else { 95 | TextOutcome::Continue 96 | } 97 | } else { 98 | TextOutcome::Continue 99 | } 100 | } 101 | 102 | pub fn md_backtab(state: &mut TextAreaState) -> TextOutcome { 103 | if is_md_table(state) { 104 | let cursor = state.cursor(); 105 | 106 | let row_str = state.line_at(cursor.y); 107 | let x = prev_tab_md_row(row_str.as_ref(), cursor.x); 108 | 109 | state.set_cursor((x, cursor.y), false); 110 | state.set_move_col(Some(x)); 111 | TextOutcome::TextChanged 112 | } else { 113 | TextOutcome::Continue 114 | } 115 | } 116 | 117 | pub fn md_line_break(state: &mut TextAreaState) -> TextOutcome { 118 | let cursor = state.cursor(); 119 | if is_md_table(state) { 120 | let line = state.line_at(cursor.y); 121 | if cursor.x == state.line_width(cursor.y) { 122 | let (x, row) = empty_md_row(line.as_ref(), state.newline()); 123 | state.insert_str(row); 124 | state.set_cursor((x, cursor.y + 1), false); 125 | TextOutcome::TextChanged 126 | } else { 127 | let (x, row) = split_md_row(line.as_ref(), cursor.x, state.newline()); 128 | state.begin_undo_seq(); 129 | state.delete_range(TextRange::new((0, cursor.y), (0, cursor.y + 1))); 130 | state.insert_str(row); 131 | state.set_cursor((x, cursor.y + 1), false); 132 | state.end_undo_seq(); 133 | TextOutcome::TextChanged 134 | } 135 | } else { 136 | let cursor = state.cursor(); 137 | if cursor.x == state.line_width(cursor.y) { 138 | let (maybe_table, maybe_header) = is_md_maybe_table(state); 139 | if maybe_header { 140 | let line = state.line_at(cursor.y); 141 | let (x, row) = empty_md_row(line.as_ref(), state.newline()); 142 | state.insert_str(row); 143 | state.set_cursor((x, cursor.y + 1), false); 144 | TextOutcome::TextChanged 145 | } else if maybe_table { 146 | let line = state.line_at(cursor.y); 147 | let (x, row) = create_md_title(line.as_ref(), state.newline()); 148 | state.insert_str(row); 149 | state.set_cursor((x, cursor.y + 1), false); 150 | TextOutcome::TextChanged 151 | } else { 152 | TextOutcome::Continue 153 | } 154 | } else { 155 | TextOutcome::Continue 156 | } 157 | } 158 | } 159 | 160 | // duplicate as empty row 161 | fn empty_md_row(txt: &str, newline: &str) -> (upos_type, String) { 162 | let row = parse_md_row(0, txt, 0); 163 | let mut new_row = String::new(); 164 | new_row.push_str(newline); 165 | new_row.push('|'); 166 | for idx in 1..row.row.len() - 1 { 167 | for g in row.row[idx].txt.graphemes(true) { 168 | new_row.push(' '); 169 | } 170 | new_row.push('|'); 171 | } 172 | 173 | let x = if row.row.len() > 1 && row.row[1].txt.len() > 0 { 174 | str_line_len(row.row[0].txt) + 1 + 1 175 | } else { 176 | str_line_len(row.row[0].txt) + 1 177 | }; 178 | 179 | (x, new_row) 180 | } 181 | 182 | // add a line break 183 | fn split_md_row(txt: &str, cursor: upos_type, newline: &str) -> (upos_type, String) { 184 | let row = parse_md_row(0, txt, 0); 185 | 186 | let mut tmp0 = String::new(); 187 | let mut tmp1 = String::new(); 188 | let mut tmp_pos = 0; 189 | tmp0.push('|'); 190 | tmp1.push('|'); 191 | for row in &row.row[1..row.row.len() - 1] { 192 | if row.txt_graphemes.contains(&cursor) { 193 | tmp_pos = row.txt_graphemes.start + 1; 194 | 195 | let mut pos = row.txt_graphemes.start; 196 | if cursor > row.txt_graphemes.start { 197 | tmp1.push(' '); 198 | } 199 | for g in row.txt.graphemes(true) { 200 | if pos < cursor { 201 | tmp0.push_str(g); 202 | } else { 203 | tmp1.push_str(g); 204 | } 205 | pos += 1; 206 | } 207 | pos = row.txt_graphemes.start; 208 | for g in row.txt.graphemes(true) { 209 | if pos < cursor { 210 | // omit one blank 211 | if pos != row.txt_graphemes.start || cursor == row.txt_graphemes.start { 212 | tmp1.push(' '); 213 | } 214 | } else { 215 | tmp0.push(' '); 216 | } 217 | pos += 1; 218 | } 219 | } else if row.txt_graphemes.start < cursor { 220 | tmp0.push_str(row.txt); 221 | tmp1.push_str(" ".repeat(row.txt_graphemes.len()).as_str()); 222 | } else if row.txt_graphemes.start >= cursor { 223 | tmp0.push_str(" ".repeat(row.txt_graphemes.len()).as_str()); 224 | tmp1.push_str(row.txt); 225 | } 226 | 227 | tmp0.push('|'); 228 | tmp1.push('|'); 229 | } 230 | tmp0.push_str(newline); 231 | tmp0.push_str(tmp1.as_str()); 232 | tmp0.push_str(newline); 233 | 234 | (tmp_pos, tmp0) 235 | } 236 | 237 | // create underlines under the header 238 | fn create_md_title(txt: &str, newline: &str) -> (upos_type, String) { 239 | let row = parse_md_row(0, txt, 0); 240 | 241 | let mut new_row = String::new(); 242 | new_row.push_str(newline); 243 | new_row.push_str(row.row[0].txt); 244 | new_row.push('|'); 245 | for idx in 1..row.row.len() - 1 { 246 | for g in row.row[idx].txt.graphemes(true) { 247 | new_row.push('-'); 248 | } 249 | new_row.push('|'); 250 | } 251 | 252 | let len = str_line_len(&new_row); 253 | 254 | (len, new_row) 255 | } 256 | 257 | fn is_md_table(state: &TextAreaState) -> bool { 258 | let cursor = state.cursor(); 259 | let cursor_byte = state.byte_at(cursor).start; 260 | state 261 | .style_match(cursor_byte, MDStyle::Table as usize) 262 | .is_some() 263 | } 264 | 265 | fn is_md_maybe_table(state: &TextAreaState) -> (bool, bool) { 266 | let mut gr = state.line_graphemes(state.cursor().y); 267 | let (maybe_table, maybe_header) = if let Some(first) = gr.next() { 268 | if first == "|" { 269 | if let Some(second) = gr.next() { 270 | if second == "-" { 271 | (true, true) 272 | } else { 273 | (true, false) 274 | } 275 | } else { 276 | (true, false) 277 | } 278 | } else { 279 | (false, false) 280 | } 281 | } else { 282 | (false, false) 283 | }; 284 | (maybe_table, maybe_header) 285 | } 286 | 287 | fn is_md_item(state: &TextAreaState) -> bool { 288 | let cursor = state.cursor(); 289 | let cursor_byte = state.byte_at(cursor).start; 290 | state 291 | .style_match(cursor_byte, MDStyle::Item as usize) 292 | .is_some() 293 | } 294 | 295 | fn next_tab_md_row(txt: &str, pos: upos_type) -> upos_type { 296 | let row = parse_md_row(0, txt, pos); 297 | if row.cursor_cell + 1 < row.row.len() { 298 | row.row[row.cursor_cell + 1].txt_graphemes.start 299 | } else { 300 | pos 301 | } 302 | } 303 | 304 | fn prev_tab_md_row(txt: &str, pos: upos_type) -> upos_type { 305 | let row = parse_md_row(0, txt, pos); 306 | if row.cursor_cell > 0 { 307 | row.row[row.cursor_cell - 1].txt_graphemes.start 308 | } else { 309 | pos 310 | } 311 | } 312 | 313 | fn md_paragraph(state: &TextAreaState) -> Option<(Range, TextRange)> { 314 | let cursor = state.cursor(); 315 | let cursor_byte = state.byte_at(cursor).start; 316 | 317 | let para_byte = state.style_match(cursor_byte, MDStyle::Paragraph as usize); 318 | 319 | if let Some(para_byte) = para_byte { 320 | Some((para_byte.clone(), state.byte_range(para_byte))) 321 | } else { 322 | None 323 | } 324 | } 325 | 326 | fn md_header(state: &TextAreaState) -> Option<(Range, TextRange)> { 327 | let cursor = state.cursor(); 328 | let cursor_byte = state.byte_at(cursor).start; 329 | 330 | let mut styles = Vec::new(); 331 | state.styles_at(cursor_byte, &mut styles); 332 | 333 | let header_byte = styles.iter().find_map(|(r, s)| { 334 | let style = MDStyle::try_from(*s).expect("style"); 335 | if matches!( 336 | style, 337 | MDStyle::Heading1 338 | | MDStyle::Heading2 339 | | MDStyle::Heading3 340 | | MDStyle::Heading4 341 | | MDStyle::Heading5 342 | | MDStyle::Heading6 343 | ) { 344 | Some(r.clone()) 345 | } else { 346 | None 347 | } 348 | }); 349 | 350 | if let Some(header_byte) = header_byte { 351 | Some((header_byte.clone(), state.byte_range(header_byte))) 352 | } else { 353 | None 354 | } 355 | } 356 | 357 | fn md_item(state: &TextAreaState) -> Option<(Range, TextRange)> { 358 | let cursor = state.cursor(); 359 | let cursor_byte = state.byte_at(cursor).start; 360 | 361 | let item_byte = state.style_match(cursor_byte, MDStyle::Item as usize); 362 | 363 | if let Some(item_byte) = item_byte { 364 | Some((item_byte.clone(), state.byte_range(item_byte))) 365 | } else { 366 | None 367 | } 368 | } 369 | 370 | fn md_prev_item(state: &TextAreaState) -> Option<(Range, TextRange)> { 371 | let cursor = state.cursor(); 372 | let cursor_byte = state.byte_at(cursor).start; 373 | 374 | let item_byte = state.style_match(cursor_byte, MDStyle::Item as usize); 375 | let list_byte = state.style_match(cursor_byte, MDStyle::List as usize); 376 | 377 | if let Some(list_byte) = list_byte { 378 | if let Some(item_byte) = item_byte { 379 | let mut sty = Vec::new(); 380 | state.styles_in(list_byte.start..item_byte.start, &mut sty); 381 | 382 | let prev = sty.iter().filter(|v| v.1 == MDStyle::Item as usize).last(); 383 | 384 | if let Some((prev_bytes, _)) = prev { 385 | let prev = state.byte_range(prev_bytes.clone()); 386 | Some((prev_bytes.clone(), prev)) 387 | } else { 388 | None 389 | } 390 | } else { 391 | None 392 | } 393 | } else { 394 | None 395 | } 396 | } 397 | 398 | fn md_next_item(state: &TextAreaState) -> Option<(Range, TextRange)> { 399 | let cursor = state.cursor(); 400 | let cursor_byte = state.byte_at(cursor).start; 401 | 402 | let item_byte = state.style_match(cursor_byte, MDStyle::Item as usize); 403 | let list_byte = state.style_match(cursor_byte, MDStyle::List as usize); 404 | 405 | if let Some(list_byte) = list_byte { 406 | if let Some(item_byte) = item_byte { 407 | let mut sty = Vec::new(); 408 | state.styles_in(item_byte.end..list_byte.end, &mut sty); 409 | 410 | let next = sty.iter().filter(|v| v.1 == MDStyle::Item as usize).next(); 411 | 412 | if let Some((next_bytes, _)) = next { 413 | let next = state.byte_range(next_bytes.clone()); 414 | Some((next_bytes.clone(), next)) 415 | } else { 416 | None 417 | } 418 | } else { 419 | None 420 | } 421 | } else { 422 | None 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /examples/mdedit_parts/parser.rs: -------------------------------------------------------------------------------- 1 | use rat_widget::text::upos_type; 2 | use std::ops::Range; 3 | use unicode_segmentation::UnicodeSegmentation; 4 | 5 | #[derive(Debug)] 6 | pub struct MDHeader<'a> { 7 | pub header: u8, 8 | pub prefix: &'a str, 9 | pub tag: &'a str, 10 | pub text: &'a str, 11 | pub text_byte: Range, 12 | } 13 | 14 | pub fn parse_md_header(relocate: usize, txt: &str) -> Option> { 15 | let mut mark_prefix_end = 0; 16 | let mut mark_tag_start = 0; 17 | let mut mark_tag_end = 0; 18 | let mut mark_text_start = 0; 19 | 20 | #[derive(Debug, PartialEq)] 21 | enum It { 22 | Leading, 23 | Tag, 24 | LeadingText, 25 | Text, 26 | End, 27 | Fail, 28 | } 29 | 30 | let mut state = It::Leading; 31 | for (idx, c) in txt.bytes().enumerate() { 32 | if state == It::Leading { 33 | if c == b' ' || c == b'\t' { 34 | mark_prefix_end = idx + 1; 35 | mark_tag_start = idx + 1; 36 | mark_tag_end = idx + 1; 37 | mark_text_start = idx + 1; 38 | } else if c == b'#' { 39 | mark_prefix_end = idx; 40 | mark_tag_start = idx; 41 | mark_tag_end = idx + 1; 42 | mark_text_start = idx + 1; 43 | state = It::Tag; 44 | } else { 45 | state = It::Fail; 46 | break; 47 | } 48 | } else if state == It::Tag { 49 | if c == b'#' { 50 | mark_tag_end = idx; 51 | mark_text_start = idx + 1; 52 | } else { 53 | mark_tag_end = idx; 54 | mark_text_start = idx + 1; 55 | state = It::LeadingText; 56 | } 57 | } else if state == It::LeadingText { 58 | if c == b' ' || c == b'\t' { 59 | mark_text_start = idx + 1; 60 | // ok 61 | } else { 62 | mark_text_start = idx; 63 | state = It::Text; 64 | } 65 | } else if state == It::Text { 66 | state = It::End; 67 | break; 68 | } 69 | } 70 | 71 | if state == It::Fail { 72 | return None; 73 | } 74 | 75 | Some(MDHeader { 76 | header: (mark_tag_end - mark_tag_start) as u8, 77 | prefix: &txt[..mark_prefix_end], 78 | tag: &txt[mark_tag_start..mark_tag_end], 79 | text: &txt[mark_text_start..], 80 | text_byte: relocate + mark_text_start..relocate + txt.len(), 81 | }) 82 | } 83 | 84 | #[derive(Debug)] 85 | pub struct MDLinkRef<'a> { 86 | pub prefix: &'a str, 87 | pub tag: &'a str, 88 | pub link: &'a str, 89 | pub title: &'a str, 90 | pub suffix: &'a str, 91 | } 92 | 93 | pub fn parse_md_link_ref(relocate: usize, txt: &str) -> Option> { 94 | let mut mark_prefix_end = 0; 95 | let mut mark_tag_start = 0; 96 | let mut mark_tag_end = 0; 97 | let mut mark_link_start = 0; 98 | let mut mark_link_end = 0; 99 | let mut mark_title_start = 0; 100 | let mut mark_title_end = 0; 101 | 102 | #[derive(Debug, PartialEq)] 103 | enum It { 104 | Leading, 105 | Tag, 106 | AfterTag, 107 | LeadingLink, 108 | BracketLink, 109 | Link, 110 | LinkEsc, 111 | LeadingTitle, 112 | TitleSingle, 113 | TitleSingleEsc, 114 | TitleDouble, 115 | TitleDoubleEsc, 116 | End, 117 | Fail, 118 | } 119 | 120 | let mut state = It::Leading; 121 | for (idx, c) in txt.bytes().enumerate() { 122 | if state == It::Leading { 123 | if c == b'[' { 124 | mark_prefix_end = idx; 125 | mark_tag_start = idx + 1; 126 | mark_tag_end = idx + 1; 127 | mark_link_start = idx + 1; 128 | mark_link_end = idx + 1; 129 | mark_title_start = idx + 1; 130 | mark_title_end = idx + 1; 131 | state = It::Tag; 132 | } else if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' { 133 | mark_prefix_end = idx + 1; 134 | mark_tag_start = idx + 1; 135 | mark_tag_end = idx + 1; 136 | mark_link_start = idx + 1; 137 | mark_link_end = idx + 1; 138 | mark_title_start = idx + 1; 139 | mark_title_end = idx + 1; 140 | } else { 141 | state = It::Fail; 142 | break; 143 | } 144 | } else if state == It::Tag { 145 | if c == b']' { 146 | mark_tag_end = idx; 147 | mark_link_start = idx + 1; 148 | mark_link_end = idx + 1; 149 | mark_title_start = idx + 1; 150 | mark_title_end = idx + 1; 151 | state = It::AfterTag; 152 | } else { 153 | mark_tag_end = idx; 154 | mark_link_start = idx + 1; 155 | mark_link_end = idx + 1; 156 | mark_title_start = idx + 1; 157 | mark_title_end = idx + 1; 158 | } 159 | } else if state == It::AfterTag { 160 | if c == b':' { 161 | mark_link_start = idx + 1; 162 | mark_link_end = idx + 1; 163 | mark_title_start = idx + 1; 164 | mark_title_end = idx + 1; 165 | state = It::LeadingLink; 166 | } else { 167 | state = It::Fail; 168 | break; 169 | } 170 | } else if state == It::LeadingLink { 171 | if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' { 172 | mark_link_start = idx + 1; 173 | mark_link_end = idx + 1; 174 | mark_title_start = idx + 1; 175 | mark_title_end = idx + 1; 176 | // ok 177 | } else if c == b'<' { 178 | mark_link_start = idx + 1; 179 | mark_link_end = idx + 1; 180 | mark_title_start = idx + 1; 181 | mark_title_end = idx + 1; 182 | state = It::BracketLink; 183 | } else { 184 | mark_link_start = idx; 185 | mark_link_end = idx; 186 | mark_title_start = idx; 187 | mark_title_end = idx; 188 | state = It::Link; 189 | } 190 | } else if state == It::BracketLink { 191 | if c == b'>' { 192 | mark_link_end = idx; 193 | mark_title_start = idx + 1; 194 | mark_title_end = idx + 1; 195 | state = It::LeadingTitle; 196 | } else { 197 | mark_link_end = idx; 198 | mark_title_start = idx; 199 | mark_title_end = idx; 200 | } 201 | } else if state == It::Link { 202 | if c == b'\\' { 203 | mark_link_end = idx; 204 | mark_title_start = idx; 205 | mark_title_end = idx; 206 | state = It::LinkEsc; 207 | } else if c == b'\n' || c == b'\r' { 208 | mark_link_end = idx; 209 | mark_title_start = idx + 1; 210 | mark_title_end = idx + 1; 211 | state = It::LeadingTitle; 212 | } else if c == b'\'' { 213 | mark_link_end = idx; 214 | mark_title_start = idx + 1; 215 | mark_title_end = idx + 1; 216 | state = It::TitleSingle; 217 | } else if c == b'"' { 218 | mark_link_end = idx; 219 | mark_title_start = idx + 1; 220 | mark_title_end = idx + 1; 221 | state = It::TitleDouble; 222 | } else { 223 | mark_link_end = idx; 224 | mark_title_start = idx; 225 | mark_title_end = idx; 226 | } 227 | } else if state == It::LinkEsc { 228 | mark_link_end = idx; 229 | mark_title_start = idx; 230 | mark_title_end = idx; 231 | state = It::Link; 232 | } else if state == It::LeadingTitle { 233 | if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' { 234 | mark_title_start = idx + 1; 235 | mark_title_end = idx + 1; 236 | } else if c == b'\'' { 237 | mark_title_start = idx + 1; 238 | mark_title_end = idx + 1; 239 | state = It::TitleSingle; 240 | } else if c == b'"' { 241 | mark_title_start = idx + 1; 242 | mark_title_end = idx + 1; 243 | state = It::TitleDouble; 244 | } else { 245 | // no title, just suffix 246 | mark_title_start = idx; 247 | mark_title_end = idx; 248 | state = It::End; 249 | break; 250 | } 251 | } else if state == It::TitleSingle { 252 | if c == b'\'' { 253 | mark_title_end = idx; 254 | state = It::End; 255 | break; 256 | } else if c == b'\\' { 257 | mark_title_end = idx; 258 | state = It::TitleSingleEsc; 259 | } else { 260 | mark_title_end = idx; 261 | } 262 | } else if state == It::TitleSingleEsc { 263 | mark_title_end = idx; 264 | state = It::TitleSingle; 265 | } else if state == It::TitleDouble { 266 | if c == b'"' { 267 | mark_title_end = idx; 268 | state = It::End; 269 | break; 270 | } else if c == b'\\' { 271 | mark_title_end = idx; 272 | state = It::TitleDoubleEsc; 273 | } else { 274 | mark_title_end = idx; 275 | } 276 | } else if state == It::TitleDoubleEsc { 277 | mark_title_end = idx; 278 | state = It::TitleDouble; 279 | } 280 | } 281 | 282 | if state == It::Fail { 283 | return None; 284 | } 285 | 286 | Some(MDLinkRef { 287 | prefix: &txt[..mark_prefix_end], 288 | tag: &txt[mark_tag_start..mark_tag_end], 289 | link: &txt[mark_link_start..mark_link_end], 290 | title: &txt[mark_title_start..mark_title_end], 291 | suffix: &txt[mark_title_end..], 292 | }) 293 | } 294 | 295 | // parse a single list item into marker and text. 296 | #[derive(Debug)] 297 | pub struct MDItem<'a> { 298 | pub prefix: &'a str, 299 | pub mark_bytes: Range, 300 | pub mark: &'a str, 301 | pub mark_suffix: &'a str, 302 | pub mark_nr: Option, 303 | pub text_prefix: &'a str, 304 | pub text_bytes: Range, 305 | pub text: &'a str, 306 | } 307 | 308 | pub fn parse_md_item(relocate: usize, txt: &str) -> Option> { 309 | let mut mark_byte = 0; 310 | let mut mark_suffix_byte = 0; 311 | let mut text_prefix_byte = 0; 312 | let mut text_byte = 0; 313 | 314 | let mut mark_nr = None; 315 | 316 | #[derive(Debug, PartialEq)] 317 | enum It { 318 | Leading, 319 | OrderedMark, 320 | TextLeading, 321 | Fail, 322 | End, 323 | } 324 | 325 | let mut state = It::Leading; 326 | for (idx, c) in txt.bytes().enumerate() { 327 | if state == It::Leading { 328 | if c == b'+' || c == b'-' || c == b'*' { 329 | mark_byte = idx; 330 | mark_suffix_byte = idx + 1; 331 | text_prefix_byte = idx + 1; 332 | text_byte = idx + 1; 333 | state = It::TextLeading; 334 | } else if c.is_ascii_digit() { 335 | mark_byte = idx; 336 | state = It::OrderedMark; 337 | } else if c == b' ' || c == b'\t' { 338 | // ok 339 | } else { 340 | state = It::Fail; 341 | break; 342 | } 343 | } else if state == It::OrderedMark { 344 | if c.is_ascii_digit() { 345 | // ok 346 | } else if c == b'.' || c == b')' { 347 | mark_suffix_byte = idx; 348 | text_prefix_byte = idx + 1; 349 | text_byte = idx + 1; 350 | mark_nr = Some( 351 | txt[mark_byte..mark_suffix_byte] 352 | .parse::() 353 | .expect("nr"), 354 | ); 355 | state = It::TextLeading; 356 | } else { 357 | state = It::Fail; 358 | break; 359 | } 360 | } else if state == It::TextLeading { 361 | if c == b' ' || c == b'\t' { 362 | // ok 363 | } else { 364 | text_byte = idx; 365 | state = It::End; 366 | break; 367 | } 368 | } 369 | } 370 | 371 | if state == It::Fail { 372 | return None; 373 | } 374 | 375 | Some(MDItem { 376 | prefix: &txt[0..mark_byte], 377 | mark_bytes: relocate + mark_byte..relocate + text_prefix_byte, 378 | mark: &txt[mark_byte..mark_suffix_byte], 379 | mark_suffix: &txt[mark_suffix_byte..text_prefix_byte], 380 | mark_nr, 381 | text_prefix: &txt[text_prefix_byte..text_byte], 382 | text_bytes: relocate + text_byte..relocate + txt.len(), 383 | text: &txt[text_byte..], 384 | }) 385 | } 386 | 387 | #[derive(Debug)] 388 | pub struct MDCell<'a> { 389 | pub txt: &'a str, 390 | pub txt_graphemes: Range, 391 | pub txt_bytes: Range, 392 | } 393 | 394 | #[derive(Debug)] 395 | pub struct MDRow<'a> { 396 | pub row: Vec>, 397 | // cursor cell-nr 398 | pub cursor_cell: usize, 399 | // cursor grapheme offset into the cell 400 | pub cursor_offset: upos_type, 401 | // cursor byte offset into the cell 402 | pub cursor_byte_offset: usize, 403 | } 404 | 405 | // split single row. translate x-position to cell+cell_offset. 406 | // __info__: returns the string before the first | and the string after the last | too!! 407 | pub fn parse_md_row(relocate: usize, txt: &str, x: upos_type) -> MDRow<'_> { 408 | let mut tmp = MDRow { 409 | row: Default::default(), 410 | cursor_cell: 0, 411 | cursor_offset: 0, 412 | cursor_byte_offset: 0, 413 | }; 414 | 415 | let mut grapheme_start = 0; 416 | let mut grapheme_last = 0; 417 | let mut esc = false; 418 | let mut cell_offset = 0; 419 | let mut cell_byte_start = 0; 420 | for (idx, (byte_idx, c)) in txt.grapheme_indices(true).enumerate() { 421 | if idx == x as usize { 422 | tmp.cursor_cell = tmp.row.len(); 423 | tmp.cursor_offset = cell_offset; 424 | tmp.cursor_byte_offset = byte_idx - cell_byte_start; 425 | } 426 | 427 | if c == "\\" { 428 | cell_offset += 1; 429 | esc = true; 430 | } else if c == "|" && !esc { 431 | cell_offset = 0; 432 | tmp.row.push(MDCell { 433 | txt: &txt[cell_byte_start..byte_idx], 434 | txt_graphemes: grapheme_start..idx as upos_type, 435 | txt_bytes: relocate + cell_byte_start..relocate + byte_idx, 436 | }); 437 | cell_byte_start = byte_idx + 1; 438 | grapheme_start = idx as upos_type + 1; 439 | } else { 440 | cell_offset += 1; 441 | esc = false; 442 | } 443 | 444 | grapheme_last = idx as upos_type; 445 | } 446 | 447 | tmp.row.push(MDCell { 448 | txt: &txt[cell_byte_start..txt.len()], 449 | txt_graphemes: grapheme_start..grapheme_last, 450 | txt_bytes: relocate + cell_byte_start..relocate + txt.len(), 451 | }); 452 | 453 | tmp 454 | } 455 | 456 | // parse quoted text 457 | #[derive(Debug)] 458 | pub struct MDBlockQuote<'a> { 459 | pub quote: &'a str, 460 | pub text_prefix: &'a str, 461 | pub text_bytes: Range, 462 | pub text: &'a str, 463 | } 464 | 465 | pub fn parse_md_block_quote(relocate: usize, txt: &str) -> Option> { 466 | let mut quote_byte = 0; 467 | let mut text_prefix_byte = 0; 468 | let mut text_byte = 0; 469 | 470 | #[derive(Debug, PartialEq)] 471 | enum It { 472 | Leading, 473 | TextLeading, 474 | Text, 475 | NewLine, 476 | End, 477 | Fail, 478 | } 479 | 480 | let mut state = It::Leading; 481 | for (idx, c) in txt.bytes().enumerate() { 482 | if state == It::Leading { 483 | if c == b'>' { 484 | quote_byte = idx; 485 | text_prefix_byte = idx + 1; 486 | state = It::TextLeading; 487 | } else if c == b' ' || c == b'\t' { 488 | // ok 489 | } else { 490 | state = It::Fail; 491 | break; 492 | } 493 | } else if state == It::TextLeading { 494 | if c == b' ' || c == b'\t' { 495 | // ok 496 | } else { 497 | text_byte = idx; 498 | state = It::Text; 499 | } 500 | } else if state == It::Text { 501 | state = It::End; 502 | break; 503 | } 504 | } 505 | 506 | if state == It::Fail { 507 | return None; 508 | } 509 | 510 | Some(MDBlockQuote { 511 | quote: &txt[quote_byte..quote_byte + 1], 512 | text_prefix: &txt[text_prefix_byte..text_byte], 513 | text_bytes: relocate + text_byte..relocate + txt.len(), 514 | text: &txt[text_byte..txt.len()], 515 | }) 516 | } 517 | -------------------------------------------------------------------------------- /examples/mdedit_parts/styles.rs: -------------------------------------------------------------------------------- 1 | use crate::mdedit_parts::parser::parse_md_item; 2 | use anyhow::{anyhow, Error}; 3 | use pulldown_cmark::{Event, HeadingLevel, Options, Parser, Tag}; 4 | use rat_widget::textarea::TextAreaState; 5 | use std::ops::Range; 6 | 7 | // Markdown styles 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 9 | pub enum MDStyle { 10 | Heading1 = 0, 11 | Heading2, 12 | Heading3, 13 | Heading4, 14 | Heading5, 15 | Heading6, 16 | 17 | Paragraph, 18 | BlockQuote, 19 | CodeBlock, 20 | MathDisplay, 21 | Rule = 10, 22 | Html, 23 | 24 | Link, 25 | LinkDef, 26 | Image, 27 | FootnoteDefinition, 28 | FootnoteReference, 29 | 30 | List, 31 | Item, 32 | TaskListMarker, 33 | ItemTag = 20, 34 | DefinitionList, 35 | DefinitionListTitle, 36 | DefinitionListDefinition, 37 | 38 | Table, 39 | TableHead, 40 | TableRow, 41 | TableCell, 42 | 43 | Emphasis, 44 | Strong, 45 | Strikethrough = 30, 46 | CodeInline, 47 | MathInline, 48 | 49 | MetadataBlock, 50 | } 51 | 52 | impl From for usize { 53 | fn from(value: MDStyle) -> Self { 54 | value as usize 55 | } 56 | } 57 | 58 | impl TryFrom for MDStyle { 59 | type Error = Error; 60 | 61 | fn try_from(value: usize) -> Result { 62 | use MDStyle::*; 63 | Ok(match value { 64 | 0 => Heading1, 65 | 1 => Heading2, 66 | 2 => Heading3, 67 | 3 => Heading4, 68 | 4 => Heading5, 69 | 5 => Heading6, 70 | 71 | 6 => Paragraph, 72 | 7 => BlockQuote, 73 | 8 => CodeBlock, 74 | 9 => MathDisplay, 75 | 10 => Rule, 76 | 11 => Html, 77 | 78 | 12 => Link, 79 | 13 => LinkDef, 80 | 14 => Image, 81 | 15 => FootnoteDefinition, 82 | 16 => FootnoteReference, 83 | 84 | 17 => List, 85 | 18 => Item, 86 | 19 => TaskListMarker, 87 | 20 => ItemTag, 88 | 21 => DefinitionList, 89 | 22 => DefinitionListTitle, 90 | 23 => DefinitionListDefinition, 91 | 92 | 24 => Table, 93 | 25 => TableHead, 94 | 26 => TableRow, 95 | 27 => TableCell, 96 | 97 | 28 => Emphasis, 98 | 29 => Strong, 99 | 30 => Strikethrough, 100 | 31 => CodeInline, 101 | 32 => MathInline, 102 | 103 | 33 => MetadataBlock, 104 | _ => return Err(anyhow!("invalid style {}", value)), 105 | }) 106 | } 107 | } 108 | 109 | pub fn parse_md_styles(state: &TextAreaState) -> Vec<(Range, usize)> { 110 | let mut styles = Vec::new(); 111 | 112 | let txt = state.text(); 113 | 114 | let p = Parser::new_ext( 115 | txt.as_str(), 116 | Options::ENABLE_MATH 117 | | Options::ENABLE_TASKLISTS 118 | | Options::ENABLE_TABLES 119 | | Options::ENABLE_STRIKETHROUGH 120 | | Options::ENABLE_SMART_PUNCTUATION 121 | | Options::ENABLE_FOOTNOTES 122 | | Options::ENABLE_GFM 123 | | Options::ENABLE_DEFINITION_LIST, 124 | ) 125 | .into_offset_iter(); 126 | 127 | for (_, linkdef) in p.reference_definitions().iter() { 128 | styles.push((linkdef.span.clone(), MDStyle::LinkDef as usize)); 129 | } 130 | 131 | for (e, r) in p { 132 | match e { 133 | Event::Start(Tag::Heading { level, .. }) => match level { 134 | HeadingLevel::H1 => styles.push((r, MDStyle::Heading1 as usize)), 135 | HeadingLevel::H2 => styles.push((r, MDStyle::Heading2 as usize)), 136 | HeadingLevel::H3 => styles.push((r, MDStyle::Heading3 as usize)), 137 | HeadingLevel::H4 => styles.push((r, MDStyle::Heading4 as usize)), 138 | HeadingLevel::H5 => styles.push((r, MDStyle::Heading5 as usize)), 139 | HeadingLevel::H6 => styles.push((r, MDStyle::Heading6 as usize)), 140 | }, 141 | Event::Start(Tag::BlockQuote(v)) => { 142 | styles.push((r, MDStyle::BlockQuote as usize)); 143 | } 144 | Event::Start(Tag::CodeBlock(v)) => { 145 | styles.push((r, MDStyle::CodeBlock as usize)); 146 | } 147 | Event::Start(Tag::FootnoteDefinition(v)) => { 148 | styles.push((r, MDStyle::FootnoteDefinition as usize)); 149 | } 150 | Event::Start(Tag::Item) => { 151 | // only color the marker 152 | let text = state.str_slice_byte(r.clone()); 153 | let item = parse_md_item(r.start, text.as_ref()).expect("md item"); 154 | styles.push(( 155 | item.mark_bytes.start..item.mark_bytes.end, 156 | MDStyle::ItemTag as usize, 157 | )); 158 | styles.push((r, MDStyle::Item as usize)); 159 | } 160 | Event::Start(Tag::Emphasis) => { 161 | styles.push((r, MDStyle::Emphasis as usize)); 162 | } 163 | Event::Start(Tag::Strong) => { 164 | styles.push((r, MDStyle::Strong as usize)); 165 | } 166 | Event::Start(Tag::Strikethrough) => { 167 | styles.push((r, MDStyle::Strikethrough as usize)); 168 | } 169 | Event::Start(Tag::Link { .. }) => { 170 | styles.push((r, MDStyle::Link as usize)); 171 | } 172 | Event::Start(Tag::Image { .. }) => { 173 | styles.push((r, MDStyle::Image as usize)); 174 | } 175 | Event::Start(Tag::MetadataBlock { .. }) => { 176 | styles.push((r, MDStyle::MetadataBlock as usize)); 177 | } 178 | Event::Start(Tag::Paragraph) => { 179 | styles.push((r, MDStyle::Paragraph as usize)); 180 | } 181 | Event::Start(Tag::HtmlBlock) => { 182 | styles.push((r, MDStyle::Html as usize)); 183 | } 184 | Event::Start(Tag::List(_)) => { 185 | styles.push((r, MDStyle::List as usize)); 186 | } 187 | Event::Start(Tag::Table(_)) => { 188 | styles.push((r, MDStyle::Table as usize)); 189 | } 190 | Event::Start(Tag::TableHead) => { 191 | styles.push((r, MDStyle::TableHead as usize)); 192 | } 193 | Event::Start(Tag::TableRow) => { 194 | styles.push((r, MDStyle::TableRow as usize)); 195 | } 196 | Event::Start(Tag::TableCell) => { 197 | styles.push((r, MDStyle::TableCell as usize)); 198 | } 199 | Event::Start(Tag::DefinitionList) => { 200 | styles.push((r, MDStyle::DefinitionList as usize)); 201 | } 202 | Event::Start(Tag::DefinitionListTitle) => { 203 | styles.push((r, MDStyle::DefinitionListTitle as usize)); 204 | } 205 | Event::Start(Tag::DefinitionListDefinition) => { 206 | styles.push((r, MDStyle::DefinitionListDefinition as usize)); 207 | } 208 | 209 | Event::Code(v) => { 210 | styles.push((r, MDStyle::CodeInline as usize)); 211 | } 212 | Event::InlineMath(v) => { 213 | styles.push((r, MDStyle::MathInline as usize)); 214 | } 215 | Event::DisplayMath(v) => { 216 | styles.push((r, MDStyle::MathDisplay as usize)); 217 | } 218 | Event::FootnoteReference(v) => { 219 | styles.push((r, MDStyle::FootnoteReference as usize)); 220 | } 221 | Event::Rule => { 222 | styles.push((r, MDStyle::Rule as usize)); 223 | } 224 | Event::TaskListMarker(v) => { 225 | styles.push((r, MDStyle::TaskListMarker as usize)); 226 | } 227 | Event::Html(v) | Event::InlineHtml(v) => { 228 | styles.push((r, MDStyle::Html as usize)); 229 | } 230 | 231 | Event::End(v) => {} 232 | Event::Text(v) => {} 233 | Event::SoftBreak => {} 234 | Event::HardBreak => {} 235 | } 236 | } 237 | 238 | styles 239 | } 240 | -------------------------------------------------------------------------------- /examples/mdedit_parts/test_markdown.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_1() { 3 | dbg!("1"); 4 | } 5 | -------------------------------------------------------------------------------- /examples/minimal.rs: -------------------------------------------------------------------------------- 1 | use crate::config::MinimalConfig; 2 | use crate::event::MinimalEvent; 3 | use crate::global::GlobalState; 4 | use crate::scenery::{Scenery, SceneryState}; 5 | use anyhow::Error; 6 | use rat_salsa::poll::{PollCrossterm, PollRendered, PollTasks, PollTimers}; 7 | use rat_salsa::{run_tui, RunConfig}; 8 | use rat_theme::dark_theme::DarkTheme; 9 | use rat_theme::scheme::IMPERIAL; 10 | use std::time::SystemTime; 11 | 12 | type AppContext<'a> = rat_salsa::AppContext<'a, GlobalState, MinimalEvent, Error>; 13 | type RenderContext<'a> = rat_salsa::RenderContext<'a, GlobalState>; 14 | 15 | fn main() -> Result<(), Error> { 16 | setup_logging()?; 17 | 18 | let config = MinimalConfig::default(); 19 | let theme = DarkTheme::new("Imperial".into(), IMPERIAL); 20 | let mut global = GlobalState::new(config, theme); 21 | 22 | let app = Scenery; 23 | let mut state = SceneryState::default(); 24 | 25 | run_tui( 26 | app, 27 | &mut global, 28 | &mut state, 29 | RunConfig::default()? 30 | .poll(PollCrossterm) 31 | .poll(PollTimers) 32 | .poll(PollTasks) 33 | .poll(PollRendered), 34 | )?; 35 | 36 | Ok(()) 37 | } 38 | 39 | /// Globally accessible data/state. 40 | pub mod global { 41 | use crate::config::MinimalConfig; 42 | use rat_theme::dark_theme::DarkTheme; 43 | use rat_widget::msgdialog::MsgDialogState; 44 | use rat_widget::statusline::StatusLineState; 45 | 46 | #[derive(Debug)] 47 | pub struct GlobalState { 48 | pub cfg: MinimalConfig, 49 | pub theme: DarkTheme, 50 | pub status: StatusLineState, 51 | pub error_dlg: MsgDialogState, 52 | } 53 | 54 | impl GlobalState { 55 | pub fn new(cfg: MinimalConfig, theme: DarkTheme) -> Self { 56 | Self { 57 | cfg, 58 | theme, 59 | status: Default::default(), 60 | error_dlg: Default::default(), 61 | } 62 | } 63 | } 64 | } 65 | 66 | /// Configuration. 67 | pub mod config { 68 | #[derive(Debug, Default)] 69 | pub struct MinimalConfig {} 70 | } 71 | 72 | /// Application wide messages. 73 | pub mod event { 74 | use rat_salsa::timer::TimeOut; 75 | use rat_salsa::RenderedEvent; 76 | 77 | #[derive(Debug)] 78 | pub enum MinimalEvent { 79 | Timer(TimeOut), 80 | Event(crossterm::event::Event), 81 | Rendered, 82 | Message(String), 83 | } 84 | 85 | impl From for MinimalEvent { 86 | fn from(_: RenderedEvent) -> Self { 87 | Self::Rendered 88 | } 89 | } 90 | 91 | impl From for MinimalEvent { 92 | fn from(value: TimeOut) -> Self { 93 | Self::Timer(value) 94 | } 95 | } 96 | 97 | impl From for MinimalEvent { 98 | fn from(value: crossterm::event::Event) -> Self { 99 | Self::Event(value) 100 | } 101 | } 102 | } 103 | 104 | pub mod scenery { 105 | use crate::event::MinimalEvent; 106 | use crate::global::GlobalState; 107 | use crate::minimal::{Minimal, MinimalState}; 108 | use crate::{AppContext, RenderContext}; 109 | use anyhow::Error; 110 | use rat_salsa::{AppState, AppWidget, Control}; 111 | use rat_widget::event::{ct_event, ConsumedEvent, Dialog, HandleEvent, Regular}; 112 | use rat_widget::focus::FocusBuilder; 113 | use rat_widget::msgdialog::MsgDialog; 114 | use rat_widget::statusline::StatusLine; 115 | use ratatui::buffer::Buffer; 116 | use ratatui::layout::{Constraint, Layout, Rect}; 117 | use ratatui::widgets::StatefulWidget; 118 | use std::time::{Duration, SystemTime}; 119 | 120 | #[derive(Debug)] 121 | pub struct Scenery; 122 | 123 | #[derive(Debug, Default)] 124 | pub struct SceneryState { 125 | pub minimal: MinimalState, 126 | } 127 | 128 | impl AppWidget for Scenery { 129 | type State = SceneryState; 130 | 131 | fn render( 132 | &self, 133 | area: Rect, 134 | buf: &mut Buffer, 135 | state: &mut Self::State, 136 | ctx: &mut RenderContext<'_>, 137 | ) -> Result<(), Error> { 138 | let t0 = SystemTime::now(); 139 | 140 | let layout = Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).split(area); 141 | 142 | Minimal.render(area, buf, &mut state.minimal, ctx)?; 143 | 144 | if ctx.g.error_dlg.active() { 145 | let err = MsgDialog::new().styles(ctx.g.theme.msg_dialog_style()); 146 | err.render(layout[0], buf, &mut ctx.g.error_dlg); 147 | } 148 | 149 | let el = t0.elapsed().unwrap_or(Duration::from_nanos(0)); 150 | ctx.g.status.status(1, format!("R {:.0?}", el).to_string()); 151 | 152 | let status_layout = 153 | Layout::horizontal([Constraint::Fill(61), Constraint::Fill(39)]).split(layout[1]); 154 | let status = StatusLine::new() 155 | .layout([ 156 | Constraint::Fill(1), 157 | Constraint::Length(8), 158 | Constraint::Length(8), 159 | ]) 160 | .styles(ctx.g.theme.statusline_style()); 161 | status.render(status_layout[1], buf, &mut ctx.g.status); 162 | 163 | Ok(()) 164 | } 165 | } 166 | 167 | impl AppState for SceneryState { 168 | fn init(&mut self, ctx: &mut AppContext<'_>) -> Result<(), Error> { 169 | ctx.focus = Some(FocusBuilder::for_container(&self.minimal)); 170 | self.minimal.init(ctx)?; 171 | Ok(()) 172 | } 173 | 174 | fn event( 175 | &mut self, 176 | event: &MinimalEvent, 177 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, MinimalEvent, Error>, 178 | ) -> Result, Error> { 179 | let t0 = SystemTime::now(); 180 | 181 | let mut r = match event { 182 | MinimalEvent::Event(event) => { 183 | let mut r = match &event { 184 | ct_event!(resized) => Control::Changed, 185 | ct_event!(key press CONTROL-'q') => Control::Quit, 186 | _ => Control::Continue, 187 | }; 188 | 189 | r = r.or_else(|| { 190 | if ctx.g.error_dlg.active() { 191 | ctx.g.error_dlg.handle(event, Dialog).into() 192 | } else { 193 | Control::Continue 194 | } 195 | }); 196 | 197 | let f = ctx.focus_mut().handle(event, Regular); 198 | ctx.queue(f); 199 | 200 | r 201 | } 202 | MinimalEvent::Rendered => { 203 | ctx.focus = Some(FocusBuilder::rebuild(&self.minimal, ctx.focus.take())); 204 | Control::Continue 205 | } 206 | MinimalEvent::Message(s) => { 207 | ctx.g.status.status(0, &*s); 208 | Control::Changed 209 | } 210 | _ => Control::Continue, 211 | }; 212 | 213 | r = r.or_else_try(|| self.minimal.event(event, ctx))?; 214 | 215 | let el = t0.elapsed()?; 216 | ctx.g.status.status(2, format!("E {:.0?}", el).to_string()); 217 | 218 | Ok(r) 219 | } 220 | 221 | fn error( 222 | &self, 223 | event: Error, 224 | ctx: &mut AppContext<'_>, 225 | ) -> Result, Error> { 226 | ctx.g.error_dlg.append(format!("{:?}", &*event).as_str()); 227 | Ok(Control::Changed) 228 | } 229 | } 230 | } 231 | 232 | pub mod minimal { 233 | use crate::{GlobalState, MinimalEvent, RenderContext}; 234 | use anyhow::Error; 235 | use rat_salsa::{AppState, AppWidget, Control}; 236 | use rat_widget::event::{try_flow, HandleEvent, MenuOutcome, Regular}; 237 | use rat_widget::focus::{FocusBuilder, FocusContainer}; 238 | use rat_widget::menu::{MenuLine, MenuLineState}; 239 | use ratatui::buffer::Buffer; 240 | use ratatui::layout::{Constraint, Direction, Layout, Rect}; 241 | use ratatui::widgets::StatefulWidget; 242 | 243 | #[derive(Debug)] 244 | pub(crate) struct Minimal; 245 | 246 | #[derive(Debug, Default)] 247 | pub struct MinimalState { 248 | pub menu: MenuLineState, 249 | } 250 | 251 | impl AppWidget for Minimal { 252 | type State = MinimalState; 253 | 254 | fn render( 255 | &self, 256 | area: Rect, 257 | buf: &mut Buffer, 258 | state: &mut Self::State, 259 | ctx: &mut RenderContext<'_>, 260 | ) -> Result<(), Error> { 261 | // TODO: repaint_mask 262 | 263 | let r = Layout::new( 264 | Direction::Vertical, 265 | [ 266 | Constraint::Fill(1), // 267 | Constraint::Length(1), 268 | ], 269 | ) 270 | .split(area); 271 | 272 | let menu = MenuLine::new() 273 | .styles(ctx.g.theme.menu_style()) 274 | .item_parsed("_Quit"); 275 | menu.render(r[1], buf, &mut state.menu); 276 | 277 | Ok(()) 278 | } 279 | } 280 | 281 | impl FocusContainer for MinimalState { 282 | fn build(&self, builder: &mut FocusBuilder) { 283 | builder.widget(&self.menu); 284 | } 285 | } 286 | 287 | impl AppState for MinimalState { 288 | fn init( 289 | &mut self, 290 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, MinimalEvent, Error>, 291 | ) -> Result<(), Error> { 292 | ctx.focus().first(); 293 | self.menu.select(Some(0)); 294 | Ok(()) 295 | } 296 | 297 | #[allow(unused_variables)] 298 | fn event( 299 | &mut self, 300 | event: &MinimalEvent, 301 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, MinimalEvent, Error>, 302 | ) -> Result, Error> { 303 | let r = match event { 304 | MinimalEvent::Event(event) => match self.menu.handle(event, Regular) { 305 | MenuOutcome::Activated(0) => Control::Quit, 306 | v => v.into(), 307 | }, 308 | _ => Control::Continue, 309 | }; 310 | 311 | Ok(r) 312 | } 313 | } 314 | } 315 | 316 | fn setup_logging() -> Result<(), Error> { 317 | // _ = fs::remove_file("log.log"); 318 | fern::Dispatch::new() 319 | .format(|out, message, record| { 320 | out.finish(format_args!( 321 | "[{} {} {}]\n {}", 322 | humantime::format_rfc3339_seconds(SystemTime::now()), 323 | record.level(), 324 | record.target(), 325 | message 326 | )) 327 | }) 328 | .level(log::LevelFilter::Debug) 329 | .chain(fern::log_file("log.log")?) 330 | .apply()?; 331 | Ok(()) 332 | } 333 | -------------------------------------------------------------------------------- /examples/queen_bee_shuttle.life: -------------------------------------------------------------------------------- 1 | 2 | [life] 3 | rules = 23/3 4 | one.color = yellow(1) 5 | 6 | [data] 7 | 0 = .........X............ 8 | 1 = .......X.X............ 9 | 2 = ......X.X............. 10 | 3 = XX...X..X...........XX 11 | 4 = XX....X.X...........XX 12 | 5 = .......X.X............ 13 | 6 = .........X............ -------------------------------------------------------------------------------- /examples/rat.life: -------------------------------------------------------------------------------- 1 | 2 | [life] 3 | name = rat 4 | rules = 1357/1357 5 | width = 17 6 | height = 10 7 | one.color = limegreen(1) 8 | zero.color = black(3) 9 | 10 | [data] 11 | 0 = ................. 12 | 1 = ................. 13 | 2 = ......11111.11... 14 | 3 = ...111.....11111. 15 | 4 = ..1...11111.11... 16 | 5 = ..1.............. 17 | 6 = ...1............. 18 | 7 = ....11........... 19 | 8 = ................. 20 | 9 = ................. -------------------------------------------------------------------------------- /examples/theme_sample.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | 3 | use crate::mask0::{Mask0, Mask0State}; 4 | use anyhow::Error; 5 | use crossterm::event::Event; 6 | use rat_salsa::poll::{PollCrossterm, PollTasks, PollTimers}; 7 | use rat_salsa::timer::TimeOut; 8 | use rat_salsa::{run_tui, AppState, AppWidget, Control, RunConfig}; 9 | use rat_theme::dark_theme::DarkTheme; 10 | use rat_theme::scheme::IMPERIAL; 11 | use rat_widget::event::{ct_event, try_flow, Dialog, HandleEvent}; 12 | use rat_widget::msgdialog::{MsgDialog, MsgDialogState}; 13 | use rat_widget::statusline::{StatusLine, StatusLineState}; 14 | use ratatui::buffer::Buffer; 15 | use ratatui::layout::{Constraint, Direction, Layout, Rect}; 16 | use ratatui::widgets::StatefulWidget; 17 | use std::fmt::Debug; 18 | use std::fs; 19 | use std::time::{Duration, SystemTime}; 20 | 21 | type AppContext<'a> = rat_salsa::AppContext<'a, GlobalState, ThemeEvent, Error>; 22 | type RenderContext<'a> = rat_salsa::RenderContext<'a, GlobalState>; 23 | 24 | fn main() -> Result<(), Error> { 25 | setup_logging()?; 26 | 27 | let config = MinimalConfig::default(); 28 | let theme = DarkTheme::new("Imperial".into(), IMPERIAL); 29 | let mut global = GlobalState::new(config, theme); 30 | 31 | let app = MinimalApp; 32 | let mut state = MinimalState::default(); 33 | 34 | run_tui( 35 | app, 36 | &mut global, 37 | &mut state, 38 | RunConfig::default()? 39 | .threads(1) 40 | .poll(PollCrossterm) 41 | .poll(PollTimers) 42 | .poll(PollTasks), 43 | )?; 44 | 45 | Ok(()) 46 | } 47 | 48 | // ----------------------------------------------------------------------- 49 | 50 | #[derive(Debug)] 51 | pub struct GlobalState { 52 | pub cfg: MinimalConfig, 53 | pub theme: DarkTheme, 54 | pub status: StatusLineState, 55 | pub error_dlg: MsgDialogState, 56 | } 57 | 58 | impl GlobalState { 59 | fn new(cfg: MinimalConfig, theme: DarkTheme) -> Self { 60 | Self { 61 | cfg, 62 | theme, 63 | status: Default::default(), 64 | error_dlg: Default::default(), 65 | } 66 | } 67 | } 68 | 69 | // ----------------------------------------------------------------------- 70 | 71 | #[derive(Debug, Default)] 72 | pub struct MinimalConfig {} 73 | 74 | #[derive(Debug)] 75 | pub enum ThemeEvent { 76 | Event(crossterm::event::Event), 77 | TimeOut(TimeOut), 78 | Message(String), 79 | } 80 | 81 | impl From for ThemeEvent { 82 | fn from(value: Event) -> Self { 83 | Self::Event(value) 84 | } 85 | } 86 | 87 | impl From for ThemeEvent { 88 | fn from(value: TimeOut) -> Self { 89 | Self::TimeOut(value) 90 | } 91 | } 92 | 93 | // ----------------------------------------------------------------------- 94 | 95 | #[derive(Debug)] 96 | struct MinimalApp; 97 | 98 | #[derive(Debug, Default)] 99 | struct MinimalState { 100 | mask0: Mask0State, 101 | } 102 | 103 | impl AppWidget for MinimalApp { 104 | type State = MinimalState; 105 | 106 | fn render( 107 | &self, 108 | area: Rect, 109 | buf: &mut Buffer, 110 | state: &mut Self::State, 111 | ctx: &mut RenderContext<'_>, 112 | ) -> Result<(), Error> { 113 | let t0 = SystemTime::now(); 114 | 115 | let layout = Layout::new( 116 | Direction::Vertical, 117 | [Constraint::Fill(1), Constraint::Length(1)], 118 | ) 119 | .split(area); 120 | 121 | Mask0.render(area, buf, &mut state.mask0, ctx)?; 122 | 123 | if ctx.g.error_dlg.active() { 124 | let err = MsgDialog::new().styles(ctx.g.theme.msg_dialog_style()); 125 | err.render(layout[0], buf, &mut ctx.g.error_dlg); 126 | } 127 | 128 | let el = t0.elapsed().unwrap_or(Duration::from_nanos(0)); 129 | ctx.g.status.status(1, format!("R {:.3?}", el).to_string()); 130 | 131 | let layout_status = 132 | Layout::horizontal([Constraint::Percentage(61), Constraint::Percentage(39)]) 133 | .split(layout[1]); 134 | let status = StatusLine::new() 135 | .layout([ 136 | Constraint::Fill(1), 137 | Constraint::Length(12), 138 | Constraint::Length(12), 139 | Constraint::Length(12), 140 | ]) 141 | .styles(ctx.g.theme.statusline_style()); 142 | status.render(layout_status[1], buf, &mut ctx.g.status); 143 | 144 | Ok(()) 145 | } 146 | } 147 | 148 | impl AppState for MinimalState { 149 | fn init(&mut self, ctx: &mut AppContext<'_>) -> Result<(), Error> { 150 | Ok(()) 151 | } 152 | 153 | fn event( 154 | &mut self, 155 | event: &ThemeEvent, 156 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, ThemeEvent, Error>, 157 | ) -> Result, Error> { 158 | let t0 = SystemTime::now(); 159 | 160 | let r = match event { 161 | ThemeEvent::Event(event) => { 162 | try_flow!(match &event { 163 | Event::Resize(_, _) => Control::Changed, 164 | ct_event!(key press CONTROL-'q') => Control::Quit, 165 | _ => Control::Continue, 166 | }); 167 | 168 | try_flow!({ 169 | if ctx.g.error_dlg.active() { 170 | ctx.g.error_dlg.handle(&event, Dialog).into() 171 | } else { 172 | Control::Continue 173 | } 174 | }); 175 | 176 | Control::Continue 177 | } 178 | ThemeEvent::Message(s) => { 179 | ctx.g.status.status(0, &*s); 180 | Control::Changed 181 | } 182 | _ => Control::Continue, 183 | }; 184 | 185 | try_flow!(self.mask0.event(&event, ctx)?); 186 | 187 | let el = t0.elapsed().unwrap_or(Duration::from_nanos(0)); 188 | ctx.g.status.status(3, format!("H {:.3?}", el).to_string()); 189 | 190 | Ok(r) 191 | } 192 | 193 | fn error(&self, event: Error, ctx: &mut AppContext<'_>) -> Result, Error> { 194 | ctx.g.error_dlg.append(format!("{:?}", &*event).as_str()); 195 | Ok(Control::Changed) 196 | } 197 | } 198 | 199 | pub mod mask0 { 200 | use crate::show_scheme::{ShowScheme, ShowSchemeState}; 201 | use crate::{GlobalState, RenderContext, ThemeEvent}; 202 | use anyhow::Error; 203 | use rat_salsa::{AppState, AppWidget, Control}; 204 | use rat_theme::dark_themes; 205 | use rat_widget::event::{try_flow, HandleEvent, MenuOutcome, Popup, Regular}; 206 | use rat_widget::menu::{MenuBuilder, MenuStructure, Menubar, MenubarState}; 207 | use rat_widget::popup::Placement; 208 | use rat_widget::scrolled::Scroll; 209 | use rat_widget::view::{View, ViewState}; 210 | use ratatui::buffer::Buffer; 211 | use ratatui::layout::{Constraint, Direction, Layout, Rect}; 212 | use ratatui::widgets::{Block, StatefulWidget}; 213 | use std::fmt::Debug; 214 | 215 | #[derive(Debug)] 216 | pub struct Mask0; 217 | 218 | #[derive(Debug)] 219 | pub struct Mask0State { 220 | pub menu: MenubarState, 221 | pub scroll: ViewState, 222 | pub scheme: ShowSchemeState, 223 | pub theme: usize, 224 | } 225 | 226 | impl Default for Mask0State { 227 | fn default() -> Self { 228 | let s = Self { 229 | menu: Default::default(), 230 | scroll: Default::default(), 231 | scheme: Default::default(), 232 | theme: 0, 233 | }; 234 | s.menu.bar.focus.set(true); 235 | s 236 | } 237 | } 238 | 239 | #[derive(Debug)] 240 | struct Menu; 241 | 242 | impl<'a> MenuStructure<'a> for Menu { 243 | fn menus(&'a self, menu: &mut MenuBuilder<'a>) { 244 | menu.item_str("Theme").item_str("Quit"); 245 | } 246 | 247 | fn submenu(&'a self, n: usize, submenu: &mut MenuBuilder<'a>) { 248 | match n { 249 | 0 => { 250 | for t in dark_themes().iter() { 251 | submenu.item_string(t.name().into()); 252 | } 253 | } 254 | _ => {} 255 | } 256 | } 257 | } 258 | 259 | impl AppWidget for Mask0 { 260 | type State = Mask0State; 261 | 262 | fn render( 263 | &self, 264 | area: Rect, 265 | buf: &mut Buffer, 266 | state: &mut Self::State, 267 | ctx: &mut RenderContext<'_>, 268 | ) -> Result<(), Error> { 269 | // TODO: repaint_mask 270 | 271 | let layout = Layout::new( 272 | Direction::Vertical, 273 | [Constraint::Fill(1), Constraint::Length(1)], 274 | ) 275 | .split(area); 276 | 277 | let view = View::new() 278 | .block(Block::bordered()) 279 | .vscroll(Scroll::new().styles(ctx.g.theme.scroll_style())); 280 | let view_area = view.inner(layout[0], &mut state.scroll); 281 | 282 | let mut v_buf = view 283 | .layout(Rect::new(0, 0, view_area.width, 38)) 284 | .into_buffer(layout[0], &mut state.scroll); 285 | 286 | v_buf.render_stateful( 287 | ShowScheme::new(ctx.g.theme.name(), ctx.g.theme.scheme()), 288 | Rect::new(0, 0, view_area.width, 38), 289 | &mut state.scheme, 290 | ); 291 | 292 | v_buf 293 | .into_widget() 294 | .render(layout[0], buf, &mut state.scroll); 295 | 296 | let layout_menu = 297 | Layout::horizontal([Constraint::Percentage(61), Constraint::Percentage(39)]) 298 | .split(layout[1]); 299 | let menu = Menubar::new(&Menu) 300 | .styles(ctx.g.theme.menu_style()) 301 | .popup_placement(Placement::Above) 302 | .into_widgets(); 303 | menu.0.render(layout_menu[0], buf, &mut state.menu); 304 | menu.1.render(layout_menu[0], buf, &mut state.menu); 305 | 306 | Ok(()) 307 | } 308 | } 309 | 310 | impl AppState for Mask0State { 311 | fn event( 312 | &mut self, 313 | event: &ThemeEvent, 314 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, ThemeEvent, Error>, 315 | ) -> Result, Error> { 316 | let r = match event { 317 | ThemeEvent::Event(event) => { 318 | try_flow!(match self.menu.handle(event, Popup) { 319 | MenuOutcome::MenuSelected(0, n) => { 320 | ctx.g.theme = dark_themes()[n].clone(); 321 | Control::Changed 322 | } 323 | MenuOutcome::MenuActivated(0, n) => { 324 | ctx.g.theme = dark_themes()[n].clone(); 325 | Control::Changed 326 | } 327 | r => r.into(), 328 | }); 329 | 330 | // TODO: handle_mask 331 | 332 | try_flow!(match self.menu.handle(event, Regular) { 333 | MenuOutcome::Activated(1) => { 334 | Control::Quit 335 | } 336 | r => r.into(), 337 | }); 338 | 339 | try_flow!(self.scroll.handle(event, Regular)); 340 | 341 | Control::Continue 342 | } 343 | _ => Control::Continue, 344 | }; 345 | 346 | Ok(r) 347 | } 348 | } 349 | } 350 | 351 | // ----------------------------------------------------------------------- 352 | 353 | pub mod show_scheme { 354 | use rat_theme::Scheme; 355 | use rat_widget::event::{HandleEvent, MouseOnly, Outcome, Regular}; 356 | use rat_widget::focus::{FocusFlag, HasFocus}; 357 | use rat_widget::reloc::{relocate_area, RelocatableState}; 358 | use ratatui::buffer::Buffer; 359 | use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect}; 360 | use ratatui::prelude::{Line, Span, StatefulWidget}; 361 | use ratatui::style::{Style, Stylize}; 362 | use ratatui::widgets::Widget; 363 | 364 | #[derive(Debug)] 365 | pub struct ShowScheme<'a> { 366 | name: &'a str, 367 | scheme: &'a Scheme, 368 | } 369 | 370 | #[derive(Debug, Default)] 371 | pub struct ShowSchemeState { 372 | pub focus: FocusFlag, 373 | pub area: Rect, 374 | } 375 | 376 | impl RelocatableState for ShowSchemeState { 377 | fn relocate(&mut self, shift: (i16, i16), clip: Rect) { 378 | self.area = relocate_area(self.area, shift, clip); 379 | } 380 | } 381 | 382 | impl HasFocus for ShowSchemeState { 383 | fn focus(&self) -> FocusFlag { 384 | self.focus.clone() 385 | } 386 | 387 | fn area(&self) -> Rect { 388 | self.area 389 | } 390 | } 391 | 392 | impl<'a> ShowScheme<'a> { 393 | pub fn new(name: &'a str, scheme: &'a Scheme) -> Self { 394 | Self { name, scheme } 395 | } 396 | } 397 | 398 | impl<'a> StatefulWidget for ShowScheme<'a> { 399 | type State = ShowSchemeState; 400 | 401 | fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { 402 | state.area = area; 403 | 404 | let l0 = Layout::new( 405 | Direction::Horizontal, 406 | [ 407 | Constraint::Fill(1), 408 | Constraint::Length(90), 409 | Constraint::Fill(1), 410 | ], 411 | ) 412 | .split(area); 413 | 414 | let l1 = Layout::new( 415 | Direction::Vertical, 416 | [ 417 | Constraint::Length(2), 418 | Constraint::Length(2), 419 | Constraint::Length(2), 420 | Constraint::Length(2), 421 | Constraint::Length(2), 422 | Constraint::Length(2), 423 | Constraint::Length(2), 424 | Constraint::Length(2), 425 | Constraint::Length(2), 426 | Constraint::Length(2), 427 | Constraint::Length(2), 428 | Constraint::Length(2), 429 | Constraint::Length(2), 430 | Constraint::Length(2), 431 | Constraint::Length(2), 432 | Constraint::Length(2), 433 | Constraint::Length(2), 434 | Constraint::Length(2), 435 | ], 436 | ) 437 | .flex(Flex::Center) 438 | .split(l0[1]); 439 | 440 | Span::from(format!("{:10}{}", "", self.name)) 441 | .style(Style::new().fg(self.scheme.secondary[3])) 442 | .render(l1[0], buf); 443 | 444 | let sc = self.scheme; 445 | for (i, (n, c)) in [ 446 | ("primary", sc.primary), 447 | ("sec\nondary", sc.secondary), 448 | ("white", sc.white), 449 | ("black", sc.black), 450 | ("gray", sc.gray), 451 | ("red", sc.red), 452 | ("orange", sc.orange), 453 | ("yellow", sc.yellow), 454 | ("limegreen", sc.limegreen), 455 | ("green", sc.green), 456 | ("bluegreen", sc.bluegreen), 457 | ("cyan", sc.cyan), 458 | ("blue", sc.blue), 459 | ("deepblue", sc.deepblue), 460 | ("purple", sc.purple), 461 | ("magenta", sc.magenta), 462 | ("redpink", sc.redpink), 463 | ] 464 | .iter() 465 | .enumerate() 466 | { 467 | Line::from(vec![ 468 | Span::from(format!("{:10}", n)), 469 | Span::from(" DARK ").bg(c[0]).fg(sc.text_color(c[0])), 470 | Span::from(" MID1 ").bg(c[1]).fg(sc.text_color(c[1])), 471 | Span::from(" MID2 ").bg(c[2]).fg(sc.text_color(c[2])), 472 | Span::from(" LITE ").bg(c[3]).fg(sc.text_color(c[3])), 473 | Span::from(" GRAY ") 474 | .bg(sc.grey_color(c[3])) 475 | .fg(sc.text_color(sc.grey_color(c[3]))), 476 | Span::from(" DARK ") 477 | .bg(sc.true_dark_color(c[0])) 478 | .fg(sc.text_color(sc.true_dark_color(c[0]))), 479 | Span::from(" MID1 ") 480 | .bg(sc.true_dark_color(c[1])) 481 | .fg(sc.text_color(sc.true_dark_color(c[1]))), 482 | Span::from(" MID2 ") 483 | .bg(sc.true_dark_color(c[2])) 484 | .fg(sc.text_color(sc.true_dark_color(c[2]))), 485 | Span::from(" LITE ") 486 | .bg(sc.true_dark_color(c[3])) 487 | .fg(sc.text_color(sc.true_dark_color(c[3]))), 488 | ]) 489 | .render(l1[i + 1], buf); 490 | } 491 | } 492 | } 493 | 494 | impl HandleEvent for ShowSchemeState { 495 | fn handle(&mut self, event: &crossterm::event::Event, qualifier: Regular) -> Outcome { 496 | Outcome::Continue 497 | } 498 | } 499 | 500 | impl HandleEvent for ShowSchemeState { 501 | fn handle(&mut self, event: &crossterm::event::Event, qualifier: MouseOnly) -> Outcome { 502 | Outcome::Continue 503 | } 504 | } 505 | } 506 | 507 | // ----------------------------------------------------------------------- 508 | 509 | fn setup_logging() -> Result<(), Error> { 510 | _ = fs::remove_file("log.log"); 511 | fern::Dispatch::new() 512 | .format(|out, message, record| { 513 | out.finish(format_args!( 514 | "[{} {} {}]\n {}", 515 | humantime::format_rfc3339_seconds(SystemTime::now()), 516 | record.level(), 517 | record.target(), 518 | message 519 | )) 520 | }) 521 | .level(log::LevelFilter::Debug) 522 | .chain(fern::log_file("log.log")?) 523 | .apply()?; 524 | Ok(()) 525 | } 526 | -------------------------------------------------------------------------------- /examples/trap.life: -------------------------------------------------------------------------------- 1 | 2 | [life] 3 | name = trap 4 | rules = 23/3 5 | one.color = orange(1) 6 | 7 | [data] 8 | 0 = .XX. 9 | 1 = ...X 10 | 2 = X... 11 | 3 = .XX. -------------------------------------------------------------------------------- /examples/tumbler.life: -------------------------------------------------------------------------------- 1 | 2 | [life] 3 | rules = 23/3 4 | one.color = purple(1) 5 | 6 | [data] 7 | 0 = X.X. 8 | 1 = ...X 9 | 2 = ...X 10 | 3 = X..X 11 | 4 = .XXX -------------------------------------------------------------------------------- /examples/ultra.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use rat_salsa::poll::PollCrossterm; 3 | use rat_salsa::{run_tui, AppState, AppWidget, Control, RunConfig}; 4 | use rat_theme::{dark_theme::DarkTheme, scheme::IMPERIAL}; 5 | use rat_widget::event::ct_event; 6 | use ratatui::prelude::{Buffer, Rect, Widget}; 7 | 8 | type AppContext<'a> = rat_salsa::AppContext<'a, GlobalState, UltraEvent, Error>; 9 | type RenderContext<'a> = rat_salsa::RenderContext<'a, GlobalState>; 10 | 11 | fn main() -> Result<(), Error> { 12 | setup_logging()?; 13 | run_tui( 14 | Ultra, 15 | &mut GlobalState::new(DarkTheme::new("Imperial".into(), IMPERIAL)), 16 | &mut UltraState::default(), 17 | RunConfig::default()?.poll(PollCrossterm), 18 | ) 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct GlobalState { 23 | pub theme: DarkTheme, 24 | pub err_msg: String, 25 | } 26 | 27 | impl GlobalState { 28 | pub fn new(theme: DarkTheme) -> Self { 29 | Self { 30 | theme, 31 | err_msg: Default::default(), 32 | } 33 | } 34 | } 35 | 36 | #[derive(Debug, PartialEq, Eq, Clone)] 37 | pub enum UltraEvent { 38 | Event(crossterm::event::Event), 39 | } 40 | 41 | impl From for UltraEvent { 42 | fn from(value: crossterm::event::Event) -> Self { 43 | Self::Event(value) 44 | } 45 | } 46 | 47 | #[derive(Debug, Default)] 48 | pub struct Ultra; 49 | 50 | #[derive(Debug, Default)] 51 | pub struct UltraState; 52 | 53 | impl AppWidget for Ultra { 54 | type State = UltraState; 55 | 56 | fn render( 57 | &self, 58 | area: Rect, 59 | buf: &mut Buffer, 60 | _state: &mut Self::State, 61 | ctx: &mut RenderContext<'_>, 62 | ) -> Result<(), Error> { 63 | ctx.g.err_msg.as_str().render(area, buf); 64 | Ok(()) 65 | } 66 | } 67 | 68 | impl AppState for UltraState { 69 | fn event( 70 | &mut self, 71 | event: &UltraEvent, 72 | _ctx: &mut AppContext<'_>, 73 | ) -> Result, Error> { 74 | let r = match event { 75 | UltraEvent::Event(event) => match event { 76 | ct_event!(key press 'q') => Control::Quit, 77 | ct_event!(key press CONTROL-'q') => Control::Quit, 78 | _ => Control::Continue, 79 | }, 80 | }; 81 | Ok(r) 82 | } 83 | 84 | fn error(&self, event: Error, ctx: &mut AppContext<'_>) -> Result, Error> { 85 | ctx.g.err_msg = format!("{:?}", event).to_string(); 86 | Ok(Control::Continue) 87 | } 88 | } 89 | 90 | fn setup_logging() -> Result<(), Error> { 91 | fern::Dispatch::new() 92 | .format(|out, message, _| out.finish(format_args!("{}", message))) 93 | .level(log::LevelFilter::Debug) 94 | .chain(fern::log_file("log.log")?) 95 | .apply()?; 96 | Ok(()) 97 | } 98 | -------------------------------------------------------------------------------- /files.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thscharler/rat-salsa-archive/d40fb0c7e6995c5502df8bb911b8fa05b5a94c12/files.gif -------------------------------------------------------------------------------- /mdedit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thscharler/rat-salsa-archive/d40fb0c7e6995c5502df8bb911b8fa05b5a94c12/mdedit.gif -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![stable](https://img.shields.io/badge/stability-RC--1-850101) 2 | [![crates.io](https://img.shields.io/crates/v/rat-salsa.svg)](https://crates.io/crates/rat-salsa) 3 | [![Documentation](https://docs.rs/rat-salsa/badge.svg)](https://docs.rs/rat-salsa) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | [![License](https://img.shields.io/badge/license-APACHE-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) 6 | ![](https://tokei.rs/b1/github/thscharler/rat-salsa) 7 | 8 | # rat-salsa 9 | 10 | An application event-loop with ratatui and crossterm. 11 | 12 | ![image][refMDEditGif] 13 | 14 | rat-salsa provides 15 | 16 | - application event loop [run_tui] 17 | - [background tasks](AppContext::spawn) 18 | - [background async tasks](AppContext::spawn_async) 19 | - [timers](AppContext::add_timer) 20 | - crossterm 21 | - [messages](AppContext::queue) 22 | - [focus](AppContext::focus) 23 | - [control-flow](Control) 24 | - traits for 25 | - [AppWidget] 26 | - [AppState] 27 | 28 | ## Changes 29 | 30 | [Changes](https://github.com/thscharler/rat-salsa/blob/master/changes.md) 31 | 32 | ## Book 33 | 34 | For a start you can have a look at the [book][refRSBook]. 35 | 36 | ## Companion Crates 37 | 38 | * [rat-widget](https://docs.rs/rat-widget) 39 | widget library. Incorporates everything below, but each crate 40 | can be used on its own too. 41 | 42 | Foundational crates: 43 | 44 | * [rat-event](https://docs.rs/rat-event) 45 | Defines the primitives for event-handling. 46 | * [rat-cursor](https://docs.rs/rat-cursor) 47 | Defines just one trait to propagate the required screen cursor position. 48 | * [rat-focus](https://docs.rs/rat-focus) 49 | Primitives for focus-handling. 50 | * [rat-reloc](https://docs.rs/rat-reloc) 51 | Relocate widgets after rendering. Needed support for view-like widgets. 52 | * [rat-scrolled](https://docs.rs/rat-scrolled) 53 | Utility widgets for scrolling. 54 | * [rat-popup](https://docs.rs/rat-popup) 55 | Utility widget to help with popups. 56 | 57 | Crates that deal with specific categories of widgets. 58 | 59 | * [rat-ftable](https://docs.rs/rat-ftable) 60 | table. uses traits to render your data, and renders only the visible cells. 61 | this makes rendering effectively O(1) in regard to the number of rows. 62 | * [rat-menu](https://docs.rs/rat-menu) 63 | Menu widgets. 64 | * [rat-text](https://docs.rs/rat-text) 65 | Text/Value input widgets. 66 | 67 | And my 10ct on theming. 68 | 69 | * [rat-theme](https://docs.rs/rat-theme) 70 | Color-palettes and widget styles. 71 | 72 | * [rat-window](https://github.com/thscharler/rat-window) 73 | __Stopped__ for now. Implement windows in the tui. 74 | Can work with dyn StatefulWidgets too. The groundwork is done, 75 | but it's missing a lot of implementation. 76 | 77 | ## Example 78 | 79 | The examples directory contains some examples 80 | 81 | - [files.rs][refFiles]: Minimal filesystem browser. 82 | - [mdedit.rs][refMDEdit]: Minimal markdown editor. 83 | - [life.rs][refLife]: Game of Life. 84 | 85 | There are some starters too 86 | 87 | - [minimal.rs][refMinimal]: Minimal application with a menubar and statusbar. 88 | - [ultra.rs][refUltra]: Absolute minimum setup. 89 | 90 | ![image][refFilesGif] 91 | 92 | 93 | [refFilesGif]: https://github.com/thscharler/rat-salsa/blob/master/files.gif?raw=true 94 | 95 | [refMDEditGif]: https://github.com/thscharler/rat-salsa/blob/master/mdedit.gif?raw=true 96 | 97 | [refLife]: https://github.com/thscharler/rat-salsa/blob/master/examples/life.rs 98 | 99 | [refMDEdit]: https://github.com/thscharler/rat-salsa/blob/master/examples/mdedit.rs 100 | 101 | [refFiles]: https://github.com/thscharler/rat-salsa/blob/master/examples/files.rs 102 | 103 | [refMinimal]: https://github.com/thscharler/rat-salsa/blob/master/examples/minimal.rs 104 | 105 | [refUltra]: https://github.com/thscharler/rat-salsa/blob/master/examples/ultra.rs 106 | 107 | [refRSBook]: https://thscharler.github.io/rat-salsa/ -------------------------------------------------------------------------------- /rsbook/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["thscharler"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Rat-Salsa" 7 | -------------------------------------------------------------------------------- /rsbook/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | 2 | # Summary 3 | 4 | [Introduction](./intro.md) 5 | 6 | 1. [Concepts](./concepts.md) 7 | 8 | 2. [minimal.rs](./minimal.md) 9 | 10 | 3. [Events](./events.md) 11 | 12 | 1. [Widgets](./events_widget.md) 13 | 14 | 2. [Widgets 2](./events_widget2.md) 15 | 16 | 3. [match Event](./events_match.md) 17 | 18 | 4. [Event control flow](./events_control_flow.md) 19 | 20 | 5. [Event control flow 2](./events_control_flow2.md) 21 | 22 | 4. [AppContext and RenderContext](./appctx.md) 23 | 24 | 5. [Focus](./focus.md) 25 | 26 | 1. [Deeper](./focus_deeper.md) 27 | 28 | 2. [Widget](./focus_widget.md) 29 | 30 | 3. [Container widget](./focus_container.md) 31 | 32 | 4. [Builder](./focus_builder.md) 33 | 34 | 6. [Examples](./examples.md) 35 | 36 | 7. [Widgets](./widgets.md) 37 | 38 | 2. [Rendering overlays](./render_overlay.md) 39 | 40 | -------------------------------------------------------------------------------- /rsbook/src/appctx.md: -------------------------------------------------------------------------------- 1 | 2 | # AppContext 3 | 4 | The AppContext gives access to application wide services. 5 | 6 | There are some builtins: 7 | 8 | * add_timer(): Define a timer that will send TimeOut events. 9 | 10 | * spawn(): Spawn long running tasks in the thread-pool. 11 | You can work with some shared memory model to get the 12 | results, but the preferred method is to return a 13 | Control::Message from the thread. 14 | 15 | * spawn_async(): Spawn async tasks in the tokio runtime. 16 | The result of the async task can be returned as a 17 | Control::Message too. 18 | 19 | * spawn_async_ext(): Gives you an extra channel to return 20 | multiple results from the async task. 21 | 22 | * queue(): Add results to the event-handling queue if 23 | a single return value from event-handling is not enough. 24 | 25 | * focus(): An instance of Focus can be stored here. 26 | Setting up the focus is the job of the application. 27 | 28 | * count: Gives you the frame-counter of the last render. 29 | 30 | All application wide stuff goes into `g` which is an 31 | instance of your Global state. 32 | 33 | 34 | # RenderContext 35 | 36 | Rendercontext is limited compared to AppContext. 37 | 38 | It gives you `g` as your Global state. 39 | 40 | And it lets you set the screen-cursor position. 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /rsbook/src/chapter_1.md: -------------------------------------------------------------------------------- 1 | 2 | # Chapter 1 3 | -------------------------------------------------------------------------------- /rsbook/src/concepts.md: -------------------------------------------------------------------------------- 1 | 2 | # Reinvent the wheel 3 | 4 | 5 | ## Widgets 6 | 7 | The `StatefulWidget` trait works good enough for building 8 | widgets, it's well known and my own ideas where not sufficiently 9 | better so I kept that one. 10 | 11 | All the widgets work just as plain StatefulWidgets. This effort 12 | lead to the [rat-widget][refRatWidget] crate. 13 | 14 | Or see the introduction in [widget chapter](./widgets.md). 15 | 16 | ## Application code 17 | 18 | For the application code `StatefulWidget` is clearly missing, but 19 | I kept the split-widget concept and there are two traits 20 | 21 | * [AppWidget][refAppWidget] 22 | 23 | - Keeps the structure of StatefulWidget, just adds a 24 | [RenderContext][refRenderContext]. 25 | * [AppState][refAppState] The state is the persistent half of 26 | every widget, so this one gets all the event-handling. 27 | 28 | There are functions for application life-cycle and and 29 | event() that is called for every application event. 30 | 31 | - I currently have a driver for crossterm events, but 32 | this can easily be replaced with something else. 33 | 34 | ## run_tui 35 | 36 | [run_tui][refRunTui] implements the event-loop and drives the 37 | application. 38 | 39 | - Polls all event-sources and ensures fairness for all events. 40 | - Renders on demand. 41 | - Maintains the background worker threads. 42 | - Maintains the timers. 43 | - Distributes application events. 44 | - Initializes the terminal and ensure clean shutdown even when 45 | panics occur. 46 | 47 | All of this is orchestrated with the [Control enum][refControl]. 48 | 49 | 50 | [refRenderContext]: https://docs.rs/rat-salsa/latest/rat_salsa/struct.RenderContext.html 51 | 52 | [refAppContext]: https://docs.rs/rat-salsa/latest/rat_salsa/struct.AppContext.html 53 | 54 | [refAppWidget]: https://docs.rs/rat-salsa/latest/rat_salsa/trait.AppWidget.html 55 | 56 | [refAppState]: https://docs.rs/rat-salsa/latest/rat_salsa/trait.AppState.html 57 | 58 | [refRunTui]: https://docs.rs/rat-salsa/latest/rat_salsa/fn.run_tui.html 59 | 60 | [refControl]: https://docs.rs/rat-salsa/latest/rat_salsa/enum.Control.html 61 | 62 | [refRatWidget]: https://docs.rs/rat-widget/latest/rat_widget/ 63 | 64 | 65 | -------------------------------------------------------------------------------- /rsbook/src/events.md: -------------------------------------------------------------------------------- 1 | 2 | # Event handling 3 | 4 | 5 | ```rust 6 | fn event( 7 | &mut self, 8 | event: &MinimalEvent, 9 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, MinimalEvent, Error>, 10 | ) -> Result, Error> { 11 | ``` 12 | rat-salsa requires the application to define its own event 13 | type and provide conversions from every outside event 14 | that the application is interested in. The details of the 15 | conversion are left to the application, but mapping everything 16 | to an application defined enum is a good start. 17 | 18 | ```rust 19 | #[derive(Debug)] 20 | pub enum MinimalEvent { 21 | Timer(TimeOut), 22 | Event(crossterm::event::Event), 23 | Rendered, 24 | Message(String), 25 | } 26 | ``` 27 | rat-salsa polls all event-sources and takes note which one 28 | has an event to process. Then it takes this notes and 29 | starts with the first event-source and asks it to send 30 | its event. 31 | 32 | The event-source converts its event-type to the application 33 | event and sends it off to the `event()` function of the 34 | main AppState. 35 | 36 | This results either in a specific action like 'render' or 37 | in a followup event. Followups are sent down to event() too, 38 | and result in another followup. 39 | 40 | At some point this ends with a result `Control::Continue`. 41 | This is the point where the event-loop goes back to its 42 | notes and asks the next event source to send its event. 43 | 44 | > Note that every event-source with an outstanding event 45 | > is processed before asking all event-sources if there 46 | > are new events. This prevents starving event-sources 47 | > further down the list. 48 | 49 | There are no special cases, or any routing of events, 50 | everything goes straight to the `event()` function. 51 | 52 | The event() function gets the extra parameter 53 | [ctx][refAppContext] for access to application global data. 54 | 55 | ## Result 56 | 57 | The result is a `Result, Error>`, that tells 58 | rat-salsa how to proceed. 59 | 60 | - [Control::Continue][refControl]: Continue with the next event. 61 | 62 | - [Control::Unchanged][refControl]: Event has been used, but 63 | requires no rendering. Just continues with the next event. 64 | 65 | Within the application this is used to break early. 66 | 67 | - [Control::Changed][refControl]: Event has been used, and 68 | a render is necessary. Continues with the next event after 69 | rendering. May send a RenderedEvent immediately after 70 | the render occurred before any other events. 71 | 72 | - [Control::Message(m)][refControl]: This contains a followup 73 | event. It will be put on the current events queue and 74 | processed in order. But before polling for new events. 75 | 76 | The individual AppWidgets making up the application are quite 77 | isolated from other parts and just have access to their own 78 | state and some global application state. 79 | 80 | All communication across AppWidgets can use this mechanism 81 | to send special events/messages. 82 | 83 | - [Control::Quit][refControl]: Ends the event-loop and resets the 84 | terminal. This returns from run_tui() and ends the application 85 | by running out of main. 86 | 87 | 88 | 89 | [refRatEvent]: https://docs.rs/rat-event/latest/rat_event/ 90 | 91 | [refControl]: https://docs.rs/rat-salsa/latest/rat_salsa/enum.Control.html 92 | 93 | [refRatWidget]: https://docs.rs/rat-widget/latest/rat_widget/ 94 | 95 | [refAppContext]: https://docs.rs/rat-salsa/latest/rat_salsa/struct.AppContext.html 96 | 97 | [refConsumedEvent]: https://docs.rs/rat-event/latest/rat_event/trait.ConsumedEvent.html 98 | -------------------------------------------------------------------------------- /rsbook/src/events_control_flow.md: -------------------------------------------------------------------------------- 1 | 2 | # Control flow 3 | 4 | There are some constructs to help with control flow in 5 | handler functions. 6 | 7 | - Trait [ConsumedEvent][refConsumedEvent] is implemented for 8 | Control and all Outcome types. 9 | 10 | The fn `or_else` and `or_else_try` run a closure if the return 11 | value is Control::Continue; `and` and `and_try` run a closure 12 | if the return value is anything else. 13 | 14 | - Macros [flow!][refFlow] and [try_flow!][refTryFlow]. These run 15 | the codeblock and return early if the result is anything but 16 | Control::Continue. `try_flow!` Ok-wraps the result, both do 17 | `.into()` conversion. 18 | 19 | Both reach similar results, and there are situations where one 20 | or the other is easier/clearer. 21 | 22 | - Extensive use of `From<>`. 23 | 24 | - Widgets use the `Outcome` enum as a result, or have their 25 | derived outcome type if it is not sufficient. All extra 26 | outcome types are convertible to the base Outcome. 27 | 28 | - On the rat-salsa side is `Control` which is modeled 29 | after `Outcome` with its own extensions. It has a 30 | `From` implementation. That means everything 31 | that is convertible to Outcome can in turn be converted to 32 | Control. 33 | 34 | This leads to 35 | 36 | - widgets don't need to know about rat-salsa. 37 | - rat-salsa doesn't need to know about every last widget. 38 | 39 | - Widgets often have action-functions that return bool to 40 | indicate 'changed'/'not changed'. There is a conversion for 41 | Outcome that maps true/false to Changed/Unchanged. So those 42 | results are integrated too. 43 | 44 | - Ord for Outcome/Control 45 | 46 | Both implement Ord; for Outcome that's straightforward, Control 47 | ignores the Message(m) payload for this purpose. 48 | 49 | Now it's possible to combine results 50 | 51 | ```rust 52 | 53 | max(r1, r2) 54 | 55 | ``` 56 | The enum values are ordered in a way that this gives a sensible 57 | result. 58 | 59 | [refConsumedEvent]: https://docs.rs/rat-event/latest/rat_event/trait.ConsumedEvent.html 60 | 61 | [refFlow]: https://docs.rs/rat-event/latest/rat_event/macro.flow.html 62 | 63 | [refTryFlow]: https://docs.rs/rat-event/latest/rat_event/macro.try_flow.html 64 | 65 | -------------------------------------------------------------------------------- /rsbook/src/events_control_flow2.md: -------------------------------------------------------------------------------- 1 | 2 | # Extended control flow 3 | 4 | AppContext has functions that help with application control flow. 5 | 6 | - `add_timer()`: Sets a timer event. This returns a TimerHandle 7 | to identify a specific timer. 8 | 9 | - `queue()` and `queue_err()`: These functions add additional 10 | items to the list that will be processed after the event 11 | handler returns. The result of the event handler will be added 12 | at the end of this list too. 13 | 14 | - `spawn()`: Run a closure as a background task. Such a closure 15 | gets a cancel-token and a back-channel to report its findings. 16 | 17 | ```rust 18 | let cancel = ctx.spawn(move |cancel, send| { 19 | let mut data = Data::new(config); 20 | 21 | loop { 22 | 23 | // ... long task ... 24 | 25 | // report partial results 26 | send.send(Ok(Control::Message(AppMsg::Partial))); 27 | 28 | if cancel.is_canceled() { 29 | break; 30 | } 31 | } 32 | 33 | Ok(Control::Message(AppMsg::Final)) 34 | }); 35 | ``` 36 | Spawns a background task. This is a move closure to own the 37 | parameters for the 'static closure. It returns a clone of the 38 | cancel token to interrupt the task if necessary. 39 | 40 | ```rust 41 | let cancel = ctx.spawn(move |cancel, send| { 42 | ``` 43 | Captures its parameters. 44 | 45 | ```rust 46 | let mut data = Data::new(config); 47 | ``` 48 | Goes into the extended calculation. This uses `send` to report 49 | a partial result as a message. At a point where canceling is 50 | sensible it checks the cancel state. 51 | 52 | ```rust 53 | loop { 54 | 55 | // ... long task ... 56 | 57 | // report partial results 58 | send.send(Ok(Control::Message(AppMsg::Partial))); 59 | 60 | if cancel.is_canceled() { 61 | break; 62 | } 63 | } 64 | ``` 65 | Finishes with some result. 66 | 67 | ```rust 68 | Ok(Control::Message(AppMsg::Final)) 69 | ``` 70 | [refRatEvent]: https://docs.rs/rat-event/latest/rat_event/ 71 | 72 | [refControl]: https://docs.rs/rat-salsa/latest/rat_salsa/enum.Control.html 73 | 74 | [refRatWidget]: https://docs.rs/rat-widget/latest/rat_widget/ 75 | 76 | [refAppContext]: https://docs.rs/rat-salsa/latest/rat_salsa/struct.AppContext.html 77 | 78 | [refConsumedEvent]: https://docs.rs/rat-event/latest/rat_event/trait.ConsumedEvent.html 79 | -------------------------------------------------------------------------------- /rsbook/src/events_match.md: -------------------------------------------------------------------------------- 1 | 2 | # Matching events 3 | 4 | ```rust 5 | try_flow!(match &event { 6 | ct_event!(resized) => Control::Changed, 7 | ct_event!(key press CONTROL-'q') => Control::Quit, 8 | _ => Control::Continue, 9 | }); 10 | ``` 11 | 12 | If you want to match specific events during event-handling 13 | match is great. Less so is the struct pattern for crossterm 14 | events. 15 | 16 | That's why I started with [ct_event!][refCtEvent] ... 17 | 18 | It provides a very readable syntax, and I think it now covers 19 | all of crossterm::Event. 20 | 21 | > [!NOTE]: If you use `key press SHIFT-'q'` it will not work. 22 | > It expects a capital 'Q' in that case. The same for any 23 | > combination with SHIFT. 24 | 25 | 26 | 27 | [refCtEvent]: https://docs.rs/rat-event/latest/rat_event/macro.ct_event.html 28 | -------------------------------------------------------------------------------- /rsbook/src/events_widget.md: -------------------------------------------------------------------------------- 1 | 2 | # Widget events 1 3 | 4 | The widgets for [rat-widget][refRatWidget] use the trait 5 | HandleEvent defined in [rat-event][refRatEvent]. 6 | 7 | ```rust 8 | try_flow!(match self.menu.handle(event, Regular) { 9 | MenuOutcome::Activated(0) => { 10 | Control::Quit 11 | } 12 | v => v.into(), 13 | }); 14 | ``` 15 | 16 | `self.menu` is the state struct for the menu widget. 17 | It can have multiple HandleEvent implementations, typical are 18 | `Regular` and `MouseOnly`. The second parameter selects the 19 | event-handler. 20 | 21 | - Regular: Does all the expected event-handling and returns an 22 | Outcome value that details what has happened. 23 | 24 | - MouseOnly: Only uses mouse events. Generally not very useful 25 | except when you want to write your own keybindings for a 26 | widget. Then you can forward that part to the MouseOnly handler 27 | and be done with the mousey part. 28 | 29 | See [mdedit][refMdEditMarkdown]. It overrides only part of the 30 | keybindings with its own implementation and forwards the rest 31 | to the Regular handler. 32 | 33 | - Readonly: Text widgets have a Regular handler and a ReadOnly 34 | handler. The latter only moves and allows selections. 35 | 36 | - DoubleClick: Some widgets add one. Double clicks are a bit 37 | rarer and often require special attention, so this behaviour is 38 | split off from Regular handling. 39 | 40 | - Dialog and Popup: These are the regular handlers for dialog and 41 | popup widgets. They have some irregular behaviour, so it's good 42 | to see this immediately. 43 | 44 | The handle functions return an outcome value that describes what 45 | has happened. This value usually is widget specific. 46 | 47 | And there is the try_flow! macro that surrounds it all. It returns 48 | early, if the event has been consumed by the handler. 49 | 50 | 51 | 52 | [refRatEvent]: https://docs.rs/rat-event/latest/rat_event/ 53 | 54 | [refControl]: https://docs.rs/rat-salsa/latest/rat_salsa/enum.Control.html 55 | 56 | [refRatWidget]: https://docs.rs/rat-widget/latest/rat_widget/ 57 | 58 | [refAppContext]: https://docs.rs/rat-salsa/latest/rat_salsa/struct.AppContext.html 59 | 60 | [refConsumedEvent]: https://docs.rs/rat-event/latest/rat_event/trait.ConsumedEvent.html 61 | 62 | [refMdEditMarkdown]: https://github.com/thscharler/rat-salsa/blob/master/examples/mdedit_parts/mod.rs 63 | 64 | 65 | -------------------------------------------------------------------------------- /rsbook/src/events_widget2.md: -------------------------------------------------------------------------------- 1 | 2 | # Widget events 2 3 | 4 | If you want to use other widgets it's fine. 5 | 6 | Consult their documentation how and if they can work with 7 | crossterm events. 8 | -------------------------------------------------------------------------------- /rsbook/src/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## minimal.rs 4 | 5 | Starter template. Not absolut minimal but rather. 6 | 7 | ## ultra.rs 8 | 9 | Starter template. The real minimal minimal. Full rat-salsa 10 | application in less than 100 lines. You can even quit with 'q'. 11 | 12 | > To prove it. 13 | 14 | ## turbo.rs 15 | 16 | Tries to mimic Turbo Pascal 7. At least the menu is here :) 17 | 18 | > Example for an elaborate menu. It even has a submenu. 19 | 20 | ## life.rs 21 | 22 | Conways Game of Life in the terminal. There are a few 23 | sample .life files you can give it to start. 24 | 25 | > Adds an additional event-source for the event-loop to handle. 26 | > It's just a simple animation ticker, but when the types align 27 | > ... 28 | 29 | ## theme_sample.rs 30 | 31 | Shows the palettes for the themes. 32 | 33 | > 34 | 35 | ## files.rs 36 | 37 | One percent of a file manager. 38 | 39 | > Not very complicated but shows a bigger application. The 40 | > only interesting thing is it uses spawn() for listing the 41 | > directories and for loading a preview. 42 | 43 | ## mdedit.rs 44 | 45 | This book has been written with it. 46 | 47 | A small markdown editor. 48 | 49 | > Dynamic content. Complex control flow. Shows Tabs+Split. Shows 50 | > TextArea. Custom event handler for a widget. 51 | -------------------------------------------------------------------------------- /rsbook/src/focus.md: -------------------------------------------------------------------------------- 1 | 2 | # Focus 3 | 4 | The struct [Focus][refFocus] can do all the focus handling for 5 | your application. 6 | 7 | As it is essential for almost any application, it got a place 8 | in AppContext. 9 | 10 | ## Usage 11 | 12 | ```rust 13 | if self.w_split.is_focused() { 14 | ctx.focus().next(); 15 | } else { 16 | ctx.focus().focus(&self.w_split); 17 | } 18 | ``` 19 | 20 | Just some example: This queries some widget state whether it 21 | currently has the focus and jumps to the next widget /sets the 22 | focus to the same widget. 23 | 24 | ## There's always a trait 25 | 26 | or two. 27 | 28 | * [HasFocus][refHasFocus]: 29 | 30 | This trait is for single widgets. 31 | 32 | It's main functions are focus() and area(). 33 | 34 | focus() returns a clone of a [FocusFlag][refFocusFlag] that 35 | is part of the widgets state. It has a hidden `Rc<>`, so this 36 | is fine. 37 | 38 | > The flag is close to the widget, so it's always there when 39 | > you need it. As an Rc it can be used elsewhere too, say 40 | > Focus. 41 | 42 | area() returns the widgets current screen area. Which is used 43 | for mouse focus. 44 | 45 | * [FocusContainer][refFocusContainer] 46 | 47 | The second trait is for container widgets. 48 | 49 | It's main function is build(). 50 | 51 | `build(&mut FocusBuilder)` gets a 52 | [FocusBuilder][refFocusBuilder] and collects all widgets and 53 | nested containers in the preferred focus order. 54 | 55 | ## AppState 56 | 57 | In your application you construct the current Focus for each 58 | event. 59 | 60 | This is necessary as 61 | - the application state might have changed 62 | - the terminal might have been resized 63 | 64 | and 65 | 66 | - it's hard to track such changes at the point where they occur. 67 | - it's cheap enough not to bother. 68 | - there is room for optimizations later. 69 | 70 | ```rust 71 | ctx.focus = Some(FocusBuilder::for_container(&self.app)); 72 | ``` 73 | 74 | If you have a AppWidget that `HasFocus` you can simply use 75 | FocusBuilder to construct the current Focus. If you then set it 76 | in the `ctx` it is immediately accessible everywhere. 77 | 78 | ## Events 79 | 80 | Focus implements HandleEvent, so event handling is simple. 81 | 82 | ```rust 83 | let f = Control::from( 84 | ctx.focus_mut().handle(event, Regular) 85 | ); 86 | ``` 87 | 88 | `Regular` event-handling for focus is 89 | 90 | - Tab: jump to the next widget. 91 | - Shift-Tab: jump to the previous widget. 92 | - Mouse click: focus that widget. 93 | 94 | Focus is independent from rat-salsa, so it returns Outcome 95 | instead of Control, thus the conversion. 96 | 97 | > _Complications_ 98 | > 99 | > `handle` returns Outcome::Changed when the focus switches to a 100 | > new widget and everything has to be rendered. On the other hand 101 | > the focused widget might want to use the same mouse click that 102 | > switched the focus to do something else. 103 | > 104 | > We end up with two results we need to return from the event 105 | > handler. 106 | 107 | ```rust 108 | let f = Control::from(ctx.focus_mut().handle(event, Regular)); 109 | let r = self.app.crossterm(event, ctx)?; 110 | ``` 111 | 112 | > Here `Ord` comes to the rescue. The values of Control are 113 | > constructed in order of importance, so 114 | 115 | ```rust 116 | Ok(max(f, r)) 117 | ``` 118 | 119 | > can save the day. If focus requires Control::Changed we return 120 | > this as the minimum regardless of what the rest of event 121 | > handling says. 122 | 123 | Or you can just return a second result to the event-loop using 124 | 125 | ```rust 126 | let f = ctx.focus_mut().handle(event, Regular); 127 | ctx.queue(f); 128 | ``` 129 | and be done with it. 130 | 131 | 132 | [refFocusContainer]: https://docs.rs/rat-focus/latest/rat_focus/trait.FocusContainer.html 133 | 134 | [refHasFocus]: https://docs.rs/rat-focus/latest/rat_focus/trait.HasFocus.html 135 | 136 | [refFocusFlag]: https://docs.rs/rat-focus/latest/rat_focus/struct.FocusFlag.html 137 | 138 | [refFocusBuilder]: https://docs.rs/rat-focus/latest/rat_focus/struct.FocusBuilder.html 139 | 140 | [refFocus]: https://docs.rs/rat-focus/latest/rat_focus/struct.Focus.html 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /rsbook/src/focus_builder.md: -------------------------------------------------------------------------------- 1 | 2 | # Focus builder 3 | 4 | The functions widget() and container() add widgets to Focus. 5 | They will be traversed in the order given. 6 | 7 | The two other important functions are 8 | 9 | * build_focus() 10 | 11 | Takes a container widget and returns a Focus. 12 | 13 | * rebuild_focus() 14 | 15 | Does the same, but takes the previous Focus too. 16 | 17 | What it does is, it builds the new Focus and checks which 18 | widgets are __no longer__ part of it. It resets all 19 | FocusFlags for those widgets. 20 | 21 | A bonus is it reuses the allocations too. 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /rsbook/src/focus_container.md: -------------------------------------------------------------------------------- 1 | # Container widgets 2 | 3 | Container widgets are just widgets with some inner structure 4 | they want to expose. 5 | 6 | For a container widget implement FocusContainer instead of HasFocus. 7 | 8 | ```rust 9 | pub trait FocusContainer { 10 | // Required method 11 | fn build(&self, builder: &mut FocusBuilder); 12 | 13 | // Provided methods 14 | fn container(&self) -> Option { ... } 15 | fn area(&self) -> Rect { ... } 16 | fn area_z(&self) -> u16 { ... } 17 | fn is_container_focused(&self) -> bool { ... } 18 | fn container_lost_focus(&self) -> bool { ... } 19 | fn container_gained_focus(&self) -> bool { ... } 20 | } 21 | ``` 22 | 23 | * build() 24 | 25 | This is called to construct the focus recursively. 26 | Use FocusBuilder::widget() to add a single widget, or 27 | FocusBuilder::container() to add a container widget. 28 | 29 | That's it. 30 | 31 | * container() 32 | 33 | The container widget may want to know if any of the contained 34 | widgets has a focus. If container() returns a ContainerFlag it 35 | will be set to a summary of the widgets it contains. 36 | 37 | The container-flag can also be used to focus the first widget 38 | for a container with Focus::focus_container(). 39 | 40 | And the container-flag is used to remove/update/replace the 41 | widgets of a container. 42 | 43 | * area() 44 | 45 | If area() returns a value than the first widget in the 46 | container is focused if you click on that area. 47 | 48 | * area_z() 49 | 50 | When stacking areas above another a z-value helps with mouse 51 | focus. 52 | 53 | * is_container_focused(), container_lost_focus(), 54 | container_gained_focus() 55 | 56 | For application code; uses the container flag. 57 | 58 | -------------------------------------------------------------------------------- /rsbook/src/focus_deeper.md: -------------------------------------------------------------------------------- 1 | 2 | # Details, details 3 | 4 | ## Focus 5 | 6 | ### Navigation 7 | 8 | * first(): Focus the first widget. 9 | * next()/prev(): Change the focus. 10 | * focus(): Focus a specific widget. 11 | * focus_at(): Focus the widget at a position. 12 | 13 | ### Debugging 14 | 15 | * You can construct the FocusFlag with a name. 16 | * Call Focus::enable_log() 17 | * You might find something useful in your log-file. 18 | 19 | ### Dynamic changes 20 | 21 | You might come to a situation where 22 | 23 | * Your state changed 24 | * which changes the widget structure/focus order/... 25 | * everything should still work 26 | 27 | then you can use one of 28 | 29 | * remove_container 30 | * update_container 31 | * replace_container 32 | 33 | to change Focus without completely rebuilding it. 34 | 35 | They reset the focus state for all widgets that are no longer 36 | part of Focus, so there is no confusion who currently owns the 37 | focus. You can call some focus function to set the new focus 38 | afterwards. 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /rsbook/src/focus_widget.md: -------------------------------------------------------------------------------- 1 | # Widget focus 2 | 3 | For a widget to work with Focus it must implement HasFocus. 4 | 5 | ```rust 6 | pub trait HasFocusFlag { 7 | // Required methods 8 | fn focus(&self) -> FocusFlag; 9 | fn area(&self) -> Rect; 10 | 11 | // Provided methods 12 | fn area_z(&self) -> u16 { ... } 13 | fn navigable(&self) -> Navigation { ... } 14 | fn is_focused(&self) -> bool { ... } 15 | fn lost_focus(&self) -> bool { ... } 16 | fn gained_focus(&self) -> bool { ... } 17 | 18 | // 19 | fn build(&self, builder: &mut FocusBuilder) { ... } 20 | } 21 | ``` 22 | 23 | * focus() 24 | 25 | The widget state should contain a FocusFlag somewhere. It returns a 26 | clone here. The current state of the widget is always accessible 27 | during rendering and event-handling. 28 | 29 | * area() 30 | 31 | Area for mouse focus. 32 | 33 | * area_z() 34 | 35 | The z-value for the area. When you add overlapping areas the 36 | z-value is used to find out which area should be focused by 37 | a given mouse event. 38 | 39 | * navigable() 40 | 41 | This indicates if/how the widget can be reached/left by Focus. 42 | It has a lot of Options, see [Navigation][refNavigation]. 43 | 44 | * is_focused(), lost_focus(), gained_focus() 45 | 46 | These are for application code. 47 | 48 | * build() 49 | 50 | Like FocusContainer there is a build method. For most widgets 51 | the default implementation will suffice. 52 | 53 | But if you have a complex widget with inner structures, 54 | you can implement this to set up your focus requirements. 55 | 56 | [refNavigation]: https://docs.rs/rat-focus/latest/rat_focus/enum.Navigation.html 57 | 58 | 59 | -------------------------------------------------------------------------------- /rsbook/src/intro.md: -------------------------------------------------------------------------------- 1 | 2 | # Introduction 3 | 4 | I wrote my first ratatui application, which was monitoring 5 | changed files, recalculated the universe and showed potential 6 | errors. That worked fine, and I got more ambitious. 7 | 8 | Write a real UI for the cmd-line application I need for my day 9 | to day work. And that quickly stalled, with quite a few bits and 10 | pieces missing. 11 | 12 | So I started this rat-salsa thing, to add a bit of spicy sauce 13 | to my ratatouille. 14 | -------------------------------------------------------------------------------- /rsbook/src/minimal.md: -------------------------------------------------------------------------------- 1 | # minimal 2 | 3 | A walkthrough for examples/minimal.rs, a starting point for a 4 | new application. 5 | 6 | ## main 7 | 8 | ```rust 9 | fn main() -> Result<(), Error> { 10 | setup_logging()?; 11 | 12 | let config = MinimalConfig::default(); 13 | let theme = DarkTheme::new("Imperial".into(), IMPERIAL); 14 | let mut global = GlobalState::new(config, theme); 15 | 16 | let app = Scenery; 17 | let mut state = SceneryState::default(); 18 | 19 | run_tui( 20 | app, 21 | &mut global, 22 | &mut state, 23 | RunConfig::default()? 24 | .poll(PollCrossterm) 25 | .poll(PollTimers) 26 | .poll(PollTasks), 27 | )?; 28 | 29 | Ok(()) 30 | } 31 | ``` 32 | 33 | run_tui is fed with 34 | 35 | - app: This is just the unit-struct Scenery. It provides the 36 | scenery for the application, adds a status bar, displays error 37 | messages, and forwards the real application Minimal. 38 | 39 | - global: whatever global state is necessary. This global state 40 | is useable across all app-widgets. Otherwise, the app-widgets 41 | only see their own state. 42 | 43 | - state: the state-struct SceneryState. 44 | 45 | - [RunConfig][refRunConfig]: configures the event-loop 46 | 47 | - If you need some special terminal init/shutdown commands, 48 | implement the [rat-salsa::Terminal][refSalsaTerminal] trait 49 | and set it here. 50 | 51 | - Set the number of worker threads. 52 | 53 | - Add the event-sources. Implement the 54 | [PollEvents][refPollEvents] trait. 55 | 56 | See [examples/life.rs][refLife] for an example. 57 | 58 | Here we go with default drivers PollCrossterm for 59 | crossterm, PollTimers for timers, PollTasks for the 60 | results from background tasks. 61 | 62 | *** 63 | 64 | The rest is not very exciting. It defines a config-struct 65 | which is just empty, loads a default theme for the application 66 | and makes both accessible via the global state. 67 | 68 | ## mod global 69 | 70 | Defines the global state... 71 | 72 | ```rust 73 | #[derive(Debug)] 74 | pub struct GlobalState { 75 | pub cfg: MinimalConfig, 76 | pub theme: DarkTheme, 77 | pub status: StatusLineState, 78 | pub error_dlg: MsgDialogState, 79 | } 80 | ``` 81 | 82 | ## mod config 83 | 84 | Defines the config... 85 | 86 | ```rust 87 | pub struct MinimalConfig {} 88 | ``` 89 | 90 | ## mod event 91 | 92 | This defines the event type throughout the application. 93 | 94 | 95 | ``` 96 | #[derive(Debug)] 97 | pub enum MinimalEvent { 98 | Timer(TimeOut), 99 | Event(crossterm::event::Event), 100 | Message(String), 101 | } 102 | 103 | ``` 104 | 105 | The trick here is that every PollXXX that you add requires that 106 | you provide a conversion from its event-type to your application 107 | event-type. 108 | 109 | 110 | ``` 111 | impl From for MinimalEvent { 112 | fn from(value: TimeOut) -> Self { 113 | Self::Timer(value) 114 | } 115 | } 116 | 117 | impl From for MinimalEvent { 118 | fn from(value: Event) -> Self { 119 | Self::Event(value) 120 | } 121 | } 122 | 123 | ``` 124 | 125 | But otherwise you are free to add more. 126 | 127 | Specifically you can add any events you want to send between the 128 | different parts of your application. There's a need for that. 129 | If you split the application into multiple AppWidget/AppState 130 | widgets there is no easy way to communicate between parts. 131 | 132 | Other approaches set up channels to do this, but rat-salsa just 133 | uses the main event-queue to distribute such messages. 134 | 135 | ## mod scenery 136 | 137 | ```rust 138 | #[derive(Debug)] 139 | pub struct Scenery; 140 | 141 | #[derive(Debug, Default)] 142 | pub struct SceneryState { 143 | pub minimal: MinimalState, 144 | } 145 | ``` 146 | 147 | Defines a unit struct for the scenery and a struct for any state. 148 | Here it holds the state for the actual application. 149 | 150 | ### AppWidget 151 | 152 | ```rust 153 | impl AppWidget for Scenery { 154 | type State = SceneryState; 155 | 156 | fn render( 157 | &self, 158 | area: Rect, 159 | buf: &mut Buffer, 160 | state: &mut Self::State, 161 | ctx: &mut RenderContext<'_>, 162 | ) -> Result<(), Error> { 163 | let t0 = SystemTime::now(); 164 | 165 | let layout = Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).split(area); 166 | 167 | Minimal.render(area, buf, &mut state.minimal, ctx)?; 168 | 169 | if ctx.g.error_dlg.active() { 170 | let err = MsgDialog::new().styles(ctx.g.theme.msg_dialog_style()); 171 | err.render(layout[0], buf, &mut ctx.g.error_dlg); 172 | } 173 | 174 | let el = t0.elapsed().unwrap_or(Duration::from_nanos(0)); 175 | ctx.g.status.status(1, format!("R {:.0?}", el).to_string()); 176 | 177 | let status_layout = 178 | Layout::horizontal([Constraint::Fill(61), Constraint::Fill(39)]).split(layout[1]); 179 | let status = StatusLine::new() 180 | .layout([ 181 | Constraint::Fill(1), 182 | Constraint::Length(8), 183 | Constraint::Length(8), 184 | ]) 185 | .styles(ctx.g.theme.statusline_style()); 186 | status.render(status_layout[1], buf, &mut ctx.g.status); 187 | 188 | Ok(()) 189 | } 190 | } 191 | ``` 192 | 193 | Implement the AppWidget trait. This forwards rendering to Minimal, and then 194 | renders a MsgDialog if needed for error messages, and the status line. 195 | The default displays some timings taken for rendering too. 196 | 197 | ### AppState 198 | 199 | ```rust 200 | impl AppState for SceneryState { 201 | ``` 202 | 203 | AppState has three type parameters that occur everywhere. I couldn't cut 204 | back that number any further ... 205 | 206 | ```rust 207 | fn init(&mut self, ctx: &mut AppContext<'_>) -> Result<(), Error> { 208 | ctx.focus = Some(FocusBuilder::for_container(&self.minimal)); 209 | self.minimal.init(ctx)?; 210 | Ok(()) 211 | } 212 | ``` 213 | 214 | init is the first event for every application. 215 | 216 | it sets up the initial [Focus](./focus) for the application and 217 | forwards to MinimalState. 218 | 219 | ```rust 220 | fn event( 221 | &mut self, 222 | event: &MinimalEvent, 223 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, MinimalEvent, Error>, 224 | ) -> Result, Error> { 225 | let t0 = SystemTime::now(); 226 | 227 | let mut r = match event { 228 | MinimalEvent::Event(event) => { 229 | let mut r = match &event { 230 | ct_event!(resized) => Control::Changed, 231 | ct_event!(key press CONTROL-'q') => Control::Quit, 232 | _ => Control::Continue, 233 | }; 234 | 235 | r = r.or_else(|| { 236 | if ctx.g.error_dlg.active() { 237 | ctx.g.error_dlg.handle(event, Dialog).into() 238 | } else { 239 | Control::Continue 240 | } 241 | }); 242 | 243 | let f = ctx.focus_mut().handle(event, Regular); 244 | ctx.queue(f); 245 | 246 | r 247 | } 248 | MinimalEvent::Rendered => { 249 | ctx.focus = Some(FocusBuilder::rebuild(&self.minimal, ctx.focus.take())); 250 | Control::Continue 251 | } 252 | MinimalEvent::Message(s) => { 253 | ctx.g.status.status(0, &*s); 254 | Control::Changed 255 | } 256 | _ => Control::Continue, 257 | }; 258 | 259 | r = r.or_else_try(|| self.minimal.event(event, ctx))?; 260 | 261 | let el = t0.elapsed()?; 262 | ctx.g.status.status(2, format!("E {:.0?}", el).to_string()); 263 | 264 | Ok(r) 265 | } 266 | ``` 267 | 268 | all event-handling goes through here. 269 | 270 | ```rust 271 | let mut r = match &event { 272 | ct_event!(resized) => Control::Changed, 273 | ct_event!(key press CONTROL-'q') => Control::Quit, 274 | _ => Control::Continue, 275 | }; 276 | ``` 277 | 278 | This reacts to specific crossterm events. Uses the [ct_event!][refCtEvent] 279 | macro, which gives a nicer syntax for event patterns. 280 | 281 | It matches a resized event and returns a Control::Changed result to 282 | the event loop to indicate the need for repaint. 283 | 284 | The second checks for `Ctrl+Q` and just quits the application without 285 | further ado. This is ok while developing things, but maybe a bit crude 286 | for actual use. 287 | 288 | The last result Control::Continue is 'nothing happened, continue 289 | with event handling'. 290 | 291 | ```rust 292 | r = r.or_else(|| { 293 | if ctx.g.error_dlg.active() { 294 | ctx.g.error_dlg.handle(event, Dialog).into() 295 | } else { 296 | Control::Continue 297 | } 298 | }); 299 | ``` 300 | 301 | > Control implements [ConsumedEvent][refConsumedEvent] which 302 | > provides a few combinators. 303 | > 304 | > Event handling can/should stop, when an event is consumed 305 | > by some part of the application. ConsumedEvent::is_consumed 306 | > for Control returns false for Control::Continue and true for 307 | > everything else. And that's what these combinators work with. 308 | 309 | `or_else(..)` is only executed if r is Control::Continue. If the 310 | error dialog is active, which is just some flag, it calls it's 311 | event-handler for `Dialog` style event-handling. It does whatever 312 | it does, the one thing special about it is that `Dialog` mode 313 | consumes all events. This means, if an error dialog is displayed, 314 | only it can react to events, everything else is shut out. 315 | 316 | If the error dialog is not active it uses Control::Continue to 317 | show event handling can continue. 318 | 319 | 320 | ```rust 321 | let f = ctx.focus_mut().handle(event, Regular); 322 | ctx.queue(f); 323 | ``` 324 | Handling events for Focus is a bit special. 325 | 326 | Focus implements an event handler for `Regular` events. Regular is similar 327 | to `Dialog` seen before, and means bog-standard event handling whatever the 328 | widget does. The speciality is that focus handling shouldn't consume the 329 | recognized events. This is important for mouse events, where the widget might 330 | do something useful with the same click event that focused it. 331 | 332 | Here `ctx.queue()` comes into play and provides a second path to return 333 | results from event-handling. The primary return value from the function 334 | call is just added to the same queue. Then everything in that queue is 335 | worked off, before polling new events. 336 | 337 | This way the focus change can initiate a render while the event handling 338 | function can still return whatever it wants. 339 | 340 | 341 | ```rust 342 | MinimalEvent::Message(s) => { 343 | ctx.g.status.status(0, &*s); 344 | Control::Changed 345 | } 346 | ``` 347 | 348 | This is a simple example for a application event. Show something 349 | in the status bar. 350 | 351 | ```rust 352 | // rebuild and handle focus for each event 353 | r = r.or_else(|| { 354 | ctx.focus = Some(FocusBuilder::rebuild(&self.minimal, ctx.focus.take())); 355 | if let MinimalEvent::Event(event) = event { 356 | let f = ctx.focus_mut().handle(event, Regular); 357 | ctx.queue(f); 358 | } 359 | Control::Continue 360 | }); 361 | ``` 362 | 363 | This rebuilds the Focus for each event. 364 | 365 | > TODO: add some feedback loop that can trigger this instead of 366 | > doing it all the time? 367 | 368 | 369 | ```rust 370 | r = r.or_else_try(|| self.minimal.event(event, ctx))?; 371 | ``` 372 | 373 | Forward events. 374 | 375 | 376 | ```rust 377 | Ok(r) 378 | ``` 379 | 380 | And finally the result of event handling is returned to the event loop, 381 | where the event-loop acts upon it. If the result is Control::Message 382 | the event will be added to the current event-queue and processed in 383 | order. Only if the current event-queue is empty will the event loop 384 | poll for a new event. This way the ordering of event+secondary events 385 | stays deterministic. 386 | 387 | 388 | ```rust 389 | fn error( 390 | &self, 391 | event: Error, 392 | ctx: &mut AppContext<'_>, 393 | ) -> Result, Error> { 394 | ctx.g.error_dlg.append(format!("{:?}", &*event).as_str()); 395 | Ok(Control::Changed) 396 | } 397 | ``` 398 | 399 | All errors that end in the event loop are forwarded here for processing. 400 | 401 | This appends the message, which for error dialog sets the dialog 402 | active too. So it will be rendered with the next render. Which is requested 403 | by returning Control::Changed. 404 | 405 | ## mod minimal 406 | 407 | This is the actual application. This example just adds a MenuLine widget and 408 | lets you quit the application via menu. 409 | 410 | ```rust 411 | #[derive(Debug)] 412 | pub(crate) struct Minimal; 413 | 414 | #[derive(Debug)] 415 | pub struct MinimalState { 416 | pub menu: MenuLineState, 417 | } 418 | ``` 419 | 420 | Define the necessary structs and any data/state. 421 | 422 | 423 | ```rust 424 | impl AppWidget for Minimal { 425 | type State = MinimalState; 426 | 427 | fn render( 428 | &self, 429 | area: Rect, 430 | buf: &mut Buffer, 431 | state: &mut Self::State, 432 | ctx: &mut RenderContext<'_>, 433 | ) -> Result<(), Error> { 434 | // TODO: repaint_mask 435 | 436 | let r = Layout::new( 437 | Direction::Vertical, 438 | [ 439 | Constraint::Fill(1), // 440 | Constraint::Length(1), 441 | ], 442 | ) 443 | .split(area); 444 | 445 | let menu = MenuLine::new() 446 | .styles(ctx.g.theme.menu_style()) 447 | .item_parsed("_Quit"); 448 | menu.render(r[1], buf, &mut state.menu); 449 | 450 | Ok(()) 451 | } 452 | } 453 | ``` 454 | 455 | Render the menu. 456 | 457 | ```rust 458 | impl HasFocus for MinimalState { 459 | fn build(&self, builder: &mut FocusBuilder) { 460 | builder.widget(&self.menu); 461 | } 462 | } 463 | ``` 464 | 465 | Implements the trait [HasFocus][refHasFocus] which is the trait 466 | for container like widgets used by [Focus][refFocus]. This adds 467 | its widgets in traversal order. 468 | 469 | ```rust 470 | impl AppState for MinimalState { 471 | ``` 472 | 473 | Implements AppState... 474 | 475 | ```rust 476 | fn init( 477 | &mut self, 478 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, MinimalEvent, Error>, 479 | ) -> Result<(), Error> { 480 | ctx.focus().first(); 481 | self.menu.select(Some(0)); 482 | Ok(()) 483 | } 484 | ``` 485 | 486 | Init sets the focus to the first widget. And does other init work. 487 | 488 | ```rust 489 | fn event( 490 | &mut self, 491 | event: &MinimalEvent, 492 | ctx: &mut rat_salsa::AppContext<'_, GlobalState, MinimalEvent, Error>, 493 | ) -> Result, Error> { 494 | let r = match event { 495 | MinimalEvent::Event(event) => { 496 | match self.menu.handle(event, Regular) { 497 | MenuOutcome::Activated(0) => Control::Quit, 498 | v => v.into(), 499 | } 500 | }, 501 | _ => Control::Continue, 502 | }; 503 | 504 | Ok(r) 505 | } 506 | ``` 507 | 508 | Calls the `Regular` event handler for the menu. MenuLine has its 509 | own return type `MenuOutcome` to signal anything interesting. 510 | What interests here is that the 'Quit' menu item has been 511 | activated. Return the according Control::Quit to end the 512 | application. 513 | 514 | All other values are converted to some Control value. 515 | 516 | ## That's it 517 | 518 | for a start :) 519 | 520 | 521 | [refRunConfig]: https://docs.rs/rat-salsa/latest/rat_salsa/struct.RunConfig.html 522 | 523 | [refLife]: https://github.com/thscharler/rat-salsa/blob/master/examples/life.life 524 | 525 | [refCtEvent]: https://docs.rs/rat-event/latest/rat_event/macro.ct_event.html 526 | 527 | [refConsumedEvent]: https://docs.rs/rat-event/latest/rat_event/trait.ConsumedEvent.html 528 | 529 | [refHasFocus]: https://docs.rs/rat-focus/latest/rat_focus/trait.HasFocus.html 530 | 531 | [refFocus]: https://docs.rs/rat-focus/latest/rat_focus/struct.Focus.html 532 | 533 | [refSalsaTerminal]: https://docs.rs/rat-salsa/latest/rat_salsa/terminal/trait.Terminal.html 534 | 535 | [refPollEvents]: https://docs.rs/rat-salsa/latest/rat_salsa/poll/trait.PollEvents.html 536 | -------------------------------------------------------------------------------- /rsbook/src/render_overlay.md: -------------------------------------------------------------------------------- 1 | 2 | # Overlays / Popups 3 | 4 | ratatui itself has no builtin facilities for widgets that render 5 | as overlay over other widgets. 6 | 7 | For widgets that are only rendered as overlay, the solution is 8 | straight forward: render them after all widgets that should be 9 | below have been rendered. 10 | 11 | That leaves widget that are only partial overlays, such as 12 | Menubar and Split. They solve this, by not implementing any 13 | widget trait, instead they act as widget-builders, and have 14 | a method `into_widgets()` that return two widgets. One for 15 | the base-rendering and one for the popup. Only those are 16 | ratatui-widgets, and they have no further configuration methods. 17 | 18 | ## Event Handling 19 | 20 | Event-handling can be structured similarly. 21 | -------------------------------------------------------------------------------- /rsbook/src/widget-extensions.md: -------------------------------------------------------------------------------- 1 | # Extensions 2 | -------------------------------------------------------------------------------- /rsbook/src/widgets.md: -------------------------------------------------------------------------------- 1 | # Widgets 2 | 3 | 4 | A part of rat-salsa but still independent is 5 | [rat-widget][refRatWidget]. 6 | 7 | All can work with Focus, use crossterm events, 8 | scrolling where needed, try to not allocate for rendering. 9 | 10 | All are regular widgets and can be used without rat-salsa. 11 | 12 | 13 | It contains 14 | 15 | * Button 16 | * Choice 17 | * Checkbox 18 | * Radio 19 | * Slider 20 | * DateInput and NumberInput 21 | * TextInput and MaskedInput 22 | * TextArea and LineNumber 23 | * Table 24 | * EditTable and EditList 25 | * Tabbed and Split 26 | * MenuLine, PopupMenu and Menubar 27 | * StatusLine and MsgDialog 28 | * FileDialog 29 | * EditList and EditTable 30 | * View, Clipper, SinglePager, DualPager: for scrolling/page-breaking 31 | * Month 32 | 33 | and adapters for 34 | 35 | * List 36 | * Paragraph 37 | 38 | 39 | [refRatWidget]: https://docs.rs/rat-widget/ 40 | -------------------------------------------------------------------------------- /src/control_queue.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Queue for all the results from event-handling. 3 | //! 4 | 5 | use crate::Control; 6 | use std::cell::RefCell; 7 | use std::collections::VecDeque; 8 | 9 | /// Queue for event-handling results. 10 | #[derive(Debug)] 11 | pub(crate) struct ControlQueue 12 | where 13 | Event: 'static + Send, 14 | Error: 'static + Send, 15 | { 16 | queue: RefCell, Error>>>, 17 | } 18 | 19 | impl Default for ControlQueue 20 | where 21 | Event: 'static + Send, 22 | Error: 'static + Send, 23 | { 24 | fn default() -> Self { 25 | Self { 26 | queue: RefCell::new(VecDeque::default()), 27 | } 28 | } 29 | } 30 | 31 | impl ControlQueue 32 | where 33 | Event: 'static + Send, 34 | Error: 'static + Send, 35 | { 36 | /// is empty 37 | pub(crate) fn is_empty(&self) -> bool { 38 | self.queue.borrow().is_empty() 39 | } 40 | 41 | /// take the first result. 42 | pub(crate) fn take(&self) -> Option, Error>> { 43 | self.queue.borrow_mut().pop_front() 44 | } 45 | 46 | /// push a new result to the queue. 47 | pub(crate) fn push(&self, ctrl: Result, Error>) { 48 | self.queue.borrow_mut().push_back(ctrl); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/framework.rs: -------------------------------------------------------------------------------- 1 | use crate::control_queue::ControlQueue; 2 | use crate::poll::{PollRendered, PollTasks, PollTimers}; 3 | use crate::poll_queue::PollQueue; 4 | use crate::run_config::RunConfig; 5 | use crate::threadpool::ThreadPool; 6 | use crate::timer::Timers; 7 | #[cfg(feature = "async")] 8 | use crate::tokio_tasks::PollTokio; 9 | use crate::{AppContext, AppState, AppWidget, Control, RenderContext}; 10 | use crossbeam::channel::{SendError, TryRecvError}; 11 | use std::any::TypeId; 12 | use std::cmp::min; 13 | use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; 14 | use std::time::Duration; 15 | use std::{io, thread}; 16 | 17 | const SLEEP: u64 = 250_000; // µs 18 | const BACKOFF: u64 = 10_000; // µs 19 | const FAST_SLEEP: u64 = 100; // µs 20 | 21 | fn _run_tui( 22 | app: App, 23 | global: &mut Global, 24 | state: &mut App::State, 25 | cfg: &mut RunConfig, 26 | ) -> Result<(), Error> 27 | where 28 | App: AppWidget, 29 | App: 'static, 30 | Global: 'static, 31 | Event: Send + 'static, 32 | Error: Send + 'static + From + From + From>, 33 | { 34 | let term = cfg.term.as_mut(); 35 | let poll = cfg.poll.as_mut_slice(); 36 | 37 | let timers = if poll 38 | .iter() 39 | .any(|v| v.as_ref().type_id() == TypeId::of::()) 40 | { 41 | Some(Timers::default()) 42 | } else { 43 | None 44 | }; 45 | let tasks = if poll 46 | .iter() 47 | .any(|v| v.as_ref().type_id() == TypeId::of::()) 48 | { 49 | Some(ThreadPool::new(cfg.n_threats)) 50 | } else { 51 | None 52 | }; 53 | let rendered_event = poll.iter().enumerate().find_map(|(n, v)| { 54 | if v.as_ref().type_id() == TypeId::of::() { 55 | Some(n) 56 | } else { 57 | None 58 | } 59 | }); 60 | #[cfg(feature = "async")] 61 | let tokio_spawn = poll.iter().find_map(|v| { 62 | if let Some(t) = v.as_any().downcast_ref::>() { 63 | Some(t.get_spawn()) 64 | } else { 65 | None 66 | } 67 | }); 68 | let queue = ControlQueue::default(); 69 | 70 | let mut appctx = AppContext { 71 | g: global, 72 | focus: None, 73 | count: 0, 74 | timers: &timers, 75 | tasks: &tasks, 76 | #[cfg(feature = "async")] 77 | tokio: &tokio_spawn, 78 | queue: &queue, 79 | }; 80 | 81 | let poll_queue = PollQueue::default(); 82 | let mut poll_sleep = Duration::from_micros(SLEEP); 83 | 84 | // init state 85 | state.init(&mut appctx)?; 86 | 87 | // initial render 88 | appctx.count = term.render(&mut |frame| { 89 | let mut ctx = RenderContext { 90 | g: appctx.g, 91 | count: frame.count(), 92 | cursor: None, 93 | }; 94 | let frame_area = frame.area(); 95 | app.render(frame_area, frame.buffer_mut(), state, &mut ctx)?; 96 | if let Some((cursor_x, cursor_y)) = ctx.cursor { 97 | frame.set_cursor_position((cursor_x, cursor_y)); 98 | } 99 | Ok(frame.count()) 100 | })?; 101 | if let Some(h) = rendered_event { 102 | let r = poll[h].read_exec(state, &mut appctx); 103 | queue.push(r); 104 | } 105 | 106 | 'ui: loop { 107 | // panic on worker panic 108 | if let Some(tasks) = &tasks { 109 | if !tasks.check_liveness() { 110 | dbg!("worker panicked"); 111 | break 'ui; 112 | } 113 | } 114 | 115 | // No events queued, check here. 116 | if queue.is_empty() { 117 | // The events are not processed immediately, but all 118 | // notifies are queued in the poll_queue. 119 | if poll_queue.is_empty() { 120 | for (n, p) in poll.iter_mut().enumerate() { 121 | match p.poll(&mut appctx) { 122 | Ok(true) => { 123 | poll_queue.push(n); 124 | } 125 | Ok(false) => {} 126 | Err(e) => { 127 | queue.push(Err(e)); 128 | } 129 | } 130 | } 131 | } 132 | 133 | // Sleep regime. 134 | if poll_queue.is_empty() { 135 | let t = if let Some(timers) = &timers { 136 | if let Some(timer_sleep) = timers.sleep_time() { 137 | min(timer_sleep, poll_sleep) 138 | } else { 139 | poll_sleep 140 | } 141 | } else { 142 | poll_sleep 143 | }; 144 | thread::sleep(t); 145 | if poll_sleep < Duration::from_micros(SLEEP) { 146 | // Back off slowly. 147 | poll_sleep += Duration::from_micros(BACKOFF); 148 | } 149 | } else { 150 | // Shorter sleep immediately after an event. 151 | poll_sleep = Duration::from_micros(FAST_SLEEP); 152 | } 153 | } 154 | 155 | // All the fall-out of the last event has cleared. 156 | // Run the next event. 157 | if queue.is_empty() { 158 | if let Some(h) = poll_queue.take() { 159 | let r = poll[h].read_exec(state, &mut appctx); 160 | queue.push(r); 161 | } 162 | } 163 | 164 | // Result of event-handling. 165 | if let Some(ctrl) = queue.take() { 166 | match ctrl { 167 | Err(e) => { 168 | let r = state.error(e, &mut appctx); 169 | queue.push(r); 170 | } 171 | Ok(Control::Continue) => {} 172 | Ok(Control::Unchanged) => {} 173 | Ok(Control::Changed) => { 174 | let r = term.render(&mut |frame| { 175 | let mut ctx = RenderContext { 176 | g: appctx.g, 177 | count: frame.count(), 178 | cursor: None, 179 | }; 180 | let frame_area = frame.area(); 181 | app.render(frame_area, frame.buffer_mut(), state, &mut ctx)?; 182 | if let Some((cursor_x, cursor_y)) = ctx.cursor { 183 | frame.set_cursor_position((cursor_x, cursor_y)); 184 | } 185 | Ok(frame.count()) 186 | }); 187 | match r { 188 | Ok(v) => { 189 | appctx.count = v; 190 | if let Some(h) = rendered_event { 191 | let r = poll[h].read_exec(state, &mut appctx); 192 | queue.push(r); 193 | } 194 | } 195 | Err(e) => queue.push(Err(e)), 196 | } 197 | } 198 | Ok(Control::Message(a)) => { 199 | let r = state.event(&a, &mut appctx); 200 | queue.push(r); 201 | } 202 | Ok(Control::Quit) => { 203 | break 'ui; 204 | } 205 | } 206 | } 207 | } 208 | 209 | state.shutdown(&mut appctx)?; 210 | 211 | Ok(()) 212 | } 213 | 214 | /// Run the event-loop 215 | /// 216 | /// The shortest version I can come up with: 217 | /// ```rust no_run 218 | /// use rat_salsa::{run_tui, AppContext, AppState, AppWidget, Control, RenderContext, RunConfig}; 219 | /// use ratatui::buffer::Buffer; 220 | /// use ratatui::layout::Rect; 221 | /// use ratatui::style::Stylize; 222 | /// use ratatui::text::Span; 223 | /// use ratatui::widgets::Widget; 224 | /// use rat_widget::event::{try_flow, ct_event}; 225 | /// 226 | /// #[derive(Debug)] 227 | /// struct MainApp; 228 | /// 229 | /// #[derive(Debug)] 230 | /// struct MainState; 231 | /// 232 | /// #[derive(Debug)] 233 | /// enum Event { 234 | /// Event(crossterm::event::Event) 235 | /// } 236 | /// 237 | /// impl From for Event { 238 | /// fn from(value: crossterm::event::Event) -> Self { 239 | /// Self::Event(value) 240 | /// } 241 | /// } 242 | /// 243 | /// impl AppWidget<(), Event, anyhow::Error> for MainApp { 244 | /// type State = MainState; 245 | /// 246 | /// fn render( 247 | /// &self, 248 | /// area: Rect, 249 | /// buf: &mut Buffer, 250 | /// _state: &mut Self::State, 251 | /// _ctx: &mut RenderContext<'_, ()>, 252 | /// ) -> Result<(), anyhow::Error> { 253 | /// Span::from("Hello world") 254 | /// .white() 255 | /// .on_blue() 256 | /// .render(area, buf); 257 | /// Ok(()) 258 | /// } 259 | /// } 260 | /// 261 | /// impl AppState<(), Event, anyhow::Error> for MainState { 262 | /// fn event( 263 | /// &mut self, 264 | /// event: &Event, 265 | /// _ctx: &mut AppContext<'_, (), (), anyhow::Error>, 266 | /// ) -> Result, anyhow::Error> { 267 | /// let Event::Event(event) = event else { 268 | /// return Ok(Control::Continue); 269 | /// }; 270 | /// 271 | /// try_flow!(match event { 272 | /// ct_event!(key press 'q') => Control::Quit, 273 | /// _ => Control::Continue, 274 | /// }); 275 | /// 276 | /// Ok(Control::Continue) 277 | /// } 278 | /// } 279 | /// 280 | /// fn main() -> Result<(), anyhow::Error> { 281 | /// use rat_salsa::poll::PollCrossterm; 282 | /// run_tui(MainApp, 283 | /// &mut (), 284 | /// &mut MainState, 285 | /// RunConfig::default()? 286 | /// .poll(PollCrossterm) 287 | /// )?; 288 | /// Ok(()) 289 | /// } 290 | /// 291 | /// ``` 292 | /// 293 | /// Maybe `examples/minimal.rs` is more useful. 294 | /// 295 | pub fn run_tui( 296 | app: Widget, 297 | global: &mut Global, 298 | state: &mut Widget::State, 299 | mut cfg: RunConfig, 300 | ) -> Result<(), Error> 301 | where 302 | Widget: AppWidget + 'static, 303 | Global: 'static, 304 | Event: Send + 'static, 305 | Error: Send + 'static + From + From + From>, 306 | { 307 | cfg.term.init()?; 308 | 309 | let r = match catch_unwind(AssertUnwindSafe(|| _run_tui(app, global, state, &mut cfg))) { 310 | Ok(v) => v, 311 | Err(e) => { 312 | _ = cfg.term.shutdown(); 313 | resume_unwind(e); 314 | } 315 | }; 316 | 317 | cfg.term.shutdown()?; 318 | 319 | r 320 | } 321 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../readme.md")] 2 | 3 | use crossbeam::channel::{SendError, Sender}; 4 | use rat_widget::event::{ConsumedEvent, Outcome}; 5 | use ratatui::buffer::Buffer; 6 | use ratatui::layout::Rect; 7 | use std::cmp::Ordering; 8 | use std::fmt::Debug; 9 | #[cfg(feature = "async")] 10 | use std::future::Future; 11 | use std::mem; 12 | #[cfg(feature = "async")] 13 | use tokio::task::AbortHandle; 14 | 15 | pub(crate) mod control_queue; 16 | mod framework; 17 | pub mod poll; 18 | pub(crate) mod poll_queue; 19 | mod run_config; 20 | pub mod terminal; 21 | mod threadpool; 22 | pub mod timer; 23 | #[cfg(feature = "async")] 24 | mod tokio_tasks; 25 | 26 | use crate::control_queue::ControlQueue; 27 | use crate::threadpool::ThreadPool; 28 | use crate::timer::{TimerDef, TimerHandle, Timers}; 29 | #[cfg(feature = "async")] 30 | use crate::tokio_tasks::TokioSpawn; 31 | use rat_widget::focus::Focus; 32 | 33 | pub use framework::*; 34 | pub use run_config::*; 35 | pub use threadpool::Cancel; 36 | #[cfg(feature = "async")] 37 | pub use tokio_tasks::PollTokio; 38 | 39 | /// Result enum for event handling. 40 | /// 41 | /// The result of an event is processed immediately after the 42 | /// function returns, before polling new events. This way an action 43 | /// can trigger another action which triggers the repaint without 44 | /// other events intervening. 45 | /// 46 | /// If you ever need to return more than one result from event-handling, 47 | /// you can hand it to AppContext/RenderContext::queue(). Events 48 | /// in the queue are processed in order, and the return value of 49 | /// the event-handler comes last. If an error is returned, everything 50 | /// send to the queue will be executed nonetheless. 51 | /// 52 | /// __See__ 53 | /// 54 | /// - [flow!](rat_widget::event::flow) 55 | /// - [try_flow!](rat_widget::event::try_flow) 56 | /// - [ConsumedEvent] 57 | #[derive(Debug, Clone, Copy)] 58 | #[must_use] 59 | #[non_exhaustive] 60 | pub enum Control { 61 | /// Continue with event-handling. 62 | /// In the event-loop this waits for the next event. 63 | Continue, 64 | /// Break event-handling without repaint. 65 | /// In the event-loop this waits for the next event. 66 | Unchanged, 67 | /// Break event-handling and repaints/renders the application. 68 | /// In the event-loop this calls `render`. 69 | Changed, 70 | /// Eventhandling can cause secondary application specific events. 71 | /// One common way is to return this `Control::Message(my_event)` 72 | /// to reenter the event-loop with your own secondary event. 73 | /// 74 | /// This acts quite like a message-queue to communicate between 75 | /// disconnected parts of your application. And indeed there is 76 | /// a hidden message-queue as part of the event-loop. 77 | /// 78 | /// The other way is to call [AppContext::queue] to initiate such 79 | /// events. 80 | Message(Event), 81 | /// Quit the application. 82 | Quit, 83 | } 84 | 85 | impl Eq for Control {} 86 | 87 | impl PartialEq for Control { 88 | fn eq(&self, other: &Self) -> bool { 89 | mem::discriminant(self) == mem::discriminant(other) 90 | } 91 | } 92 | 93 | impl Ord for Control { 94 | fn cmp(&self, other: &Self) -> Ordering { 95 | match self { 96 | Control::Continue => match other { 97 | Control::Continue => Ordering::Equal, 98 | Control::Unchanged => Ordering::Less, 99 | Control::Changed => Ordering::Less, 100 | Control::Message(_) => Ordering::Less, 101 | Control::Quit => Ordering::Less, 102 | }, 103 | Control::Unchanged => match other { 104 | Control::Continue => Ordering::Greater, 105 | Control::Unchanged => Ordering::Equal, 106 | Control::Changed => Ordering::Less, 107 | Control::Message(_) => Ordering::Less, 108 | Control::Quit => Ordering::Less, 109 | }, 110 | Control::Changed => match other { 111 | Control::Continue => Ordering::Greater, 112 | Control::Unchanged => Ordering::Greater, 113 | Control::Changed => Ordering::Equal, 114 | Control::Message(_) => Ordering::Less, 115 | Control::Quit => Ordering::Less, 116 | }, 117 | Control::Message(_) => match other { 118 | Control::Continue => Ordering::Greater, 119 | Control::Unchanged => Ordering::Greater, 120 | Control::Changed => Ordering::Greater, 121 | Control::Message(_) => Ordering::Equal, 122 | Control::Quit => Ordering::Less, 123 | }, 124 | Control::Quit => match other { 125 | Control::Continue => Ordering::Greater, 126 | Control::Unchanged => Ordering::Greater, 127 | Control::Changed => Ordering::Greater, 128 | Control::Message(_) => Ordering::Greater, 129 | Control::Quit => Ordering::Equal, 130 | }, 131 | } 132 | } 133 | } 134 | 135 | impl PartialOrd for Control { 136 | fn partial_cmp(&self, other: &Self) -> Option { 137 | Some(self.cmp(other)) 138 | } 139 | } 140 | 141 | impl ConsumedEvent for Control { 142 | fn is_consumed(&self) -> bool { 143 | !matches!(self, Control::Continue) 144 | } 145 | } 146 | 147 | impl> From for Control { 148 | fn from(value: T) -> Self { 149 | let r = value.into(); 150 | match r { 151 | Outcome::Continue => Control::Continue, 152 | Outcome::Unchanged => Control::Unchanged, 153 | Outcome::Changed => Control::Changed, 154 | } 155 | } 156 | } 157 | 158 | /// 159 | /// AppWidget mimics StatefulWidget and adds a [RenderContext] 160 | /// 161 | pub trait AppWidget 162 | where 163 | Event: 'static + Send, 164 | Error: 'static + Send, 165 | { 166 | /// Type of the State. 167 | type State: AppState + ?Sized; 168 | 169 | /// Renders an application widget. 170 | fn render( 171 | &self, 172 | area: Rect, 173 | buf: &mut Buffer, 174 | state: &mut Self::State, 175 | ctx: &mut RenderContext<'_, Global>, 176 | ) -> Result<(), Error>; 177 | } 178 | 179 | /// 180 | pub struct RenderedEvent; 181 | 182 | /// 183 | /// AppState executes events and has some init and error-handling. 184 | /// 185 | /// There is no separate shutdown, handle this case with an application 186 | /// specific event. 187 | /// 188 | #[allow(unused_variables)] 189 | pub trait AppState 190 | where 191 | Event: 'static + Send, 192 | Error: 'static + Send, 193 | { 194 | /// Initialize the application. Runs before the first repaint. 195 | fn init( 196 | &mut self, // 197 | ctx: &mut AppContext<'_, Global, Event, Error>, 198 | ) -> Result<(), Error> { 199 | Ok(()) 200 | } 201 | 202 | /// Shutdown the application. Runs after the event-loop has ended. 203 | /// 204 | /// __Panic__ 205 | /// 206 | /// Doesn't run if a panic occurred. 207 | /// 208 | ///__Errors__ 209 | /// Any errors will be returned to main(). 210 | fn shutdown(&mut self, ctx: &mut AppContext<'_, Global, Event, Error>) -> Result<(), Error> { 211 | Ok(()) 212 | } 213 | 214 | /// Handle an event. 215 | fn event( 216 | &mut self, 217 | event: &Event, 218 | ctx: &mut AppContext<'_, Global, Event, Error>, 219 | ) -> Result, Error> { 220 | Ok(Control::Continue) 221 | } 222 | 223 | /// Do error handling. 224 | fn error( 225 | &self, 226 | event: Error, 227 | ctx: &mut AppContext<'_, Global, Event, Error>, 228 | ) -> Result, Error> { 229 | Ok(Control::Continue) 230 | } 231 | } 232 | 233 | /// 234 | /// Application context for event handling. 235 | /// 236 | #[derive(Debug)] 237 | pub struct AppContext<'a, Global, Event, Error> 238 | where 239 | Event: 'static + Send, 240 | Error: 'static + Send, 241 | { 242 | /// Global state for the application. 243 | pub g: &'a mut Global, 244 | /// Can be set to hold a Focus, if needed. 245 | pub focus: Option, 246 | /// Last frame count rendered. 247 | pub count: usize, 248 | 249 | /// Application timers. 250 | pub(crate) timers: &'a Option, 251 | /// Background tasks. 252 | pub(crate) tasks: &'a Option>, 253 | /// Background tasks. 254 | #[cfg(feature = "async")] 255 | pub(crate) tokio: &'a Option>, 256 | /// Queue foreground tasks. 257 | pub(crate) queue: &'a ControlQueue, 258 | } 259 | 260 | /// 261 | /// Application context for rendering. 262 | /// 263 | #[derive(Debug)] 264 | pub struct RenderContext<'a, Global> { 265 | /// Some global state for the application. 266 | pub g: &'a mut Global, 267 | /// Frame counter. 268 | pub count: usize, 269 | /// Output cursor position. Set after rendering is complete. 270 | pub cursor: Option<(u16, u16)>, 271 | } 272 | 273 | impl<'a, Global, Event, Error> AppContext<'a, Global, Event, Error> 274 | where 275 | Event: 'static + Send, 276 | Error: 'static + Send, 277 | { 278 | /// Add a timer. 279 | /// 280 | /// __Panic__ 281 | /// 282 | /// Panics if no timer support is configured. 283 | #[inline] 284 | pub fn add_timer(&self, t: TimerDef) -> TimerHandle { 285 | self.timers 286 | .as_ref() 287 | .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)") 288 | .add(t) 289 | } 290 | 291 | /// Remove a timer. 292 | /// 293 | /// __Panic__ 294 | /// 295 | /// Panics if no timer support is configured. 296 | #[inline] 297 | pub fn remove_timer(&self, tag: TimerHandle) { 298 | self.timers 299 | .as_ref() 300 | .expect("No timers configured. In main() add RunConfig::default()?.poll(PollTimers)") 301 | .remove(tag); 302 | } 303 | 304 | /// Replace a timer. 305 | /// Remove the old timer and create a new one. 306 | /// If the old timer no longer exists it just creates the new one. 307 | /// 308 | /// __Panic__ 309 | /// 310 | /// Panics if no timer support is configured. 311 | #[inline] 312 | pub fn replace_timer(&self, h: Option, t: TimerDef) -> TimerHandle { 313 | if let Some(h) = h { 314 | self.remove_timer(h); 315 | } 316 | self.add_timer(t) 317 | } 318 | 319 | /// Add a background worker task. 320 | /// 321 | /// ```rust ignore 322 | /// let cancel = ctx.spawn(|cancel, send| { 323 | /// // ... do stuff 324 | /// Ok(Control::Continue) 325 | /// }); 326 | /// ``` 327 | /// 328 | /// __Panic__ 329 | /// 330 | /// Panics if no worker-thread support is configured. 331 | #[inline] 332 | pub fn spawn( 333 | &self, 334 | task: impl FnOnce(Cancel, &Sender, Error>>) -> Result, Error> 335 | + Send 336 | + 'static, 337 | ) -> Result> 338 | where 339 | Event: 'static + Send, 340 | Error: 'static + Send, 341 | { 342 | self.tasks 343 | .as_ref() 344 | .expect( 345 | "No thread-pool configured. In main() add RunConfig::default()?.poll(PollTasks)", 346 | ) 347 | .send(Box::new(task)) 348 | } 349 | 350 | /// Spawn a future in the executor. 351 | #[inline] 352 | #[cfg(feature = "async")] 353 | pub fn spawn_async(&self, future: F) -> AbortHandle 354 | where 355 | F: Future, Error>> + Send + 'static, 356 | { 357 | self.tokio.as_ref().expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))") 358 | .spawn(Box::new(future)) 359 | } 360 | 361 | /// Spawn a future in the executor. 362 | /// You get an extra channel to send back more than one result. 363 | #[inline] 364 | #[cfg(feature = "async")] 365 | pub fn spawn_async_ext(&self, cr_future: C) -> AbortHandle 366 | where 367 | C: FnOnce(tokio::sync::mpsc::Sender, Error>>) -> F, 368 | F: Future, Error>> + Send + 'static, 369 | { 370 | let rt = self.tokio.as_ref().expect("No tokio runtime is configured. In main() add RunConfig::default()?.poll(PollTokio::new(rt))"); 371 | let future = cr_future(rt.sender()); 372 | rt.spawn(Box::new(future)) 373 | } 374 | 375 | /// Queue additional results. 376 | #[inline] 377 | pub fn queue(&self, ctrl: impl Into>) { 378 | self.queue.push(Ok(ctrl.into())); 379 | } 380 | 381 | /// Queue an error. 382 | #[inline] 383 | pub fn queue_err(&self, err: Error) { 384 | self.queue.push(Err(err)); 385 | } 386 | 387 | /// Access the focus-field. 388 | /// 389 | /// __Panic__ 390 | /// 391 | /// Panics if no focus has been set. 392 | #[inline] 393 | pub fn focus(&self) -> &Focus { 394 | self.focus.as_ref().expect("focus") 395 | } 396 | 397 | /// Access the focus-field. 398 | /// 399 | /// __Panic__ 400 | /// 401 | /// Panics if no focus has been set. 402 | #[inline] 403 | pub fn focus_mut(&mut self) -> &mut Focus { 404 | self.focus.as_mut().expect("focus") 405 | } 406 | } 407 | 408 | impl<'a, Global> RenderContext<'a, Global> { 409 | /// Set the cursor, if the given value is Some. 410 | pub fn set_screen_cursor(&mut self, cursor: Option<(u16, u16)>) { 411 | if let Some(c) = cursor { 412 | self.cursor = Some(c); 413 | } 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /src/poll.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Defines the trait for event-sources. 3 | //! 4 | 5 | use crate::timer::{TimeOut, TimerEvent}; 6 | use crate::{AppContext, AppState, Control, RenderedEvent}; 7 | use crossbeam::channel::TryRecvError; 8 | use std::any::Any; 9 | use std::fmt::Debug; 10 | use std::time::Duration; 11 | 12 | /// Trait for an event-source. 13 | /// 14 | /// If you need to add your own do the following: 15 | /// 16 | /// * Implement this trait for a struct that fits. 17 | /// 18 | pub trait PollEvents: Any 19 | where 20 | State: AppState + ?Sized, 21 | Event: 'static + Send, 22 | Error: 'static + Send, 23 | { 24 | fn as_any(&self) -> &dyn Any; 25 | 26 | /// Poll for a new event. 27 | /// 28 | /// Events are not processed immediately when they occur. Instead, 29 | /// all event sources are polled, the poll state is put into a queue. 30 | /// Then the queue is emptied one by one and `read_execute()` is called. 31 | /// 32 | /// This prevents issues with poll-ordering of multiple sources, and 33 | /// one source cannot just flood the app with events. 34 | fn poll( 35 | &mut self, // 36 | ctx: &mut AppContext<'_, Global, Event, Error>, 37 | ) -> Result; 38 | 39 | /// Read the event and distribute it. 40 | /// 41 | /// If you add a new event, that doesn't fit into AppEvents, you'll 42 | /// have to define a new trait for your AppState and use that. 43 | fn read_exec( 44 | &mut self, 45 | state: &mut State, 46 | ctx: &mut AppContext<'_, Global, Event, Error>, 47 | ) -> Result, Error>; 48 | } 49 | 50 | /// Processes results from background tasks. 51 | #[derive(Debug)] 52 | pub struct PollTasks; 53 | 54 | impl PollEvents for PollTasks 55 | where 56 | State: AppState + ?Sized, 57 | Event: 'static + Send, 58 | Error: 'static + Send + From, 59 | { 60 | fn as_any(&self) -> &dyn Any { 61 | self 62 | } 63 | 64 | fn poll(&mut self, ctx: &mut AppContext<'_, Global, Event, Error>) -> Result { 65 | if let Some(tasks) = ctx.tasks { 66 | Ok(!tasks.is_empty()) 67 | } else { 68 | Ok(false) 69 | } 70 | } 71 | 72 | fn read_exec( 73 | &mut self, 74 | _state: &mut State, 75 | ctx: &mut AppContext<'_, Global, Event, Error>, 76 | ) -> Result, Error> { 77 | if let Some(tasks) = ctx.tasks { 78 | tasks.try_recv() 79 | } else { 80 | Ok(Control::Continue) 81 | } 82 | } 83 | } 84 | 85 | /// Processes timers. 86 | #[derive(Debug, Default)] 87 | pub struct PollTimers; 88 | 89 | impl PollEvents for PollTimers 90 | where 91 | State: AppState + ?Sized, 92 | Event: 'static + Send + From, 93 | Error: 'static + Send + From, 94 | { 95 | fn as_any(&self) -> &dyn Any { 96 | self 97 | } 98 | 99 | fn poll(&mut self, ctx: &mut AppContext<'_, Global, Event, Error>) -> Result { 100 | if let Some(timers) = ctx.timers { 101 | Ok(timers.poll()) 102 | } else { 103 | Ok(false) 104 | } 105 | } 106 | 107 | fn read_exec( 108 | &mut self, 109 | state: &mut State, 110 | ctx: &mut AppContext<'_, Global, Event, Error>, 111 | ) -> Result, Error> { 112 | if let Some(timers) = ctx.timers { 113 | match timers.read() { 114 | None => Ok(Control::Continue), 115 | Some(TimerEvent(t)) => state.event(&t.into(), ctx), 116 | } 117 | } else { 118 | Ok(Control::Continue) 119 | } 120 | } 121 | } 122 | 123 | /// Processes crossterm events. 124 | #[derive(Debug)] 125 | pub struct PollCrossterm; 126 | 127 | impl PollEvents for PollCrossterm 128 | where 129 | State: AppState + ?Sized, 130 | Event: 'static + Send + From, 131 | Error: 'static + Send + From, 132 | { 133 | fn as_any(&self) -> &dyn Any { 134 | self 135 | } 136 | 137 | fn poll(&mut self, _ctx: &mut AppContext<'_, Global, Event, Error>) -> Result { 138 | Ok(crossterm::event::poll(Duration::from_millis(0))?) 139 | } 140 | 141 | fn read_exec( 142 | &mut self, 143 | state: &mut State, 144 | ctx: &mut AppContext<'_, Global, Event, Error>, 145 | ) -> Result, Error> { 146 | match crossterm::event::read() { 147 | Ok(event) => state.event(&event.into(), ctx), 148 | Err(e) => Err(e.into()), 149 | } 150 | } 151 | } 152 | 153 | /// Sends an event after a render of the UI. 154 | #[derive(Debug, Default)] 155 | pub struct PollRendered; 156 | 157 | impl PollEvents for PollRendered 158 | where 159 | State: AppState + ?Sized, 160 | Event: 'static + Send + From, 161 | Error: 'static + Send + From, 162 | { 163 | fn as_any(&self) -> &dyn Any { 164 | self 165 | } 166 | 167 | fn poll(&mut self, _ctx: &mut AppContext<'_, Global, Event, Error>) -> Result { 168 | // doesn't poll. it's triggered by a repaint. 169 | Ok(false) 170 | } 171 | 172 | fn read_exec( 173 | &mut self, 174 | state: &mut State, 175 | ctx: &mut AppContext<'_, Global, Event, Error>, 176 | ) -> Result, Error> { 177 | state.event(&RenderedEvent.into(), ctx) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/poll_queue.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::VecDeque; 3 | 4 | /// Queue for which EventPoll wants to be read. 5 | #[derive(Debug, Default)] 6 | pub(crate) struct PollQueue { 7 | queue: RefCell>, 8 | } 9 | 10 | impl PollQueue { 11 | /// Empty 12 | pub(crate) fn is_empty(&self) -> bool { 13 | self.queue.borrow().is_empty() 14 | } 15 | 16 | /// Take the next handle. 17 | pub(crate) fn take(&self) -> Option { 18 | self.queue.borrow_mut().pop_front() 19 | } 20 | 21 | /// Push a handle to the queue. 22 | pub(crate) fn push(&self, poll: usize) { 23 | self.queue.borrow_mut().push_back(poll); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/run_config.rs: -------------------------------------------------------------------------------- 1 | use crate::poll::PollEvents; 2 | use crate::terminal::{CrosstermTerminal, Terminal}; 3 | use crate::AppWidget; 4 | use crossbeam::channel::TryRecvError; 5 | use std::fmt::{Debug, Formatter}; 6 | use std::io; 7 | 8 | /// Captures some parameters for [crate::run_tui()]. 9 | pub struct RunConfig 10 | where 11 | App: AppWidget, 12 | Event: 'static + Send, 13 | Error: 'static + Send, 14 | { 15 | /// How many worker threads are wanted? 16 | /// Most of the time 1 should be sufficient to offload any gui-blocking tasks. 17 | pub(crate) n_threats: usize, 18 | /// This is the renderer that connects to the backend, and calls out 19 | /// for rendering the application. 20 | /// 21 | /// Defaults to RenderCrossterm. 22 | pub(crate) term: Box>, 23 | /// List of all event-handlers for the application. 24 | /// 25 | /// Defaults to PollTimers, PollCrossterm, PollTasks. Add yours here. 26 | pub(crate) poll: Vec>>, 27 | } 28 | 29 | impl Debug for RunConfig 30 | where 31 | App: AppWidget, 32 | Event: 'static + Send, 33 | Error: 'static + Send, 34 | { 35 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 36 | f.debug_struct("RunConfig") 37 | .field("n_threads", &self.n_threats) 38 | .field("render", &"...") 39 | .field("events", &"...") 40 | .finish() 41 | } 42 | } 43 | 44 | impl RunConfig 45 | where 46 | App: AppWidget, 47 | Event: 'static + Send, 48 | Error: 'static + Send + From + From, 49 | { 50 | /// New configuration with some defaults. 51 | #[allow(clippy::should_implement_trait)] 52 | pub fn default() -> Result { 53 | Ok(Self { 54 | n_threats: 1, 55 | term: Box::new(CrosstermTerminal::new()?), 56 | poll: Default::default(), 57 | }) 58 | } 59 | 60 | /// Number of background-threads. 61 | /// Default is 1. 62 | pub fn threads(mut self, n: usize) -> Self { 63 | self.n_threats = n; 64 | self 65 | } 66 | 67 | /// Terminal is a rat-salsa::terminal::Terminal not a ratatui::Terminal. 68 | pub fn term(mut self, term: impl Terminal + 'static) -> Self { 69 | self.term = Box::new(term); 70 | self 71 | } 72 | 73 | /// Add one more poll impl. 74 | pub fn poll( 75 | mut self, 76 | poll: impl PollEvents + 'static, 77 | ) -> Self { 78 | self.poll.push(Box::new(poll)); 79 | self 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/terminal.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! rat-salsa's own Terminal trait to hide some details. 3 | //! 4 | //! This hides the actual implementation for init/shutdown 5 | //! and can be used as dyn Terminal to avoid adding more T's. 6 | //! 7 | 8 | use crossterm::cursor::{DisableBlinking, EnableBlinking, SetCursorStyle}; 9 | use crossterm::event::{ 10 | DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, 11 | }; 12 | #[cfg(not(windows))] 13 | use crossterm::event::{ 14 | KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, 15 | }; 16 | #[cfg(not(windows))] 17 | use crossterm::terminal::supports_keyboard_enhancement; 18 | use crossterm::terminal::{ 19 | disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, 20 | }; 21 | use crossterm::ExecutableCommand; 22 | use rat_widget::event::util::set_have_keyboard_enhancement; 23 | use ratatui::backend::CrosstermBackend; 24 | use ratatui::Frame; 25 | use std::fmt::Debug; 26 | use std::io; 27 | use std::io::{stdout, Stdout}; 28 | 29 | /// Encapsulates Terminal and Backend. 30 | /// 31 | /// This is used as dyn Trait to hide the Background type parameter. 32 | /// 33 | /// If you want to send other than the default Commands to the backend, 34 | /// implement this trait. 35 | pub trait Terminal 36 | where 37 | Error: 'static + Send, 38 | { 39 | /// Terminal init. 40 | fn init(&mut self) -> Result<(), Error> 41 | where 42 | Error: From; 43 | 44 | /// Terminal shutdown. 45 | fn shutdown(&mut self) -> Result<(), Error> 46 | where 47 | Error: From; 48 | 49 | /// Render the app widget. 50 | /// 51 | /// Creates the render-context, fetches the frame and calls render. 52 | /// Returns the frame number of the rendered frame or any error. 53 | #[allow(clippy::needless_lifetimes)] 54 | #[allow(clippy::type_complexity)] 55 | fn render( 56 | &mut self, 57 | f: &mut dyn FnMut(&mut Frame<'_>) -> Result, 58 | ) -> Result 59 | where 60 | Error: From; 61 | } 62 | 63 | /// Default RenderUI for crossterm. 64 | #[derive(Debug)] 65 | pub struct CrosstermTerminal { 66 | term: ratatui::Terminal>, 67 | } 68 | 69 | impl CrosstermTerminal { 70 | pub fn new() -> Result { 71 | Ok(Self { 72 | term: ratatui::Terminal::new(CrosstermBackend::new(stdout()))?, 73 | }) 74 | } 75 | } 76 | 77 | impl Terminal for CrosstermTerminal 78 | where 79 | Error: 'static + Send, 80 | { 81 | fn init(&mut self) -> Result<(), Error> 82 | where 83 | Error: From, 84 | { 85 | stdout().execute(EnterAlternateScreen)?; 86 | stdout().execute(EnableMouseCapture)?; 87 | stdout().execute(EnableBracketedPaste)?; 88 | stdout().execute(EnableBlinking)?; 89 | stdout().execute(SetCursorStyle::BlinkingBar)?; 90 | enable_raw_mode()?; 91 | #[cfg(not(windows))] 92 | { 93 | stdout().execute(PushKeyboardEnhancementFlags( 94 | KeyboardEnhancementFlags::REPORT_EVENT_TYPES 95 | | KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES 96 | | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS 97 | | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES, 98 | ))?; 99 | 100 | let enhanced = supports_keyboard_enhancement().unwrap_or_default(); 101 | set_have_keyboard_enhancement(enhanced); 102 | } 103 | #[cfg(windows)] 104 | { 105 | set_have_keyboard_enhancement(true); 106 | } 107 | 108 | self.term.clear()?; 109 | 110 | Ok(()) 111 | } 112 | 113 | fn shutdown(&mut self) -> Result<(), Error> 114 | where 115 | Error: From, 116 | { 117 | #[cfg(not(windows))] 118 | stdout().execute(PopKeyboardEnhancementFlags)?; 119 | disable_raw_mode()?; 120 | stdout().execute(SetCursorStyle::DefaultUserShape)?; 121 | stdout().execute(DisableBlinking)?; 122 | stdout().execute(DisableBracketedPaste)?; 123 | stdout().execute(DisableMouseCapture)?; 124 | stdout().execute(LeaveAlternateScreen)?; 125 | Ok(()) 126 | } 127 | 128 | #[allow(clippy::needless_lifetimes)] 129 | fn render( 130 | &mut self, 131 | f: &mut dyn FnMut(&mut Frame<'_>) -> Result, 132 | ) -> Result 133 | where 134 | Error: From, 135 | { 136 | let mut res = Ok(0); 137 | _ = self.term.hide_cursor(); 138 | self.term.draw(|frame| res = f(frame))?; 139 | res 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/threadpool.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! The thread-pool for the background tasks. 3 | //! 4 | 5 | use crate::Control; 6 | use crossbeam::channel::{bounded, unbounded, Receiver, SendError, Sender, TryRecvError}; 7 | use log::warn; 8 | use std::sync::atomic::{AtomicBool, Ordering}; 9 | use std::sync::Arc; 10 | use std::thread::JoinHandle; 11 | use std::{mem, thread}; 12 | 13 | /// Type for a background task. 14 | type BoxTask = Box< 15 | dyn FnOnce(Cancel, &Sender, Error>>) -> Result, Error> 16 | + Send, 17 | >; 18 | 19 | /// Cancel background tasks. 20 | #[derive(Debug, Default, Clone)] 21 | pub struct Cancel(Arc); 22 | 23 | impl Cancel { 24 | pub fn new() -> Self { 25 | Self(Arc::new(AtomicBool::new(false))) 26 | } 27 | 28 | pub fn is_canceled(&self) -> bool { 29 | self.0.load(Ordering::Acquire) 30 | } 31 | 32 | pub fn cancel(&self) { 33 | self.0.store(true, Ordering::Release); 34 | } 35 | } 36 | 37 | /// Basic thread-pool. 38 | /// 39 | /// 40 | /// 41 | #[derive(Debug)] 42 | pub(crate) struct ThreadPool 43 | where 44 | Event: 'static + Send, 45 | Error: 'static + Send, 46 | { 47 | send: Sender<(Cancel, BoxTask)>, 48 | recv: Receiver, Error>>, 49 | handles: Vec>, 50 | } 51 | 52 | impl ThreadPool 53 | where 54 | Event: 'static + Send, 55 | Error: 'static + Send, 56 | { 57 | /// New thread-pool with the given task executor. 58 | pub(crate) fn new(n_worker: usize) -> Self { 59 | let (send, t_recv) = unbounded::<(Cancel, BoxTask)>(); 60 | let (t_send, recv) = unbounded::, Error>>(); 61 | 62 | let mut handles = Vec::new(); 63 | 64 | for _ in 0..n_worker { 65 | let t_recv = t_recv.clone(); 66 | let t_send = t_send.clone(); 67 | 68 | let handle = thread::spawn(move || { 69 | let t_recv = t_recv; 70 | 71 | 'l: loop { 72 | match t_recv.recv() { 73 | Ok((cancel, task)) => { 74 | let flow = task(cancel, &t_send); 75 | if let Err(err) = t_send.send(flow) { 76 | warn!("{:?}", err); 77 | break 'l; 78 | } 79 | } 80 | Err(err) => { 81 | warn!("{:?}", err); 82 | break 'l; 83 | } 84 | } 85 | } 86 | }); 87 | 88 | handles.push(handle); 89 | } 90 | 91 | Self { 92 | send, 93 | recv, 94 | handles, 95 | } 96 | } 97 | 98 | /// Start a background task. 99 | /// 100 | /// The background task gets a `Arc>` for cancellation support, 101 | /// and a channel to communicate back to the event loop. 102 | /// 103 | /// A clone of the `Arc>` is returned by this function. 104 | /// 105 | /// If you need more, create an extra channel for communication to the background task. 106 | #[inline] 107 | pub(crate) fn send(&self, task: BoxTask) -> Result> { 108 | if self.handles.is_empty() { 109 | return Err(SendError(())); 110 | } 111 | 112 | let cancel = Cancel::new(); 113 | match self.send.send((cancel.clone(), task)) { 114 | Ok(_) => Ok(cancel), 115 | Err(_) => Err(SendError(())), 116 | } 117 | } 118 | 119 | /// Check the workers for liveness. 120 | pub(crate) fn check_liveness(&self) -> bool { 121 | for h in &self.handles { 122 | if h.is_finished() { 123 | return false; 124 | } 125 | } 126 | true 127 | } 128 | 129 | /// Is the receive-channel empty? 130 | #[inline] 131 | pub(crate) fn is_empty(&self) -> bool { 132 | self.recv.is_empty() 133 | } 134 | 135 | /// Receive a result. 136 | pub(crate) fn try_recv(&self) -> Result, Error> 137 | where 138 | Error: From, 139 | { 140 | match self.recv.try_recv() { 141 | Ok(v) => v, 142 | Err(TryRecvError::Empty) => Ok(Control::Continue), 143 | Err(e) => Err(e.into()), 144 | } 145 | } 146 | } 147 | 148 | impl Drop for ThreadPool 149 | where 150 | Event: 'static + Send, 151 | Error: 'static + Send, 152 | { 153 | fn drop(&mut self) { 154 | // dropping the channel will be noticed by the threads running the 155 | // background tasks. 156 | drop(mem::replace(&mut self.send, bounded(0).0)); 157 | for h in self.handles.drain(..) { 158 | _ = h.join(); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/timer.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Support for timers. 3 | //! 4 | 5 | use std::cell::{Cell, RefCell}; 6 | use std::time::{Duration, Instant}; 7 | 8 | /// Holds all the timers. 9 | #[derive(Debug, Default)] 10 | pub(crate) struct Timers { 11 | tags: Cell, 12 | timers: RefCell>, 13 | } 14 | 15 | /// Handle for a submitted timer. 16 | #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] 17 | pub struct TimerHandle(usize); 18 | 19 | #[derive(Debug)] 20 | struct TimerImpl { 21 | tag: usize, 22 | count: usize, 23 | repeat: Option, 24 | next: Instant, 25 | timer: Duration, 26 | } 27 | 28 | impl Timers { 29 | /// Returns the next sleep time. 30 | pub(crate) fn sleep_time(&self) -> Option { 31 | let timers = self.timers.borrow(); 32 | if let Some(timer) = timers.last() { 33 | let now = Instant::now(); 34 | if now > timer.next { 35 | Some(Duration::from_nanos(0)) 36 | } else { 37 | Some(timer.next.duration_since(now)) 38 | } 39 | } else { 40 | None 41 | } 42 | } 43 | 44 | /// Polls for the next timer event. 45 | pub(crate) fn poll(&self) -> bool { 46 | let timers = self.timers.borrow(); 47 | if let Some(timer) = timers.last() { 48 | Instant::now() >= timer.next 49 | } else { 50 | false 51 | } 52 | } 53 | 54 | /// Polls for the next timer event. 55 | /// Removes/recalculates the event and reorders the queue. 56 | pub(crate) fn read(&self) -> Option { 57 | let mut timers = self.timers.borrow_mut(); 58 | 59 | let timer = timers.pop(); 60 | if let Some(mut timer) = timer { 61 | if Instant::now() >= timer.next { 62 | let evt = TimerEvent(TimeOut { 63 | handle: TimerHandle(timer.tag), 64 | counter: timer.count, 65 | }); 66 | 67 | // reschedule 68 | if let Some(repeat) = timer.repeat { 69 | timer.count += 1; 70 | if timer.count < repeat { 71 | timer.next += timer.timer; 72 | Self::add_impl(timers.as_mut(), timer); 73 | } 74 | } 75 | 76 | Some(evt) 77 | } else { 78 | timers.push(timer); 79 | None 80 | } 81 | } else { 82 | None 83 | } 84 | } 85 | 86 | fn add_impl(timers: &mut Vec, t: TimerImpl) { 87 | 'f: { 88 | for i in 0..timers.len() { 89 | if timers[i].next <= t.next { 90 | timers.insert(i, t); 91 | break 'f; 92 | } 93 | } 94 | timers.push(t); 95 | } 96 | } 97 | 98 | /// Add a timer. 99 | #[must_use] 100 | pub(crate) fn add(&self, t: TimerDef) -> TimerHandle { 101 | let tag = self.tags.get() + 1; 102 | self.tags.set(tag); 103 | 104 | let t = TimerImpl { 105 | tag, 106 | count: 0, 107 | repeat: t.repeat, 108 | next: if let Some(next) = t.next { 109 | next 110 | } else { 111 | Instant::now() + t.timer 112 | }, 113 | timer: t.timer, 114 | }; 115 | 116 | let mut timers = self.timers.borrow_mut(); 117 | Self::add_impl(timers.as_mut(), t); 118 | 119 | TimerHandle(tag) 120 | } 121 | 122 | /// Remove a timer. 123 | pub(crate) fn remove(&self, tag: TimerHandle) { 124 | let mut timer = self.timers.borrow_mut(); 125 | for i in 0..timer.len() { 126 | if timer[i].tag == tag.0 { 127 | timer.remove(i); 128 | break; 129 | } 130 | } 131 | } 132 | } 133 | 134 | /// Timing event data. Used by [TimerEvent]. 135 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 136 | pub struct TimeOut { 137 | pub handle: TimerHandle, 138 | pub counter: usize, 139 | } 140 | 141 | /// Timer event. 142 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 143 | pub struct TimerEvent(pub TimeOut); 144 | 145 | /// Holds the information to start a timer. 146 | #[derive(Debug, Default)] 147 | pub struct TimerDef { 148 | /// Optional repeat. 149 | repeat: Option, 150 | /// Duration 151 | timer: Duration, 152 | /// Specific time. 153 | next: Option, 154 | } 155 | 156 | impl TimerDef { 157 | pub fn new() -> Self { 158 | Default::default() 159 | } 160 | 161 | /// Repeat forever. 162 | pub fn repeat_forever(mut self) -> Self { 163 | self.repeat = Some(usize::MAX); 164 | self 165 | } 166 | 167 | /// Repeat count. 168 | pub fn repeat(mut self, repeat: usize) -> Self { 169 | self.repeat = Some(repeat); 170 | self 171 | } 172 | 173 | /// Timer interval. 174 | pub fn timer(mut self, timer: Duration) -> Self { 175 | self.timer = timer; 176 | self 177 | } 178 | 179 | /// Next time the timer is due. Can set a start delay for a repeating timer, 180 | /// or as an oneshot event for a given instant. 181 | pub fn next(mut self, next: Instant) -> Self { 182 | self.next = Some(next); 183 | self 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/tokio_tasks.rs: -------------------------------------------------------------------------------- 1 | use crate::poll::PollEvents; 2 | use crate::{AppContext, AppState, Control}; 3 | use log::error; 4 | use std::any::Any; 5 | use std::cell::RefCell; 6 | use std::future::Future; 7 | use std::rc::Rc; 8 | use tokio::runtime::Runtime; 9 | use tokio::sync::mpsc::{channel, Receiver, Sender}; 10 | use tokio::task::{AbortHandle, JoinHandle}; 11 | 12 | /// Add PollTokio to the configuration to enable spawning 13 | /// async operations from the application. 14 | /// 15 | /// You cannot work with `tokio-main` but need to initialize 16 | /// the runtime manually. 17 | /// 18 | /// 19 | /// 20 | #[derive(Debug)] 21 | pub struct PollTokio 22 | where 23 | Event: 'static + Send, 24 | Error: 'static + Send, 25 | { 26 | rt: Rc>, 27 | pending: Rc, Error>>>>>, 28 | send_queue: Sender, Error>>, 29 | recv_queue: Receiver, Error>>, 30 | } 31 | 32 | #[derive(Debug)] 33 | pub(crate) struct TokioSpawn 34 | where 35 | Event: 'static + Send, 36 | Error: 'static + Send, 37 | { 38 | rt: Rc>, 39 | pending: Rc, Error>>>>>, 40 | send_queue: Sender, Error>>, 41 | } 42 | 43 | impl TokioSpawn 44 | where 45 | Event: 'static + Send, 46 | Error: 'static + Send, 47 | { 48 | pub(crate) fn spawn( 49 | &self, 50 | future: Box, Error>> + Send>, 51 | ) -> AbortHandle { 52 | let h = self.rt.borrow().spawn(Box::into_pin(future)); 53 | let ah = h.abort_handle(); 54 | self.pending.borrow_mut().push(h); 55 | ah 56 | } 57 | 58 | pub(crate) fn sender(&self) -> Sender, Error>> { 59 | self.send_queue.clone() 60 | } 61 | } 62 | 63 | impl PollTokio 64 | where 65 | Event: 'static + Send, 66 | Error: 'static + Send, 67 | { 68 | pub fn new(rt: Runtime) -> Self { 69 | let (send, recv) = channel(100); 70 | Self { 71 | rt: Rc::new(RefCell::new(rt)), 72 | pending: Default::default(), 73 | send_queue: send, 74 | recv_queue: recv, 75 | } 76 | } 77 | 78 | pub(crate) fn get_spawn(&self) -> TokioSpawn { 79 | TokioSpawn { 80 | rt: self.rt.clone(), 81 | pending: self.pending.clone(), 82 | send_queue: self.send_queue.clone(), 83 | } 84 | } 85 | } 86 | 87 | impl PollEvents 88 | for PollTokio 89 | where 90 | State: AppState + ?Sized, 91 | Event: 'static + Send, 92 | Error: 'static + Send, 93 | { 94 | fn as_any(&self) -> &dyn Any { 95 | self 96 | } 97 | 98 | fn poll(&mut self, _ctx: &mut AppContext<'_, Global, Event, Error>) -> Result { 99 | let mut pending = Vec::new(); 100 | 101 | for v in self.pending.borrow_mut().drain(..) { 102 | if v.is_finished() { 103 | let r = self.rt.borrow().block_on(v); 104 | match r { 105 | Ok(r) => { 106 | if let Err(e) = self.send_queue.blocking_send(r) { 107 | error!("{:?}", e); 108 | } 109 | } 110 | Err(e) => error!("{:?}", e), 111 | } 112 | } else { 113 | pending.push(v); 114 | } 115 | } 116 | self.pending.replace(pending); 117 | 118 | Ok(!self.recv_queue.is_empty()) 119 | } 120 | 121 | fn read_exec( 122 | &mut self, 123 | _state: &mut State, 124 | _ctx: &mut AppContext<'_, Global, Event, Error>, 125 | ) -> Result, Error> { 126 | self.recv_queue 127 | .blocking_recv() 128 | .unwrap_or_else(|| Ok(Control::Continue)) 129 | } 130 | } 131 | --------------------------------------------------------------------------------