diff --git a/CHANGELOG.md b/CHANGELOG.md index defed0ed2..56ee9303e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ - [r3bl_macro](#r3bl_macro) - [next-release-macro](#next-release-macro) - [r3bl_test_fixtures](#r3bl_test_fixtures) + - [next-release-test-fixtures](#next-release-test-fixtures) - [v0.0.3 2024-09-12](#v003-2024-09-12) - [v0.0.2 2024-07-13](#v002-2024-07-13) - [v0.0.1 2024-07-12](#v001-2024-07-12) @@ -387,6 +388,17 @@ reflect how the functionality is used in the real world. of this functionality have emerged, including crates that are created by "3rd party developers" (people not R3BL and not part of `r3bl-open-core` repo). +- Added: + - Provide a totally new interface for the `main_event_loop()` that allows for more + flexibility in how the event loop is run, using dependency injection. This is a + breaking change, but it is needed to make the codebase more maintainable and possible + to test end to end. This new change introduces the concept of providing some + dependencies to the function itself in order to use it: state, input device, output + device, and app. The function now returns these dependencies as well, so that they can + be used to create a running pipeline of small "applets" where all of these + dependencies are passed around, allowing a new generation of experiences to be built, + that are not monolithic, but are composable and testable. + ### v0.5.9 (2024-09-12) - Updated: @@ -732,6 +744,12 @@ in the real world. [`nazmulidris/rust-scratch/tcp-api-server`](https://github.com/nazmulidris/rust-scratch/) repo. This allows customization of the miette global report handler at the process level. Useful for apps that need to override the default report handler formatting. + - Add `OutputDevice` that abstracts away the output device (eg: `stdout`, `stderr`, + `SharedWriter`, etc.). This is useful for end to end testing, and adapting to a variety of + different input and output devices (in the future). + - Add `InputDevice` that abstracts away the input device (eg: `stdin`). This is useful + for end to end testing. This is useful for end to end testing, and adapting to a + variety of different input and output devices (in the future). ## `r3bl_analytics_schema` @@ -776,6 +794,16 @@ Deleted: ## `r3bl_test_fixtures` +### next-release-test-fixtures + +- Changed: + - Some type aliases were defined here redundantly, since they were also defined in + `r3bl_core` crate. Remove these duplicate types and add a dependency to `r3bl_core` + crate. + +- Added: + - Add constructor and tests for `OutputDevice` struct for `StdoutMock`. + ### v0.0.3 (2024-09-12) - Updated: diff --git a/Cargo.lock b/Cargo.lock index 01aea734b..2626e2dde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1824,6 +1824,7 @@ dependencies = [ "futures-util", "miette", "pretty_assertions", + "r3bl_core", "strip-ansi-escapes", "strum", "strum_macros", @@ -1851,6 +1852,7 @@ dependencies = [ "r3bl_core", "r3bl_macro", "r3bl_terminal_async", + "r3bl_test_fixtures", "rand", "serde", "serde_json", diff --git a/cmdr/src/edi/launcher.rs b/cmdr/src/edi/launcher.rs index e1175cf12..9103f16dd 100644 --- a/cmdr/src/edi/launcher.rs +++ b/cmdr/src/edi/launcher.rs @@ -34,6 +34,6 @@ pub async fn run_app(maybe_file_path: Option) -> CommonResult<()> { )]; // Create a window. - TerminalWindow::main_event_loop(app, exit_keys, state).await?; + _ = TerminalWindow::main_event_loop(app, exit_keys, state).await?; }) } diff --git a/core/src/common/mod.rs b/core/src/common/mod.rs index 9fc1646a2..cd73afe8e 100644 --- a/core/src/common/mod.rs +++ b/core/src/common/mod.rs @@ -20,11 +20,9 @@ pub mod common_enums; pub mod common_math; pub mod common_result_and_error; pub mod miette_setup_global_report_handler; -pub mod type_aliases; // Re-export. pub use common_enums::*; pub use common_math::*; pub use common_result_and_error::*; pub use miette_setup_global_report_handler::*; -pub use type_aliases::*; diff --git a/core/src/terminal_io/input_device.rs b/core/src/terminal_io/input_device.rs new file mode 100644 index 000000000..08ee3359e --- /dev/null +++ b/core/src/terminal_io/input_device.rs @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crossterm::event::EventStream; +use futures_util::{FutureExt, StreamExt}; +use miette::IntoDiagnostic; + +use crate::{CrosstermEventResult, PinnedInputStream}; + +pub struct InputDevice { + pub resource: PinnedInputStream, +} + +impl InputDevice { + pub fn new_event_stream() -> InputDevice { + InputDevice { + resource: Box::pin(EventStream::new()), + } + } +} + +impl InputDevice { + pub async fn next(&mut self) -> miette::Result { + match self.resource.next().fuse().await { + Some(it) => it.into_diagnostic(), + None => miette::bail!("Failed to get next event from input source."), + } + } +} diff --git a/core/src/terminal_io/mod.rs b/core/src/terminal_io/mod.rs index 19a096ea1..e72b0d05b 100644 --- a/core/src/terminal_io/mod.rs +++ b/core/src/terminal_io/mod.rs @@ -16,9 +16,15 @@ */ // Attach sources. +pub mod input_device; +pub mod output_device; pub mod pretty_print; pub mod shared_writer; +pub mod type_aliases; // Re-export. +pub use input_device::*; +pub use output_device::*; pub use pretty_print::*; pub use shared_writer::*; +pub use type_aliases::*; diff --git a/core/src/terminal_io/output_device.rs b/core/src/terminal_io/output_device.rs new file mode 100644 index 000000000..037e1a987 --- /dev/null +++ b/core/src/terminal_io/output_device.rs @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::sync::Arc; + +use crate::{SafeRawTerminal, SendRawTerminal, StdMutex}; + +pub type LockedOutputDevice<'a> = &'a mut dyn std::io::Write; + +/// Macro to simplify locking and getting a mutable reference to the output device. +/// +/// Usage example: +/// ```rust +/// use r3bl_core::{output_device_as_mut, OutputDevice, LockedOutputDevice}; +/// let device = OutputDevice::new_stdout(); +/// let mut_ref: LockedOutputDevice<'_> = output_device_as_mut!(device); +/// let _ = mut_ref.write_all(b"Hello, world!\n"); +/// ``` +#[macro_export] +macro_rules! output_device_as_mut { + ($device:expr) => { + &mut *$device.lock() + }; +} + +/// This struct represents an output device that can be used to write to the terminal. It +/// is safe to clone. In order to write to it, see the examples in [Self::lock()] or +/// [output_device_as_mut] macro. +#[derive(Clone)] +pub struct OutputDevice { + pub resource: SafeRawTerminal, +} + +impl OutputDevice { + pub fn new_stdout() -> Self { + Self { + resource: Arc::new(StdMutex::new(std::io::stdout())), + } + } +} + +impl OutputDevice { + /// Locks the output device for writing. To use it, use the following code: + /// + /// ```rust + /// use r3bl_core::{OutputDevice, LockedOutputDevice}; + /// + /// let device = OutputDevice::new_stdout(); + /// let mut_ref: LockedOutputDevice<'_> = &mut *device.lock(); + /// let _ = mut_ref.write_all(b"Hello, world!\n"); + /// ``` + /// + /// This method returns a [`std::sync::MutexGuard`] which provides a mechanism to + /// access the underlying resource in a thread-safe manner. The `MutexGuard` ensures + /// that the resource is locked for the duration of the guard's lifetime, preventing + /// other threads from accessing it simultaneously. + pub fn lock(&self) -> std::sync::MutexGuard<'_, SendRawTerminal> { + self.resource.lock().unwrap() + } +} diff --git a/core/src/common/type_aliases.rs b/core/src/terminal_io/type_aliases.rs similarity index 100% rename from core/src/common/type_aliases.rs rename to core/src/terminal_io/type_aliases.rs diff --git a/run b/run index 0d8efbf61..c4b350a6b 100755 --- a/run +++ b/run @@ -50,6 +50,7 @@ def main [...args: string] { "clean" => {clean} "install-cargo-tools" => {install-cargo-tools} "test" => {test} + "watch-all-tests" => {watch-all-tests} "docs" => {docs} "check" => {check} "check-watch" => {check-watch} @@ -82,6 +83,7 @@ def print-help [command: string] { print $' (ansi green)clean(ansi reset)' print $' (ansi green)install-cargo-tools(ansi reset)' print $' (ansi green)test(ansi reset)' + print $' (ansi green)watch-all-tests(ansi reset)' print $' (ansi green)docs(ansi reset)' print $' (ansi green)check(ansi reset)' print $' (ansi green)check-watch(ansi reset)' @@ -100,6 +102,12 @@ def print-help [command: string] { } } +def watch-all-tests [] { + cargo watch -x 'test --workspace --quiet --color always -- --test-threads 4' -c -q --delay 2 + # cargo watch -x 'test --workspace' -c -q --delay 2 + # cargo watch --exec check --exec 'test --quiet --color always -- --test-threads 4' --clear --quiet --delay 2 +} + # https://thelinuxcode.com/create-ramdisk-linux/ def ramdisk-create [] { # Use findmnt -t tmpfs target_foo to check if the ramdisk is mounted. diff --git a/terminal_async/src/readline_impl/readline.rs b/terminal_async/src/readline_impl/readline.rs index c86c67319..0413e324c 100644 --- a/terminal_async/src/readline_impl/readline.rs +++ b/terminal_async/src/readline_impl/readline.rs @@ -444,7 +444,8 @@ impl Readline { pub fn new( prompt: String, safe_raw_terminal: SafeRawTerminal, - /* move */ pinned_input_stream: PinnedInputStream, + /* move */ + pinned_input_stream: PinnedInputStream, ) -> Result<(Self, SharedWriter), ReadlineError> { // Line control channel - signals are send to this channel to control `LineState`. // A task is spawned to monitor this channel. diff --git a/test_fixtures/Cargo.toml b/test_fixtures/Cargo.toml index f8d8f29f3..725a8bed9 100644 --- a/test_fixtures/Cargo.toml +++ b/test_fixtures/Cargo.toml @@ -19,6 +19,8 @@ homepage = "https://r3bl.com" license = "Apache-2.0" [dependencies] +r3bl_core = { path = "../core", version = "0.9.16" } + # Async stream for DI and testing. futures-core = "0.3.30" async-stream = "0.3.5" diff --git a/test_fixtures/src/async_stream_fixtures.rs b/test_fixtures/src/async_stream_fixtures.rs index d53f83cf9..87a981a40 100644 --- a/test_fixtures/src/async_stream_fixtures.rs +++ b/test_fixtures/src/async_stream_fixtures.rs @@ -18,8 +18,7 @@ use std::time::Duration; use async_stream::stream; - -use super::PinnedInputStream; +use r3bl_core::PinnedInputStream; pub fn gen_input_stream(generator_vec: Vec) -> PinnedInputStream where diff --git a/test_fixtures/src/lib.rs b/test_fixtures/src/lib.rs index 038a02f17..79ed94278 100644 --- a/test_fixtures/src/lib.rs +++ b/test_fixtures/src/lib.rs @@ -193,16 +193,9 @@ //! } //! ``` -use std::pin::Pin; - -use futures_core::Stream; - -// Type aliases. -pub type StdMutex = std::sync::Mutex; -pub type PinnedInputStream = Pin>>; - // Attach sources. pub mod async_stream_fixtures; +pub mod output_device_fixtures; pub mod stdout_fixtures; // Re-export. diff --git a/test_fixtures/src/output_device_fixtures.rs b/test_fixtures/src/output_device_fixtures.rs new file mode 100644 index 000000000..ea594d6b8 --- /dev/null +++ b/test_fixtures/src/output_device_fixtures.rs @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::sync::Arc; + +use r3bl_core::{OutputDevice, StdMutex}; + +use crate::StdoutMock; + +impl StdoutMock { + pub fn new_output_device() -> (OutputDevice, StdoutMock) { + let stdout_mock = StdoutMock::default(); + let this = OutputDevice { + resource: Arc::new(StdMutex::new(stdout_mock.clone())), + }; + (this, stdout_mock) + } +} + +#[cfg(test)] +mod tests { + use r3bl_core::{output_device_as_mut, LockedOutputDevice}; + + use crate::StdoutMock; + + #[test] + fn test_output_device() { + let (device, mock) = StdoutMock::new_output_device(); + let mut_ref: LockedOutputDevice<'_> = output_device_as_mut!(device); + let _ = mut_ref.write_all(b"Hello, world!\n"); + assert_eq!( + mock.get_copy_of_buffer_as_string_strip_ansi(), + "Hello, world!\n" + ); + } +} diff --git a/test_fixtures/src/stdout_fixtures.rs b/test_fixtures/src/stdout_fixtures.rs index d8ee2f127..bec482d43 100644 --- a/test_fixtures/src/stdout_fixtures.rs +++ b/test_fixtures/src/stdout_fixtures.rs @@ -18,10 +18,9 @@ use std::{io::{Result, Write}, sync::Arc}; +use r3bl_core::StdMutex; use strip_ansi_escapes::strip; -use super::StdMutex; - /// You can safely clone this struct, since it only contains an `Arc>>`. The /// inner `buffer` will not be cloned, just the [Arc] will be cloned. #[derive(Clone)] diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 18adb9e51..81f4c972e 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -86,6 +86,7 @@ pretty_assertions = "1.4.0" # - This is not a dependency for the library, and is not used when the library is # published or used as a dependency. r3bl_terminal_async = { path = "../terminal_async" } +r3bl_test_fixtures = { path = "../test_fixtures" } # For assert_eq2! macro. pretty_assertions = "1.4.0" diff --git a/tui/examples/demo/ex_app_no_layout/launcher.rs b/tui/examples/demo/ex_app_no_layout/launcher.rs index 80e663282..3d9649865 100644 --- a/tui/examples/demo/ex_app_no_layout/launcher.rs +++ b/tui/examples/demo/ex_app_no_layout/launcher.rs @@ -28,6 +28,6 @@ pub async fn run_app() -> CommonResult<()> { vec![InputEvent::Keyboard(keypress! { @char 'x' })]; // Create a window. - TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? + _ = TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? }); } diff --git a/tui/examples/demo/ex_app_with_1col_layout/launcher.rs b/tui/examples/demo/ex_app_with_1col_layout/launcher.rs index ce25eed06..011188ccb 100644 --- a/tui/examples/demo/ex_app_with_1col_layout/launcher.rs +++ b/tui/examples/demo/ex_app_with_1col_layout/launcher.rs @@ -30,6 +30,6 @@ pub async fn run_app() -> CommonResult<()> { vec![InputEvent::Keyboard(keypress! { @char 'x' })]; // Create a window. - TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? + _ = TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? }); } diff --git a/tui/examples/demo/ex_app_with_2col_layout/launcher.rs b/tui/examples/demo/ex_app_with_2col_layout/launcher.rs index ce25eed06..011188ccb 100644 --- a/tui/examples/demo/ex_app_with_2col_layout/launcher.rs +++ b/tui/examples/demo/ex_app_with_2col_layout/launcher.rs @@ -30,6 +30,6 @@ pub async fn run_app() -> CommonResult<()> { vec![InputEvent::Keyboard(keypress! { @char 'x' })]; // Create a window. - TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? + _ = TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? }); } diff --git a/tui/examples/demo/ex_editor/launcher.rs b/tui/examples/demo/ex_editor/launcher.rs index 417e022f9..cd52820d1 100644 --- a/tui/examples/demo/ex_editor/launcher.rs +++ b/tui/examples/demo/ex_editor/launcher.rs @@ -31,6 +31,6 @@ pub async fn run_app() -> CommonResult<()> { )]; // Create a window. - TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? + _ = TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? }); } diff --git a/tui/examples/demo/ex_pitch/launcher.rs b/tui/examples/demo/ex_pitch/launcher.rs index ec3f58f41..11e8e37cb 100644 --- a/tui/examples/demo/ex_pitch/launcher.rs +++ b/tui/examples/demo/ex_pitch/launcher.rs @@ -31,6 +31,6 @@ pub async fn run_app() -> CommonResult<()> { )]; // Create a window. - TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? + _ = TerminalWindow::main_event_loop(app, exit_keys, State::default()).await? }); } diff --git a/tui/examples/demo/ex_rc/launcher.rs b/tui/examples/demo/ex_rc/launcher.rs index 2596c1dbc..bd7be6f1a 100644 --- a/tui/examples/demo/ex_rc/launcher.rs +++ b/tui/examples/demo/ex_rc/launcher.rs @@ -31,6 +31,6 @@ pub async fn run_app() -> CommonResult<()> { )]; // Create a window. - TerminalWindow::main_event_loop(app, exit_keys, State::default()).await?; + _ = TerminalWindow::main_event_loop(app, exit_keys, State::default()).await?; }); } diff --git a/tui/src/tui/dialog/dialog_engine/dialog_engine_api.rs b/tui/src/tui/dialog/dialog_engine/dialog_engine_api.rs index 5d5fd0ac3..ba7a3b7a6 100644 --- a/tui/src/tui/dialog/dialog_engine/dialog_engine_api.rs +++ b/tui/src/tui/dialog/dialog_engine/dialog_engine_api.rs @@ -892,7 +892,7 @@ mod test_dialog_engine_api_render_engine { let window_size = size!( col_count: 70, row_count: 15 ); let dialog_engine = &mut mock_real_objects_for_dialog::make_dialog_engine(); let global_data = &mut { - let mut it = make_global_data(Some(window_size)); + let (mut it, _) = make_global_data(Some(window_size)); it.state.dialog_buffers.clear(); it }; @@ -911,7 +911,10 @@ mod test_dialog_engine_api_render_engine { let self_id: FlexBoxId = FlexBoxId::from(0); let window_size = size!( col_count: 70, row_count: 15 ); let dialog_engine = &mut mock_real_objects_for_dialog::make_dialog_engine(); - let global_data = &mut make_global_data(Some(window_size)); + let global_data = &mut { + let (it, _) = make_global_data(Some(window_size)); + it + }; let has_focus = &mut HasFocus::default(); let args = DialogEngineArgs { self_id, diff --git a/tui/src/tui/dialog/test_dialog.rs b/tui/src/tui/dialog/test_dialog.rs index 420b6b9be..493b321c3 100644 --- a/tui/src/tui/dialog/test_dialog.rs +++ b/tui/src/tui/dialog/test_dialog.rs @@ -15,10 +15,12 @@ * limitations under the License. */ +#[cfg(test)] pub mod mock_real_objects_for_dialog { use std::{collections::HashMap, fmt::Debug}; use r3bl_core::Size; + use r3bl_test_fixtures::StdoutMock; use tokio::sync::mpsc; use crate::{test_fixtures::mock_real_objects_for_editor, @@ -29,17 +31,24 @@ pub mod mock_real_objects_for_dialog { HasDialogBuffers, CHANNEL_WIDTH}; - pub fn make_global_data(window_size: Option) -> GlobalData { + pub fn make_global_data( + window_size: Option, + ) -> (GlobalData, StdoutMock) { let (main_thread_channel_sender, _) = mpsc::channel::<_>(CHANNEL_WIDTH); let state = create_state(); let window_size = window_size.unwrap_or_default(); let maybe_saved_offscreen_buffer = Default::default(); - GlobalData { + let (output_device, stdout_mock) = StdoutMock::new_output_device(); + + let global_data = GlobalData { state, window_size, maybe_saved_offscreen_buffer, main_thread_channel_sender, - } + output_device, + }; + + (global_data, stdout_mock) } #[derive(Clone, PartialEq, Default, Debug)] diff --git a/tui/src/tui/editor/editor_buffer/system_clipboard_service_provider.rs b/tui/src/tui/editor/editor_buffer/system_clipboard_service_provider.rs index 1e2b94355..d6d0362b8 100644 --- a/tui/src/tui/editor/editor_buffer/system_clipboard_service_provider.rs +++ b/tui/src/tui/editor/editor_buffer/system_clipboard_service_provider.rs @@ -36,7 +36,7 @@ impl ClipboardService for SystemClipboard { call_if_true!(DEBUG_TUI_COPY_PASTE, { tracing::debug!( "\nšŸ“‹šŸ“‹šŸ“‹ Selected Text was copied to clipboard: \n{}", - format!("{content}").black().on_green(), + content.to_string().black().on_green(), ); }); }) diff --git a/tui/src/tui/editor/test_fixtures.rs b/tui/src/tui/editor/test_fixtures.rs index 8e9709add..6411a19c4 100644 --- a/tui/src/tui/editor/test_fixtures.rs +++ b/tui/src/tui/editor/test_fixtures.rs @@ -15,27 +15,35 @@ * limitations under the License. */ +#[cfg(test)] pub mod mock_real_objects_for_editor { use std::fmt::Debug; use r3bl_core::{position, size, Size}; + use r3bl_test_fixtures::StdoutMock; use tokio::sync::mpsc; use crate::{EditorEngine, FlexBox, GlobalData, PartialFlexBox, CHANNEL_WIDTH}; - pub fn make_global_data(window_size: Option) -> GlobalData + pub fn make_global_data( + window_size: Option, + ) -> (GlobalData, StdoutMock) where S: Debug + Default + Clone + Sync + Send, AS: Debug + Default + Clone + Sync + Send, { let (sender, _) = mpsc::channel::<_>(CHANNEL_WIDTH); + let (output_device, stdout_mock) = StdoutMock::new_output_device(); - GlobalData { + let global_data = GlobalData { window_size: window_size.unwrap_or_default(), maybe_saved_offscreen_buffer: Default::default(), main_thread_channel_sender: sender, state: Default::default(), - } + output_device, + }; + + (global_data, stdout_mock) } pub fn make_editor_engine_with_bounds(size: Size) -> EditorEngine { diff --git a/tui/src/tui/md_parser/convert_to_plain_text.rs b/tui/src/tui/md_parser/convert_to_plain_text.rs index 577b1f3ee..77a49106f 100644 --- a/tui/src/tui/md_parser/convert_to_plain_text.rs +++ b/tui/src/tui/md_parser/convert_to_plain_text.rs @@ -44,7 +44,7 @@ use crate::{constants::{BACK_TICK, MdDocument, MdLineFragment}; -impl<'a> PrettyPrintDebug for MdDocument<'a> { +impl PrettyPrintDebug for MdDocument<'_> { fn pretty_print_debug(&self) -> String { let mut it = vec![]; for (index, block) in self.iter().enumerate() { @@ -54,7 +54,7 @@ impl<'a> PrettyPrintDebug for MdDocument<'a> { } } -impl<'a> PrettyPrintDebug for List> { +impl PrettyPrintDebug for List> { fn pretty_print_debug(&self) -> String { self.inner .iter() @@ -64,7 +64,7 @@ impl<'a> PrettyPrintDebug for List> { } } -impl<'a> PrettyPrintDebug for MdBlock<'a> { +impl PrettyPrintDebug for MdBlock<'_> { fn pretty_print_debug(&self) -> String { match self { MdBlock::Heading(heading_data) => { diff --git a/tui/src/tui/mod.rs b/tui/src/tui/mod.rs index fc48b96e1..e60d78c5e 100644 --- a/tui/src/tui/mod.rs +++ b/tui/src/tui/mod.rs @@ -44,7 +44,7 @@ pub const DEBUG_TUI_SHOW_PIPELINE: bool = false; pub const DEBUG_TUI_SHOW_PIPELINE_EXPANDED: bool = false; -/// Controls input event debugging [crate::AsyncEventStream], and execution of render ops +/// Controls input event debugging [crate::InputDeviceExt], and execution of render ops /// [crate::exec_render_op!] debugging output. pub const DEBUG_TUI_SHOW_TERMINAL_BACKEND: bool = false; diff --git a/tui/src/tui/terminal_lib_backends/crossterm_backend/mod.rs b/tui/src/tui/terminal_lib_backends/crossterm_backend/mod.rs index c6f10ef72..1485c26b9 100644 --- a/tui/src/tui/terminal_lib_backends/crossterm_backend/mod.rs +++ b/tui/src/tui/terminal_lib_backends/crossterm_backend/mod.rs @@ -18,9 +18,11 @@ // Attach. pub mod debug; pub mod offscreen_buffer_paint_impl; -pub mod render_op_impl; +pub mod paint_impl; +pub mod render_op; // Re-export. pub use debug::*; pub use offscreen_buffer_paint_impl::*; -pub use render_op_impl::*; +pub use paint_impl::*; +pub use render_op::*; diff --git a/tui/src/tui/terminal_lib_backends/crossterm_backend/offscreen_buffer_paint_impl.rs b/tui/src/tui/terminal_lib_backends/crossterm_backend/offscreen_buffer_paint_impl.rs index 9d50ec38e..8fb009c77 100644 --- a/tui/src/tui/terminal_lib_backends/crossterm_backend/offscreen_buffer_paint_impl.rs +++ b/tui/src/tui/terminal_lib_backends/crossterm_backend/offscreen_buffer_paint_impl.rs @@ -19,19 +19,20 @@ use r3bl_core::{call_if_true, ch, position, ChUnit, + LockedOutputDevice, Size, TuiStyle, UnicodeString, SPACER}; use crate::{render_ops, + terminal_lib_backends::render_op::RenderOp, Flush as _, FlushKind, OffscreenBuffer, OffscreenBufferPaint, PixelChar, PixelCharDiffChunks, - RenderOp, RenderOps, DEBUG_TUI_COMPOSITOR, DEBUG_TUI_SHOW_PIPELINE}; @@ -39,19 +40,25 @@ use crate::{render_ops, pub struct OffscreenBufferPaintImplCrossterm; impl OffscreenBufferPaint for OffscreenBufferPaintImplCrossterm { - fn paint(&mut self, render_ops: RenderOps, flush_kind: FlushKind, window_size: Size) { + fn paint( + &mut self, + render_ops: RenderOps, + flush_kind: FlushKind, + window_size: Size, + locked_output_device: LockedOutputDevice<'_>, + ) { let mut skip_flush = false; if let FlushKind::ClearBeforeFlush = flush_kind { - RenderOp::default().clear_before_flush(); + RenderOp::clear_before_flush(locked_output_device); } // Execute each RenderOp. - render_ops.execute_all(&mut skip_flush, window_size); + render_ops.execute_all(&mut skip_flush, window_size, locked_output_device); // Flush everything to the terminal. if !skip_flush { - RenderOp::default().flush() + RenderOp::clear_before_flush(locked_output_device); }; // Debug output. @@ -62,15 +69,20 @@ impl OffscreenBufferPaint for OffscreenBufferPaintImplCrossterm { }); } - fn paint_diff(&mut self, render_ops: RenderOps, window_size: Size) { + fn paint_diff( + &mut self, + render_ops: RenderOps, + window_size: Size, + locked_output_device: LockedOutputDevice<'_>, + ) { let mut skip_flush = false; // Execute each RenderOp. - render_ops.execute_all(&mut skip_flush, window_size); + render_ops.execute_all(&mut skip_flush, window_size, locked_output_device); // Flush everything to the terminal. if !skip_flush { - RenderOp::default().flush() + RenderOp::flush(locked_output_device) }; // Debug output. @@ -358,8 +370,7 @@ mod tests { fn test_render_plain_text() { let my_offscreen_buffer = make_offscreen_buffer_plain_text(); // println!("my_offscreen_buffer: \n{:#?}", my_offscreen_buffer); - let mut paint = OffscreenBufferPaintImplCrossterm {}; - let render_ops = paint.render(&my_offscreen_buffer); + let render_ops = OffscreenBufferPaintImplCrossterm.render(&my_offscreen_buffer); // println!("render_ops: {:#?}", render_ops); // Output: diff --git a/tui/src/tui/terminal_lib_backends/crossterm_backend/paint_impl.rs b/tui/src/tui/terminal_lib_backends/crossterm_backend/paint_impl.rs new file mode 100644 index 000000000..17582b916 --- /dev/null +++ b/tui/src/tui/terminal_lib_backends/crossterm_backend/paint_impl.rs @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::borrow::Cow; + +use crossterm::{self, + style::{Attribute, Print, SetAttribute}, + QueueableCommand}; +use r3bl_core::{LockedOutputDevice, Size, TuiStyle, UnicodeString}; + +use crate::{exec_render_op_2, sanitize_and_save_abs_position, RenderOpsLocalData}; + +#[derive(Debug)] +pub struct PaintArgs<'a> { + pub text: Cow<'a, str>, + pub log_msg: Cow<'a, str>, + pub maybe_style: &'a Option, + pub window_size: Size, +} + +fn style_to_attribute(&style: &TuiStyle) -> Vec { + let mut it = vec![]; + if style.bold { + it.push(Attribute::Bold); + } + if style.italic { + it.push(Attribute::Italic); + } + if style.dim { + it.push(Attribute::Dim); + } + if style.underline { + it.push(Attribute::Underlined); + } + if style.reverse { + it.push(Attribute::Reverse); + } + if style.hidden { + it.push(Attribute::Hidden); + } + if style.strikethrough { + it.push(Attribute::Fraktur); + } + it +} + +/// Use [TuiStyle] to set crossterm [crossterm::style::Attributes] ([docs]( +/// https://docs.rs/crossterm/latest/crossterm/style/index.html#attributes)). +pub fn paint_style_and_text( + paint_args: &mut PaintArgs<'_>, + mut needs_reset: Cow<'_, bool>, + local_data: &mut RenderOpsLocalData, + locked_output_device: LockedOutputDevice<'_>, +) { + let PaintArgs { maybe_style, .. } = paint_args; + + if let Some(style) = maybe_style { + let attrib_vec = style_to_attribute(style); + attrib_vec.iter().for_each(|attr| { + exec_render_op_2!( + locked_output_device, + SetAttribute(*attr), + format!("PaintWithAttributes -> SetAttribute({attr:?})") + ); + needs_reset = Cow::Owned(true); + }); + } + + paint_text(paint_args, local_data, locked_output_device); + + if *needs_reset { + exec_render_op_2!( + locked_output_device, + SetAttribute(Attribute::Reset), + format!("PaintWithAttributes -> SetAttribute(Reset))") + ); + } +} + +pub fn paint_text( + paint_args: &PaintArgs<'_>, + local_data: &mut RenderOpsLocalData, + locked_output_device: LockedOutputDevice<'_>, +) { + let PaintArgs { + text, + log_msg, + window_size, + .. + } = paint_args; + + let unicode_string: UnicodeString = text.as_ref().into(); + let mut cursor_position_copy = local_data.cursor_position; + + // Actually paint text. + { + let text = Cow::Borrowed(text); + let log_msg: &str = log_msg; + exec_render_op_2!( + locked_output_device, + Print(&text), + format!("Print( {} {log_msg})", &text) + ); + }; + + // Update cursor position after paint. + let display_width = unicode_string.display_width; + + cursor_position_copy.col_index += display_width; + sanitize_and_save_abs_position(cursor_position_copy, *window_size, local_data); +} diff --git a/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/flush.rs b/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/flush.rs new file mode 100644 index 000000000..d3bc58195 --- /dev/null +++ b/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/flush.rs @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crossterm::{self, + style::ResetColor, + terminal::{Clear, ClearType}, + QueueableCommand as _}; +use r3bl_core::LockedOutputDevice; + +use super::RenderOpImplCrossterm; +use crate::{exec_render_op, exec_render_op_2, Flush}; + +impl Flush for RenderOpImplCrossterm { + fn flush(locked_output_device: LockedOutputDevice<'_>) { + flush(locked_output_device); + } + + fn clear_before_flush(locked_output_device: LockedOutputDevice<'_>) { + clear_before_flush(locked_output_device); + } +} + +pub fn clear_before_flush(locked_output_device: LockedOutputDevice<'_>) { + exec_render_op_2!( + locked_output_device, + ResetColor, + "ResetColor -> before flush()" + ); + exec_render_op_2!( + locked_output_device, + Clear(ClearType::All), + "Clear -> before flush()" + ) +} + +pub fn flush(locked_output_device: LockedOutputDevice<'_>) { + exec_render_op!(locked_output_device.flush(), "flush() -> output_device"); +} diff --git a/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/mod.rs b/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/mod.rs new file mode 100644 index 000000000..27756f5ef --- /dev/null +++ b/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/mod.rs @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Attach. +pub mod flush; +pub mod paint; +pub mod render_op_struct; + +// Re-export. +pub use flush::*; +pub use render_op_struct::*; diff --git a/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/paint.rs b/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/paint.rs new file mode 100644 index 000000000..c1700493d --- /dev/null +++ b/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/paint.rs @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crossterm::{self, + style::ResetColor, + terminal::{Clear, ClearType}, + QueueableCommand as _}; +use r3bl_core::{LockedOutputDevice, Size}; + +use super::RenderOpImplCrossterm; +use crate::{exec_render_op_2, PaintRenderOp, RenderOp, RenderOpsLocalData}; + +impl PaintRenderOp for RenderOpImplCrossterm { + fn paint( + &mut self, + skip_flush: &mut bool, + command_ref: &RenderOp, + window_size: Size, + local_data: &mut RenderOpsLocalData, + locked_output_device: LockedOutputDevice<'_>, + ) { + match command_ref { + RenderOp::Noop => {} + RenderOp::EnterRawMode => { + RenderOpImplCrossterm::raw_mode_enter(skip_flush, locked_output_device); + } + RenderOp::ExitRawMode => { + RenderOpImplCrossterm::raw_mode_exit(skip_flush, locked_output_device); + } + RenderOp::MoveCursorPositionAbs(abs_pos) => { + RenderOpImplCrossterm::move_cursor_position_abs( + *abs_pos, + window_size, + local_data, + locked_output_device, + ); + } + RenderOp::MoveCursorPositionRelTo(box_origin_pos, content_rel_pos) => { + RenderOpImplCrossterm::move_cursor_position_rel_to( + *box_origin_pos, + *content_rel_pos, + window_size, + local_data, + locked_output_device, + ); + } + RenderOp::ClearScreen => { + exec_render_op_2!( + locked_output_device, + Clear(ClearType::All), + "ClearScreen" + ) + } + RenderOp::SetFgColor(color) => { + RenderOpImplCrossterm::set_fg_color(color, locked_output_device); + } + RenderOp::SetBgColor(color) => { + RenderOpImplCrossterm::set_bg_color(color, locked_output_device); + } + RenderOp::ResetColor => { + exec_render_op_2!(locked_output_device, ResetColor, "ResetColor") + } + RenderOp::ApplyColors(style) => { + RenderOpImplCrossterm::apply_colors(style, locked_output_device); + } + RenderOp::CompositorNoClipTruncPaintTextWithAttributes(text, maybe_style) => { + RenderOpImplCrossterm::paint_text_with_attributes( + text, + maybe_style, + window_size, + local_data, + locked_output_device, + ); + } + RenderOp::PaintTextWithAttributes(_text, _maybe_style) => { + // This should never be executed! The compositor always renders to an offscreen + // buffer first, then that is diff'd and then painted via calls to + // CompositorNoClipTruncPaintTextWithAttributes. + } + } + } +} diff --git a/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/render_op_struct.rs b/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/render_op_struct.rs new file mode 100644 index 000000000..9f4f1ff1d --- /dev/null +++ b/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op/render_op_struct.rs @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::borrow::Cow; + +use crossterm::{self, + cursor::{Hide, MoveTo, Show}, + event::{DisableMouseCapture, EnableMouseCapture}, + style::{SetBackgroundColor, SetForegroundColor}, + terminal::{self, + Clear, + ClearType, + EnterAlternateScreen, + LeaveAlternateScreen}, + QueueableCommand as _}; +use r3bl_core::{LockedOutputDevice, Position, Size, TuiColor, TuiStyle}; + +use super::flush::{self}; +use crate::{crossterm_color_converter::convert_from_tui_color_to_crossterm_color, + exec_render_op, + exec_render_op_2, + paint_impl, + sanitize_and_save_abs_position, + RenderOpsLocalData}; + +/// Given a crossterm command, this will run it and [tracing::error!] or [tracing::info!] +/// the [Result] that is returned. +/// +/// Paste docs: +#[macro_export] +macro_rules! exec_render_op { + ( + $arg_cmd: expr, + $arg_log_msg: expr + ) => {{ + use $crate::tui::DEBUG_TUI_SHOW_TERMINAL_BACKEND; + match $arg_cmd { + Ok(_) => { + let msg = format!("crossterm: āœ… {} successfully", $arg_log_msg); + r3bl_core::call_if_true! { + DEBUG_TUI_SHOW_TERMINAL_BACKEND, + tracing::info!(msg) + }; + } + Err(err) => { + let msg = + format!("crossterm: āŒ Failed to {} due to {}", $arg_log_msg, err); + r3bl_core::call_if_true!( + DEBUG_TUI_SHOW_TERMINAL_BACKEND, + tracing::error!(msg) + ); + } + } + }}; +} + +/// Given a crossterm command, this will run it and [tracing::error!] or [tracing::info!] +/// the [Result] that is returned. +/// +/// Paste docs: +#[macro_export] +macro_rules! exec_render_op_2 { + ( + $arg_locked_output_device: expr, + $arg_cmd: expr, + $arg_log_msg: expr + ) => {{ + use $crate::tui::DEBUG_TUI_SHOW_TERMINAL_BACKEND; + match $arg_locked_output_device.queue($arg_cmd) { + Ok(_) => { + let msg = format!("crossterm: āœ… {} successfully", $arg_log_msg); + r3bl_core::call_if_true! { + DEBUG_TUI_SHOW_TERMINAL_BACKEND, + tracing::info!(msg) + }; + } + Err(err) => { + let msg = + format!("crossterm: āŒ Failed to {} due to {}", $arg_log_msg, err); + r3bl_core::call_if_true!( + DEBUG_TUI_SHOW_TERMINAL_BACKEND, + tracing::error!(msg) + ); + } + } + }}; +} + +/// Struct representing the implementation of [crate::RenderOp] for crossterm terminal +/// backend. This empty struct is needed since the [crate::Flush] trait needs to be +/// implemented. +pub struct RenderOpImplCrossterm; + +impl RenderOpImplCrossterm { + pub fn move_cursor_position_rel_to( + box_origin_pos: Position, + content_rel_pos: Position, + window_size: Size, + local_data: &mut RenderOpsLocalData, + locked_output_device: LockedOutputDevice<'_>, + ) { + let new_abs_pos = box_origin_pos + content_rel_pos; + Self::move_cursor_position_abs( + new_abs_pos, + window_size, + local_data, + locked_output_device, + ); + } + + pub fn move_cursor_position_abs( + abs_pos: Position, + window_size: Size, + local_data: &mut RenderOpsLocalData, + locked_output_device: LockedOutputDevice<'_>, + ) { + let Position { + col_index: col, + row_index: row, + } = sanitize_and_save_abs_position(abs_pos, window_size, local_data); + exec_render_op_2!( + locked_output_device, + MoveTo(*col, *row), + format!("MoveCursorPosition(col: {}, row: {})", *col, *row) + ) + } + + pub fn raw_mode_exit( + skip_flush: &mut bool, + locked_output_device: LockedOutputDevice<'_>, + ) { + exec_render_op_2!( + locked_output_device, + Show, + "ExitRawMode -> āœ”ļø Show, LeaveAlternateScreen, DisableMouseCapture" + ); + exec_render_op_2!( + locked_output_device, + LeaveAlternateScreen, + "ExitRawMode -> āœ”ļø Show, āœ”ļø LeaveAlternateScreen, DisableMouseCapture" + ); + exec_render_op_2!( + locked_output_device, + DisableMouseCapture, + "ExitRawMode -> āœ”ļø Show, āœ”ļø LeaveAlternateScreen, āœ”ļø DisableMouseCapture" + ); + flush::flush(locked_output_device); + exec_render_op!( + terminal::disable_raw_mode(), + "ExitRawMode -> disable_raw_mode()" + ); + *skip_flush = true; + } + + pub fn raw_mode_enter( + skip_flush: &mut bool, + locked_output_device: LockedOutputDevice<'_>, + ) { + exec_render_op!( + terminal::enable_raw_mode(), + "EnterRawMode -> enable_raw_mode()" + ); + exec_render_op_2!( + locked_output_device, + EnableMouseCapture, + "EnterRawMode -> āœ”ļø EnableMouseCapture, EnterAlternateScreen, MoveTo(0,0), Clear(ClearType::All), Hide" + ); + exec_render_op_2!( + locked_output_device, + EnterAlternateScreen, + "EnterRawMode -> āœ”ļø EnableMouseCapture, āœ”ļø EnterAlternateScreen, MoveTo(0,0), Clear(ClearType::All), Hide" + ); + exec_render_op_2!( + locked_output_device, + MoveTo(0,0), + "EnterRawMode -> āœ”ļø EnableMouseCapture, āœ”ļø EnterAlternateScreen, āœ”ļø MoveTo(0,0), Clear(ClearType::All), Hide" + ); + exec_render_op_2!( + locked_output_device, + Clear(ClearType::All), + "EnterRawMode -> āœ”ļø EnableMouseCapture, āœ”ļø EnterAlternateScreen, āœ”ļø MoveTo(0,0), āœ”ļø Clear(ClearType::All), Hide" + ); + exec_render_op_2!( + locked_output_device, + Hide, + "EnterRawMode -> āœ”ļø EnableMouseCapture, āœ”ļø EnterAlternateScreen, āœ”ļø MoveTo(0,0), āœ”ļø Clear(ClearType::All), āœ”ļø Hide" + ); + flush::flush(locked_output_device); + *skip_flush = true; + } + + pub fn set_fg_color(color: &TuiColor, locked_output_device: LockedOutputDevice<'_>) { + let color = convert_from_tui_color_to_crossterm_color(*color); + exec_render_op_2!( + locked_output_device, + SetForegroundColor(color), + format!("SetFgColor({color:?})") + ) + } + + pub fn set_bg_color(color: &TuiColor, locked_output_device: LockedOutputDevice<'_>) { + let color: crossterm::style::Color = + convert_from_tui_color_to_crossterm_color(*color); + exec_render_op_2!( + locked_output_device, + SetBackgroundColor(color), + format!("SetBgColor({color:?})") + ) + } + + pub fn paint_text_with_attributes( + text_arg: &String, + maybe_style: &Option, + window_size: Size, + local_data: &mut RenderOpsLocalData, + locked_output_device: LockedOutputDevice<'_>, + ) { + // Gen log_msg. + let log_msg = Cow::from(format!("\"{text_arg}\"")); + + let text: Cow<'_, str> = Cow::from(text_arg); + + let mut paint_args = paint_impl::PaintArgs { + text, + log_msg, + maybe_style, + window_size, + }; + + let needs_reset = Cow::Owned(false); + + // Paint plain_text. + paint_impl::paint_style_and_text( + &mut paint_args, + needs_reset, + local_data, + locked_output_device, + ); + } + + /// Use [crossterm::style::Color] to set crossterm Colors. Docs: + /// + pub fn apply_colors( + maybe_style: &Option, + locked_output_device: LockedOutputDevice<'_>, + ) { + if let Some(style) = maybe_style { + // Handle background color. + if let Some(tui_color_bg) = style.color_bg { + let color_bg: crossterm::style::Color = + crate::convert_from_tui_color_to_crossterm_color(tui_color_bg); + exec_render_op_2!( + locked_output_device, + SetBackgroundColor(color_bg), + format!("ApplyColors -> SetBgColor({color_bg:?})") + ) + } + + // Handle foreground color. + if let Some(tui_color_fg) = style.color_fg { + let color_fg: crossterm::style::Color = + crate::convert_from_tui_color_to_crossterm_color(tui_color_fg); + exec_render_op_2!( + locked_output_device, + SetForegroundColor(color_fg), + format!("ApplyColors -> SetFgColor({color_fg:?})") + ) + } + } + } +} diff --git a/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op_impl.rs b/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op_impl.rs deleted file mode 100644 index 923307b9b..000000000 --- a/tui/src/tui/terminal_lib_backends/crossterm_backend/render_op_impl.rs +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Copyright (c) 2022 R3BL LLC - * All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -use std::{borrow::Cow, - io::{stderr, stdout, Write}}; - -use crossterm::{self, - cursor::{Hide, MoveTo, Show}, - event::{DisableMouseCapture, EnableMouseCapture}, - queue, - style::{Attribute, - Print, - ResetColor, - SetAttribute, - SetBackgroundColor, - SetForegroundColor}, - terminal::{self, - Clear, - ClearType, - EnterAlternateScreen, - LeaveAlternateScreen}}; -use r3bl_core::{call_if_true, - console_log, - throws, - CommonResult, - Position, - Size, - TuiColor, - TuiStyle, - UnicodeString}; - -use crate::{crossterm_color_converter::convert_from_tui_color_to_crossterm_color, - exec_render_op, - sanitize_and_save_abs_position, - Flush, - PaintRenderOp, - RenderOp, - RenderOpsLocalData}; - -/// Struct representing the implementation of [RenderOp] for crossterm terminal backend. This empty -/// struct is needed since the [Flush] trait needs to be implemented. -pub struct RenderOpImplCrossterm; - -mod impl_trait_paint_render_op { - use super::*; - - impl PaintRenderOp for RenderOpImplCrossterm { - fn paint( - &mut self, - skip_flush: &mut bool, - command_ref: &RenderOp, - window_size: Size, - local_data: &mut RenderOpsLocalData, - ) { - match command_ref { - RenderOp::Noop => {} - RenderOp::EnterRawMode => { - RenderOpImplCrossterm::raw_mode_enter(skip_flush, window_size); - } - RenderOp::ExitRawMode => { - RenderOpImplCrossterm::raw_mode_exit(skip_flush); - } - RenderOp::MoveCursorPositionAbs(abs_pos) => { - RenderOpImplCrossterm::move_cursor_position_abs( - *abs_pos, - window_size, - local_data, - ); - } - RenderOp::MoveCursorPositionRelTo(box_origin_pos, content_rel_pos) => { - RenderOpImplCrossterm::move_cursor_position_rel_to( - *box_origin_pos, - *content_rel_pos, - window_size, - local_data, - ); - } - RenderOp::ClearScreen => { - exec_render_op!( - queue!(stdout(), Clear(ClearType::All)), - "ClearScreen" - ) - } - RenderOp::SetFgColor(color) => { - RenderOpImplCrossterm::set_fg_color(color); - } - RenderOp::SetBgColor(color) => { - RenderOpImplCrossterm::set_bg_color(color); - } - RenderOp::ResetColor => { - exec_render_op!(queue!(stdout(), ResetColor), "ResetColor") - } - RenderOp::ApplyColors(style) => { - RenderOpImplCrossterm::apply_colors(style); - } - RenderOp::CompositorNoClipTruncPaintTextWithAttributes( - text, - maybe_style, - ) => { - RenderOpImplCrossterm::paint_text_with_attributes( - text, - maybe_style, - window_size, - local_data, - ); - } - RenderOp::PaintTextWithAttributes(_text, _maybe_style) => { - // This should never be executed! The compositor always renders to an offscreen - // buffer first, then that is diff'd and then painted via calls to - // CompositorNoClipTruncPaintTextWithAttributes. - } - } - } - } -} - -pub mod impl_trait_flush { - use super::*; - - impl Flush for RenderOpImplCrossterm { - fn flush(&mut self) { flush(); } - fn clear_before_flush(&mut self) { clear_before_flush(); } - } - - fn clear_before_flush() { - exec_render_op! { - queue!(stdout(), - ResetColor, - Clear(ClearType::All), - ), - "flush() -> after ResetColor, Clear" - } - } - - pub fn flush() { - exec_render_op!(stdout().flush(), "flush() -> stdout"); - exec_render_op!(stderr().flush(), "flush() -> stderr"); - } -} - -mod impl_self { - use super::*; - - impl RenderOpImplCrossterm { - pub fn move_cursor_position_rel_to( - box_origin_pos: Position, - content_rel_pos: Position, - window_size: Size, - local_data: &mut RenderOpsLocalData, - ) { - let new_abs_pos = box_origin_pos + content_rel_pos; - Self::move_cursor_position_abs(new_abs_pos, window_size, local_data); - } - - pub fn move_cursor_position_abs( - abs_pos: Position, - window_size: Size, - local_data: &mut RenderOpsLocalData, - ) { - let Position { - col_index: col, - row_index: row, - } = sanitize_and_save_abs_position(abs_pos, window_size, local_data); - exec_render_op!( - queue!(stdout(), MoveTo(*col, *row)), - format!("MoveCursorPosition(col: {}, row: {})", *col, *row) - ) - } - - pub fn raw_mode_exit(skip_flush: &mut bool) { - exec_render_op! { - queue!(stdout(), - Show, - LeaveAlternateScreen, - DisableMouseCapture - ), - "ExitRawMode -> Show, LeaveAlternateScreen, DisableMouseCapture" - }; - impl_trait_flush::flush(); - exec_render_op! {terminal::disable_raw_mode(), "ExitRawMode -> disable_raw_mode()"} - *skip_flush = true; - } - - pub fn raw_mode_enter(skip_flush: &mut bool, _: Size) { - exec_render_op! { - terminal::enable_raw_mode(), - "EnterRawMode -> enable_raw_mode()" - }; - exec_render_op! { - queue!(stdout(), - EnableMouseCapture, - EnterAlternateScreen, - MoveTo(0,0), - Clear(ClearType::All), - Hide, - ), - "EnterRawMode -> EnableMouseCapture, EnterAlternateScreen, MoveTo(0,0), Clear(ClearType::All), Hide" - } - impl_trait_flush::flush(); - *skip_flush = true; - } - - pub fn set_fg_color(color: &TuiColor) { - let color = convert_from_tui_color_to_crossterm_color(*color); - exec_render_op!( - queue!(stdout(), SetForegroundColor(color)), - format!("SetFgColor({color:?})") - ) - } - - pub fn set_bg_color(color: &TuiColor) { - let color: crossterm::style::Color = - convert_from_tui_color_to_crossterm_color(*color); - exec_render_op!( - queue!(stdout(), SetBackgroundColor(color)), - format!("SetBgColor({color:?})") - ) - } - - pub fn paint_text_with_attributes( - text_arg: &String, - maybe_style: &Option, - window_size: Size, - local_data: &mut RenderOpsLocalData, - ) { - use perform_paint::*; - - // Gen log_msg. - let log_msg = Cow::from(format!("\"{text_arg}\"")); - - let text: Cow<'_, str> = Cow::from(text_arg); - - let mut paint_args = PaintArgs { - text, - log_msg, - maybe_style, - window_size, - }; - - let needs_reset = Cow::Owned(false); - - // Paint plain_text. - paint_style_and_text(&mut paint_args, needs_reset, local_data); - } - - /// Use [crossterm::style::Color] to set crossterm Colors. - /// Docs: - pub fn apply_colors(maybe_style: &Option) { - if let Some(style) = maybe_style { - // Handle background color. - if let Some(tui_color_bg) = style.color_bg { - let color_bg: crossterm::style::Color = - crate::convert_from_tui_color_to_crossterm_color(tui_color_bg); - exec_render_op!( - queue!(stdout(), SetBackgroundColor(color_bg)), - format!("ApplyColors -> SetBgColor({color_bg:?})") - ) - } - - // Handle foreground color. - if let Some(tui_color_fg) = style.color_fg { - let color_fg: crossterm::style::Color = - crate::convert_from_tui_color_to_crossterm_color(tui_color_fg); - exec_render_op!( - queue!(stdout(), SetForegroundColor(color_fg)), - format!("ApplyColors -> SetFgColor({color_fg:?})") - ) - } - } - } - } -} - -mod perform_paint { - use super::*; - - #[derive(Debug)] - pub struct PaintArgs<'a> { - pub text: Cow<'a, str>, - pub log_msg: Cow<'a, str>, - pub maybe_style: &'a Option, - pub window_size: Size, - } - - fn style_to_attribute(&style: &TuiStyle) -> Vec { - let mut it = vec![]; - if style.bold { - it.push(Attribute::Bold); - } - if style.italic { - it.push(Attribute::Italic); - } - if style.dim { - it.push(Attribute::Dim); - } - if style.underline { - it.push(Attribute::Underlined); - } - if style.reverse { - it.push(Attribute::Reverse); - } - if style.hidden { - it.push(Attribute::Hidden); - } - if style.strikethrough { - it.push(Attribute::Fraktur); - } - it - } - - /// Use [Style] to set crossterm [Attributes] ([docs]( - /// https://docs.rs/crossterm/latest/crossterm/style/index.html#attributes)). - pub fn paint_style_and_text( - paint_args: &mut PaintArgs<'_>, - mut needs_reset: Cow<'_, bool>, - local_data: &mut RenderOpsLocalData, - ) { - let PaintArgs { maybe_style, .. } = paint_args; - - if let Some(style) = maybe_style { - let attrib_vec = style_to_attribute(style); - attrib_vec.iter().for_each(|attr| { - exec_render_op!( - queue!(stdout(), SetAttribute(*attr)), - format!("PaintWithAttributes -> SetAttribute({attr:?})") - ); - needs_reset = Cow::Owned(true); - }); - } - - paint_text(paint_args, local_data); - - if *needs_reset { - exec_render_op!( - queue!(stdout(), SetAttribute(Attribute::Reset)), - format!("PaintWithAttributes -> SetAttribute(Reset))") - ); - } - } - - pub fn paint_text(paint_args: &PaintArgs<'_>, local_data: &mut RenderOpsLocalData) { - let PaintArgs { - text, - log_msg, - window_size, - .. - } = paint_args; - - let unicode_string: UnicodeString = text.as_ref().into(); - let mut cursor_position_copy = local_data.cursor_position; - - // Actually paint text. - { - let text = Cow::Borrowed(text); - let log_msg: &str = log_msg; - exec_render_op!( - queue!(stdout(), Print(&text)), - format!("Print( {} {log_msg})", &text) - ); - }; - - // Update cursor position after paint. - let display_width = unicode_string.display_width; - - cursor_position_copy.col_index += display_width; - sanitize_and_save_abs_position(cursor_position_copy, *window_size, local_data); - } -} - -/// Given a crossterm command, this will run it and [tracing::error!] or [tracing::info!] -/// the [Result] that is returned. -/// -/// Paste docs: -#[macro_export] -macro_rules! exec_render_op { - ( - $arg_cmd: expr, - $arg_log_msg: expr - ) => {{ - // Generate a new function that returns [CommonResult]. This needs to be called. - // The only purpose of this generated method is to handle errors that may result - // from calling log! macro when there are issues accessing the log file for - // whatever reason. - use $crate::tui::DEBUG_TUI_SHOW_TERMINAL_BACKEND; - - let _fn_wrap_for_logging_err = || -> CommonResult<()> { - throws!({ - // Execute the command. - if let Err(err) = $arg_cmd { - let msg = format!("crossterm: āŒ Failed to {} due to {}", $arg_log_msg, err); - call_if_true!( - DEBUG_TUI_SHOW_TERMINAL_BACKEND, - tracing::error!(msg) - ); - } else { - let msg = format!("crossterm: āœ… {} successfully", $arg_log_msg); - call_if_true! { - DEBUG_TUI_SHOW_TERMINAL_BACKEND, - tracing::info!(msg) - }; - } - }) - }; - - // Call this generated function. It will fail if there are problems w/ log!(). In this case, if - // `DEBUG_TUI_SHOW_TERMINAL_BACKEND` is true, then it will dump the error to stderr. - if let Err(logging_err) = _fn_wrap_for_logging_err() { - let msg = format!( - "āŒ Failed to log exec output of {}, {}", - stringify!($arg_cmd), - $arg_log_msg - ); - call_if_true! { - DEBUG_TUI_SHOW_TERMINAL_BACKEND, - console_log!(ERROR_RAW &msg, logging_err) - }; - } - }}; -} diff --git a/tui/src/tui/terminal_lib_backends/async_event_stream_ext.rs b/tui/src/tui/terminal_lib_backends/input_device_ext.rs similarity index 82% rename from tui/src/tui/terminal_lib_backends/async_event_stream_ext.rs rename to tui/src/tui/terminal_lib_backends/input_device_ext.rs index e15199ea9..35ca4e294 100644 --- a/tui/src/tui/terminal_lib_backends/async_event_stream_ext.rs +++ b/tui/src/tui/terminal_lib_backends/input_device_ext.rs @@ -63,33 +63,23 @@ //! - //! - -use crossterm::event::EventStream; -use futures_util::{FutureExt, StreamExt}; -use r3bl_core::call_if_true; +use futures_util::FutureExt; +use r3bl_core::{call_if_true, InputDevice}; use super::InputEvent; use crate::DEBUG_TUI_SHOW_TERMINAL_BACKEND; -pub struct AsyncEventStream { - event_stream: EventStream, +pub trait InputDeviceExt { + #[allow(async_fn_in_trait)] + async fn next_input_event(&mut self) -> Option; } -impl Default for AsyncEventStream { - fn default() -> Self { - Self { - event_stream: EventStream::new(), - } - } -} - -impl AsyncEventStream { - pub async fn try_to_get_input_event( - async_event_stream: &mut AsyncEventStream, - ) -> Option { - let maybe_event = async_event_stream.event_stream.next().fuse().await; - match maybe_event { - Some(Ok(event)) => { - let input_event: Result = event.try_into(); +impl InputDeviceExt for InputDevice { + async fn next_input_event(&mut self) -> Option { + let maybe_result_event = self.next().fuse().await; + match maybe_result_event { + Ok(event) => { + let input_event = InputEvent::try_from(event); match input_event { Ok(input_event) => Some(input_event), Err(e) => { @@ -100,13 +90,12 @@ impl AsyncEventStream { } } } - Some(Err(e)) => { + Err(e) => { call_if_true!(DEBUG_TUI_SHOW_TERMINAL_BACKEND, { tracing::error!("Error: {e:?}"); }); None } - _ => None, } } } diff --git a/tui/src/tui/terminal_lib_backends/mod.rs b/tui/src/tui/terminal_lib_backends/mod.rs index 29e0b5c13..a85c679a6 100644 --- a/tui/src/tui/terminal_lib_backends/mod.rs +++ b/tui/src/tui/terminal_lib_backends/mod.rs @@ -55,10 +55,10 @@ pub enum TerminalLibBackend { pub const TERMINAL_LIB_BACKEND: TerminalLibBackend = TerminalLibBackend::Crossterm; // Attach source files. -pub mod async_event_stream_ext; pub mod crossterm_backend; pub mod crossterm_color_converter; pub mod enhanced_keys; +pub mod input_device_ext; pub mod input_event; pub mod keypress; pub mod modifier_keys_mask; @@ -75,10 +75,10 @@ pub mod termion_backend; pub mod z_order; // Re-export. -pub use async_event_stream_ext::*; pub use crossterm_backend::*; pub use crossterm_color_converter::*; pub use enhanced_keys::*; +pub use input_device_ext::*; pub use input_event::*; pub use keypress::*; pub use modifier_keys_mask::*; diff --git a/tui/src/tui/terminal_lib_backends/offscreen_buffer.rs b/tui/src/tui/terminal_lib_backends/offscreen_buffer.rs index 92f2595ca..1c5b9e377 100644 --- a/tui/src/tui/terminal_lib_backends/offscreen_buffer.rs +++ b/tui/src/tui/terminal_lib_backends/offscreen_buffer.rs @@ -24,6 +24,7 @@ use r3bl_core::{ch, style_error, style_primary, GraphemeClusterSegment, + LockedOutputDevice, Position, Size, TuiColor, @@ -438,9 +439,20 @@ pub trait OffscreenBufferPaint { fn render_diff(&mut self, diff_chunks: &PixelCharDiffChunks) -> RenderOps; - fn paint(&mut self, render_ops: RenderOps, flush_kind: FlushKind, window_size: Size); - - fn paint_diff(&mut self, render_ops: RenderOps, window_size: Size); + fn paint( + &mut self, + render_ops: RenderOps, + flush_kind: FlushKind, + window_size: Size, + locked_output_device: LockedOutputDevice<'_>, + ); + + fn paint_diff( + &mut self, + render_ops: RenderOps, + window_size: Size, + locked_output_device: LockedOutputDevice<'_>, + ); } #[cfg(test)] diff --git a/tui/src/tui/terminal_lib_backends/paint.rs b/tui/src/tui/terminal_lib_backends/paint.rs index f48d4f79d..78006d83f 100644 --- a/tui/src/tui/terminal_lib_backends/paint.rs +++ b/tui/src/tui/terminal_lib_backends/paint.rs @@ -17,7 +17,7 @@ use std::fmt::Debug; -use r3bl_core::{call_if_true, Position, Size}; +use r3bl_core::{call_if_true, output_device_as_mut, LockedOutputDevice, Position, Size}; use super::{FlushKind, RenderOp, RenderOpsLocalData, RenderPipeline}; use crate::{GlobalData, @@ -38,6 +38,7 @@ pub trait PaintRenderOp { render_op: &RenderOp, window_size: Size, local_data: &mut RenderOpsLocalData, + locked_output_device: LockedOutputDevice<'_>, ); } @@ -57,54 +58,75 @@ pub fn paint( AS: Debug + Default + Clone + Sync + Send, { let maybe_saved_offscreen_buffer = global_data.maybe_saved_offscreen_buffer.clone(); - let window_size = global_data.window_size; - let offscreen_buffer = pipeline.convert(window_size); + let locked_output_device: LockedOutputDevice<'_> = + output_device_as_mut!(global_data.output_device); + match maybe_saved_offscreen_buffer { None => { - perform_full_paint(&offscreen_buffer, flush_kind, window_size); + perform_full_paint( + &offscreen_buffer, + flush_kind, + window_size, + locked_output_device, + ); } Some(saved_offscreen_buffer) => { // Compare offscreen buffers & paint only the diff. match saved_offscreen_buffer.diff(&offscreen_buffer) { OffscreenBufferDiffResult::NotComparable => { - perform_full_paint(&offscreen_buffer, flush_kind, window_size); + perform_full_paint( + &offscreen_buffer, + flush_kind, + window_size, + locked_output_device, + ); } OffscreenBufferDiffResult::Comparable(ref diff_chunks) => { - perform_diff_paint(diff_chunks, window_size); + perform_diff_paint(diff_chunks, window_size, locked_output_device); } } } } global_data.maybe_saved_offscreen_buffer = Some(offscreen_buffer); +} - fn perform_diff_paint(diff_chunks: &PixelCharDiffChunks, window_size: Size) { - match TERMINAL_LIB_BACKEND { - TerminalLibBackend::Crossterm => { - let mut crossterm_impl = OffscreenBufferPaintImplCrossterm {}; - let render_ops = crossterm_impl.render_diff(diff_chunks); - crossterm_impl.paint_diff(render_ops, window_size); - } - TerminalLibBackend::Termion => todo!(), // FUTURE: implement OffscreenBufferPaint trait for termion +fn perform_diff_paint( + diff_chunks: &PixelCharDiffChunks, + window_size: Size, + locked_output_device: LockedOutputDevice<'_>, +) { + match TERMINAL_LIB_BACKEND { + TerminalLibBackend::Crossterm => { + let mut crossterm_impl = OffscreenBufferPaintImplCrossterm; + let render_ops = crossterm_impl.render_diff(diff_chunks); + crossterm_impl.paint_diff(render_ops, window_size, locked_output_device); } + TerminalLibBackend::Termion => todo!(), // FUTURE: implement OffscreenBufferPaint trait for termion } +} - fn perform_full_paint( - offscreen_buffer: &OffscreenBuffer, - flush_kind: FlushKind, - window_size: Size, - ) { - match TERMINAL_LIB_BACKEND { - TerminalLibBackend::Crossterm => { - let mut crossterm_impl = OffscreenBufferPaintImplCrossterm {}; - let render_ops = crossterm_impl.render(offscreen_buffer); - crossterm_impl.paint(render_ops, flush_kind, window_size); - } - TerminalLibBackend::Termion => todo!(), // FUTURE: implement OffscreenBufferPaint trait for termion +fn perform_full_paint( + offscreen_buffer: &OffscreenBuffer, + flush_kind: FlushKind, + window_size: Size, + locked_output_device: LockedOutputDevice<'_>, +) { + match TERMINAL_LIB_BACKEND { + TerminalLibBackend::Crossterm => { + let mut crossterm_impl = OffscreenBufferPaintImplCrossterm; + let render_ops = crossterm_impl.render(offscreen_buffer); + crossterm_impl.paint( + render_ops, + flush_kind, + window_size, + locked_output_device, + ); } + TerminalLibBackend::Termion => todo!(), // FUTURE: implement OffscreenBufferPaint trait for termion } } diff --git a/tui/src/tui/terminal_lib_backends/raw_mode.rs b/tui/src/tui/terminal_lib_backends/raw_mode.rs index bb959a11f..1b6f694be 100644 --- a/tui/src/tui/terminal_lib_backends/raw_mode.rs +++ b/tui/src/tui/terminal_lib_backends/raw_mode.rs @@ -15,7 +15,7 @@ * limitations under the License. */ -use r3bl_core::Size; +use r3bl_core::{output_device_as_mut, LockedOutputDevice, OutputDevice, Size}; use super::{RenderOp, RenderOps, RenderOpsLocalData}; @@ -25,23 +25,29 @@ use super::{RenderOp, RenderOps, RenderOpsLocalData}; pub struct RawMode; impl RawMode { - pub fn start(window_size: Size) { + pub fn start(window_size: Size, output_device: OutputDevice) { let mut skip_flush = false; + let locked_output_device: LockedOutputDevice<'_> = + output_device_as_mut!(output_device); RenderOps::route_paint_render_op_to_backend( &mut RenderOpsLocalData::default(), &mut skip_flush, &RenderOp::EnterRawMode, window_size, + locked_output_device, ); } - pub fn end(window_size: Size) { + pub fn end(window_size: Size, output_device: OutputDevice) { let mut skip_flush = false; + let locked_output_device: LockedOutputDevice<'_> = + output_device_as_mut!(output_device); RenderOps::route_paint_render_op_to_backend( &mut RenderOpsLocalData::default(), &mut skip_flush, &RenderOp::ExitRawMode, window_size, + locked_output_device, ); } } diff --git a/tui/src/tui/terminal_lib_backends/render_op.rs b/tui/src/tui/terminal_lib_backends/render_op.rs index 38e1e1c9d..d082efccd 100644 --- a/tui/src/tui/terminal_lib_backends/render_op.rs +++ b/tui/src/tui/terminal_lib_backends/render_op.rs @@ -18,7 +18,7 @@ use std::{fmt::{Debug, Formatter, Result}, ops::{AddAssign, Deref, DerefMut}}; -use r3bl_core::{Position, Size, TuiColor, TuiStyle}; +use r3bl_core::{LockedOutputDevice, Position, Size, TuiColor, TuiStyle}; use serde::{Deserialize, Serialize}; use super::TERMINAL_LIB_BACKEND; @@ -166,7 +166,12 @@ pub mod render_ops_impl { use super::*; impl RenderOps { - pub fn execute_all(&self, skip_flush: &mut bool, window_size: Size) { + pub fn execute_all( + &self, + skip_flush: &mut bool, + window_size: Size, + locked_output_device: LockedOutputDevice<'_>, + ) { let mut local_data = RenderOpsLocalData::default(); for render_op in self.list.iter() { RenderOps::route_paint_render_op_to_backend( @@ -174,6 +179,7 @@ pub mod render_ops_impl { skip_flush, render_op, window_size, + locked_output_device, ); } } @@ -183,14 +189,16 @@ pub mod render_ops_impl { skip_flush: &mut bool, render_op: &RenderOp, window_size: Size, + locked_output_device: LockedOutputDevice<'_>, ) { match TERMINAL_LIB_BACKEND { TerminalLibBackend::Crossterm => { - RenderOpImplCrossterm {}.paint( + RenderOpImplCrossterm.paint( skip_flush, render_op, window_size, local_data, + locked_output_device, ); } TerminalLibBackend::Termion => todo!(), // FUTURE: implement PaintRenderOp trait for termion @@ -316,19 +324,19 @@ mod render_op_impl_trait_flush { use super::*; impl Flush for RenderOp { - fn flush(&mut self) { + fn flush(locked_output_device: LockedOutputDevice<'_>) { match TERMINAL_LIB_BACKEND { TerminalLibBackend::Crossterm => { - RenderOpImplCrossterm {}.flush(); + RenderOpImplCrossterm::flush(locked_output_device); } TerminalLibBackend::Termion => todo!(), // FUTURE: implement flush for termion } } - fn clear_before_flush(&mut self) { + fn clear_before_flush(locked_output_device: LockedOutputDevice<'_>) { match TERMINAL_LIB_BACKEND { TerminalLibBackend::Crossterm => { - RenderOpImplCrossterm {}.clear_before_flush(); + RenderOpImplCrossterm::clear_before_flush(locked_output_device); } TerminalLibBackend::Termion => todo!(), // FUTURE: implement clear_before_flush for termion } @@ -343,8 +351,9 @@ pub enum FlushKind { } pub trait Flush { - fn flush(&mut self); - fn clear_before_flush(&mut self); + fn flush(locked_output_device: LockedOutputDevice<'_>); + + fn clear_before_flush(locked_output_device: LockedOutputDevice<'_>); } pub trait DebugFormatRenderOp { diff --git a/tui/src/tui/terminal_window/main_event_loop.rs b/tui/src/tui/terminal_window/main_event_loop.rs index 4ad938299..541e798ae 100644 --- a/tui/src/tui/terminal_window/main_event_loop.rs +++ b/tui/src/tui/terminal_window/main_event_loop.rs @@ -19,6 +19,8 @@ use std::{fmt::Debug, marker::PhantomData}; use r3bl_core::{call_if_true, ch, + ok, + output_device_as_mut, position, throws, Ansi256GradientIndex, @@ -27,6 +29,9 @@ use r3bl_core::{call_if_true, ColorWheelSpeed, CommonResult, GradientGenerationPolicy, + InputDevice, + LockedOutputDevice, + OutputDevice, Size, TextColorizationPolicy, TooSmallToDisplayResult, @@ -38,214 +43,195 @@ use tokio::sync::mpsc; use super::{BoxedSafeApp, Continuation, DefaultInputEventHandler, EventPropagation}; use crate::{render_pipeline, telemetry_global_static, - AsyncEventStream, ComponentRegistryMap, - FlexBoxId, Flush as _, FlushKind, GlobalData, HasFocus, + InputDeviceExt, InputEvent, MinSize, RawMode, RenderOp, RenderPipeline, + TerminalWindowMainThreadSignal, ZOrder, DEBUG_TUI_MOD}; -pub struct TerminalWindow; - pub const CHANNEL_WIDTH: usize = 1_000; -#[derive(Debug)] -pub enum TerminalWindowMainThreadSignal +pub async fn main_event_loop_impl( + mut app: BoxedSafeApp, + exit_keys: Vec, + state: S, + initial_size: Size, + mut input_device: InputDevice, + output_device: OutputDevice, +) -> CommonResult<( + /* global_data */ GlobalData, + /* event stream */ InputDevice, + /* stdout */ OutputDevice, +)> where - AS: Debug + Default + Clone + Sync + Send, + S: Debug + Default + Clone + Sync + Send, + AS: Debug + Default + Clone + Sync + Send + 'static, { - /// Exit the main event loop. - Exit, - /// Render the app. - Render(Option), - /// Apply an action to the app. - ApplyAction(AS), -} - -impl TerminalWindow { - /// This is the main event loop for the entire application. It is responsible for - /// handling all input events, and dispatching them to the [crate::App] for - /// processing. It is also responsible for rendering the [crate::App] after each input - /// event. It is also responsible for handling all signals sent from the [crate::App] - /// to the main event loop (eg: exit, re-render, apply action, etc). - pub async fn main_event_loop( - mut app: BoxedSafeApp, - exit_keys: Vec, - state: S, - ) -> CommonResult<()> - where - S: Debug + Default + Clone + Sync + Send, - AS: Debug + Default + Clone + Sync + Send + 'static, - { - throws!({ - // mpsc channel to send signals from the app to the main event loop (eg: for exit, - // re-render, apply action, etc). - let (main_thread_channel_sender, mut main_thread_channel_receiver) = - mpsc::channel::>(CHANNEL_WIDTH); - - // Initialize the terminal window data struct. - let global_data = &mut GlobalData::try_to_create_instance( - main_thread_channel_sender.clone(), - state, - )?; - - // Start raw mode. - RawMode::start(global_data.window_size); - - // Create a new event stream (async). - let async_event_stream = &mut AsyncEventStream::default(); - - let app = &mut app; - - // This map is used to cache [Component]s that have been created and are meant to be reused between - // multiple renders. - // 1. It is entirely up to the [App] on how this [ComponentRegistryMap] is used. - // 2. The methods provided allow components to be added to the map. - let component_registry_map = &mut ComponentRegistryMap::default(); - let has_focus = &mut HasFocus::default(); - - // Init the app, and perform first render. - app.app_init(component_registry_map, has_focus); - AppManager::render_app(app, global_data, component_registry_map, has_focus)?; - - global_data.dump_to_log("main_event_loop -> Startup šŸš€"); - - // Main event loop. - loop { - tokio::select! { - // Handle signals on the channel. - // This branch is cancel safe since recv is cancel safe. - maybe_signal = main_thread_channel_receiver.recv() => { - if let Some(ref signal) = maybe_signal { - match signal { - TerminalWindowMainThreadSignal::Exit => { - // šŸ’ Actually exit the main loop! - RawMode::end(global_data.window_size); - break; - }, - TerminalWindowMainThreadSignal::Render(_) => { - AppManager::render_app( - app, - global_data, - component_registry_map, - has_focus, - )?; - }, - TerminalWindowMainThreadSignal::ApplyAction(action) => { - let result = app.app_handle_signal(action, global_data, component_registry_map, has_focus); - handle_result_generated_by_app_after_handling_action_or_input_event( - result, - None, - &exit_keys, - app, - global_data, - component_registry_map, - has_focus, - ); - }, - } - } - } - - // Handle input event. - // This branch is cancel safe because no state is declared inside the - // future in the following block. - // - All the state comes from other variables (self.*). - // - So if this future is dropped, then the item in the - // pinned_input_stream isn't used and the state isn't modified. - maybe_input_event = AsyncEventStream::try_to_get_input_event(async_event_stream) => { - if let Some(input_event) = maybe_input_event { - telemetry_global_static::set_start_ts(); - - call_if_true!(DEBUG_TUI_MOD, { - if let InputEvent::Keyboard(_)= input_event { - tracing::info!("main_event_loop -> Tick: ā° {input_event}"); - } - }); - - Self::handle_resize_if_applicable(input_event, - global_data, app, - component_registry_map, - has_focus); - - Self::actually_process_input_event( - global_data, + // mpsc channel to send signals from the app to the main event loop (eg: for exit, + // re-render, apply action, etc). + let (main_thread_channel_sender, mut main_thread_channel_receiver) = + mpsc::channel::>(CHANNEL_WIDTH); + + // Initialize the terminal window data struct. + let mut global_data = GlobalData::try_to_create_instance( + main_thread_channel_sender.clone(), + state, + initial_size, + output_device.clone(), + )?; + let global_data_ref = &mut global_data; + + // Start raw mode. + RawMode::start(global_data_ref.window_size, output_device.clone()); + + let app = &mut app; + + // This map is used to cache [Component]s that have been created and are meant to be reused between + // multiple renders. + // 1. It is entirely up to the [App] on how this [ComponentRegistryMap] is used. + // 2. The methods provided allow components to be added to the map. + let component_registry_map = &mut ComponentRegistryMap::default(); + let has_focus = &mut HasFocus::default(); + + // Init the app, and perform first render. + app.app_init(component_registry_map, has_focus); + AppManager::render_app(app, global_data_ref, component_registry_map, has_focus)?; + + global_data_ref.dump_to_log("main_event_loop -> Startup šŸš€"); + + // Main event loop. + loop { + tokio::select! { + // Handle signals on the channel. + // This branch is cancel safe since recv is cancel safe. + maybe_signal = main_thread_channel_receiver.recv() => { + if let Some(ref signal) = maybe_signal { + match signal { + TerminalWindowMainThreadSignal::Exit => { + // šŸ’ Actually exit the main loop! + RawMode::end(global_data_ref.window_size, output_device.clone()); + break; + }, + TerminalWindowMainThreadSignal::Render(_) => { + AppManager::render_app( app, - input_event, + global_data_ref, + component_registry_map, + has_focus, + )?; + }, + TerminalWindowMainThreadSignal::ApplyAction(action) => { + let result = app.app_handle_signal(action, global_data_ref, component_registry_map, has_focus); + handle_result_generated_by_app_after_handling_action_or_input_event( + result, + None, &exit_keys, + app, + global_data_ref, component_registry_map, has_focus, ); - } + }, } } - } // End loop. + } - call_if_true!(DEBUG_TUI_MOD, { - tracing::info!("main_event_loop -> Shutdown šŸ›‘"); - }); - }); - } + // Handle input event. + // This branch is cancel safe because no state is declared inside the + // future in the following block. + // - All the state comes from other variables (self.*). + // - So if this future is dropped, then the item in the + // pinned_input_stream isn't used and the state isn't modified. + maybe_input_event = input_device.next_input_event() => { + if let Some(input_event) = maybe_input_event { + telemetry_global_static::set_start_ts(); - fn actually_process_input_event( - global_data: &mut GlobalData, - app: &mut BoxedSafeApp, - input_event: InputEvent, - exit_keys: &[InputEvent], - component_registry_map: &mut ComponentRegistryMap, - has_focus: &mut HasFocus, - ) where - S: Debug + Default + Clone + Sync + Send, - AS: Debug + Default + Clone + Sync + Send + 'static, - { - let result = app.app_handle_input_event( - input_event, - global_data, - component_registry_map, - has_focus, - ); - - handle_result_generated_by_app_after_handling_action_or_input_event( - result, - Some(input_event), - exit_keys, - app, - global_data, - component_registry_map, - has_focus, - ); - } + call_if_true!(DEBUG_TUI_MOD, { + if let InputEvent::Keyboard(_)= input_event { + tracing::info!("main_event_loop -> Tick: ā° {input_event}"); + } + }); - /// Before any app gets to process the `input_event`, perform special handling in case - /// it is a resize event. - pub fn handle_resize_if_applicable( - input_event: InputEvent, - global_data: &mut GlobalData, - app: &mut BoxedSafeApp, - component_registry_map: &mut ComponentRegistryMap, - has_focus: &mut HasFocus, - ) where - S: Debug + Default + Clone + Sync + Send, - AS: Debug + Default + Clone + Sync + Send, - { - if let InputEvent::Resize(new_size) = input_event { - global_data.set_size(new_size); - global_data.maybe_saved_offscreen_buffer = None; - let _ = AppManager::render_app( - app, - global_data, - component_registry_map, - has_focus, - ); + handle_resize_if_applicable(input_event, + global_data_ref, app, + component_registry_map, + has_focus); + + actually_process_input_event( + global_data_ref, + app, + input_event, + &exit_keys, + component_registry_map, + has_focus, + ); + } + } } + } // End loop. + + call_if_true!(DEBUG_TUI_MOD, { + tracing::info!("main_event_loop -> Shutdown šŸ›‘"); + }); + + ok!((global_data, input_device, output_device)) +} + +fn actually_process_input_event( + global_data: &mut GlobalData, + app: &mut BoxedSafeApp, + input_event: InputEvent, + exit_keys: &[InputEvent], + component_registry_map: &mut ComponentRegistryMap, + has_focus: &mut HasFocus, +) where + S: Debug + Default + Clone + Sync + Send, + AS: Debug + Default + Clone + Sync + Send + 'static, +{ + let result = app.app_handle_input_event( + input_event, + global_data, + component_registry_map, + has_focus, + ); + + handle_result_generated_by_app_after_handling_action_or_input_event( + result, + Some(input_event), + exit_keys, + app, + global_data, + component_registry_map, + has_focus, + ); +} + +/// Before any app gets to process the `input_event`, perform special handling in case +/// it is a resize event. +pub fn handle_resize_if_applicable( + input_event: InputEvent, + global_data: &mut GlobalData, + app: &mut BoxedSafeApp, + component_registry_map: &mut ComponentRegistryMap, + has_focus: &mut HasFocus, +) where + S: Debug + Default + Clone + Sync + Send, + AS: Debug + Default + Clone + Sync + Send, +{ + if let InputEvent::Resize(new_size) = input_event { + global_data.set_size(new_size); + global_data.maybe_saved_offscreen_buffer = None; + let _ = + AppManager::render_app(app, global_data, component_registry_map, has_focus); } } @@ -347,7 +333,9 @@ where match render_result { Err(error) => { - RenderOp::default().flush(); + let locked_output_device: LockedOutputDevice<'_> = + output_device_as_mut!(global_data.output_device); + RenderOp::flush(locked_output_device); telemetry_global_static::set_end_ts(); @@ -430,3 +418,5 @@ fn render_window_too_small_error(window_size: Size) -> RenderPipeline { pipeline } + +// 00: add a test for main_event_loop_impl & return GlobalState diff --git a/tui/src/tui/terminal_window/mod.rs b/tui/src/tui/terminal_window/mod.rs index cd7cb69d2..2f3292f29 100644 --- a/tui/src/tui/terminal_window/mod.rs +++ b/tui/src/tui/terminal_window/mod.rs @@ -22,6 +22,7 @@ pub mod default_input_handler; pub mod event_routing_support; pub mod main_event_loop; pub mod manage_focus; +pub mod public_api; pub mod shared_global_data; pub mod static_global_data; pub mod type_aliases; @@ -33,6 +34,7 @@ pub use default_input_handler::*; pub use event_routing_support::*; pub use main_event_loop::*; pub use manage_focus::*; +pub use public_api::*; pub use shared_global_data::*; pub use static_global_data::*; pub use type_aliases::*; diff --git a/tui/src/tui/terminal_window/public_api.rs b/tui/src/tui/terminal_window/public_api.rs new file mode 100644 index 000000000..861b85b69 --- /dev/null +++ b/tui/src/tui/terminal_window/public_api.rs @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 R3BL LLC + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::fmt::Debug; + +use r3bl_core::{CommonResult, InputDevice, OutputDevice}; + +use super::{main_event_loop_impl, BoxedSafeApp, GlobalData}; +use crate::{terminal_lib_operations, FlexBoxId, InputEvent}; + +pub struct TerminalWindow; + +#[derive(Debug)] +pub enum TerminalWindowMainThreadSignal +where + AS: Debug + Default + Clone + Sync + Send, +{ + /// Exit the main event loop. + Exit, + /// Render the app. + Render(Option), + /// Apply an action to the app. + ApplyAction(AS), +} + +impl TerminalWindow { + /// This is the main event loop for the entire application. It is responsible for + /// handling all input events, and dispatching them to the [crate::App] for + /// processing. It is also responsible for rendering the [crate::App] after each input + /// event. It is also responsible for handling all signals sent from the [crate::App] + /// to the main event loop (eg: exit, re-render, apply action, etc). + pub async fn main_event_loop( + app: BoxedSafeApp, + exit_keys: Vec, + state: S, + ) -> CommonResult<( + /* global_data */ GlobalData, + /* event stream */ InputDevice, + /* stdout */ OutputDevice, + )> + where + S: Debug + Default + Clone + Sync + Send, + AS: Debug + Default + Clone + Sync + Send + 'static, + { + let initial_size = terminal_lib_operations::lookup_size()?; + let input_device = InputDevice::new_event_stream(); + let output_device = OutputDevice::new_stdout(); + + main_event_loop_impl( + app, + exit_keys, + state, + initial_size, + input_device, + output_device, + ) + .await + } +} diff --git a/tui/src/tui/terminal_window/shared_global_data.rs b/tui/src/tui/terminal_window/shared_global_data.rs index 380874bb7..12704be55 100644 --- a/tui/src/tui/terminal_window/shared_global_data.rs +++ b/tui/src/tui/terminal_window/shared_global_data.rs @@ -17,23 +17,23 @@ use std::fmt::{Debug, Formatter}; -use r3bl_core::{call_if_true, CommonResult, Size}; +use r3bl_core::{call_if_true, CommonResult, OutputDevice, Size}; use tokio::sync::mpsc::Sender; use super::TerminalWindowMainThreadSignal; -use crate::{terminal_lib_operations, - OffscreenBuffer, - DEBUG_TUI_COMPOSITOR, - DEBUG_TUI_MOD}; +use crate::{OffscreenBuffer, DEBUG_TUI_COMPOSITOR, DEBUG_TUI_MOD}; /// This is a global data structure that holds state for the entire application /// [crate::App] and the terminal window [crate::TerminalWindow] itself. /// -/// These are global state values for the entire application: +/// # Fields /// - The `window_size` holds the [Size] of the terminal window. /// - The `maybe_saved_offscreen_buffer` holds the last rendered [OffscreenBuffer]. /// - The `main_thread_channel_sender` is used to send [TerminalWindowMainThreadSignal]s /// - The `state` holds the application's state. +/// - The `output_device` is the terminal's output device (anything that implements +/// [r3bl_core::SafeRawTerminal] which can be [std::io::stdout] or +/// [r3bl_core::SharedWriter], etc.`). pub struct GlobalData where S: Debug + Default + Clone + Sync + Send, @@ -43,68 +43,66 @@ where pub maybe_saved_offscreen_buffer: Option, pub main_thread_channel_sender: Sender>, pub state: S, + pub output_device: OutputDevice, } -mod impl_self { - use super::*; - - impl Debug for GlobalData - where - S: Debug + Default + Clone + Sync + Send, - AS: Debug + Default + Clone + Sync + Send, - { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let vec_lines = { - let mut it = vec![]; - it.push(format!("window_size: {0:?}", self.window_size)); - it.push(match &self.maybe_saved_offscreen_buffer { - None => "no saved offscreen buffer".to_string(), - Some(ref offscreen_buffer) => match DEBUG_TUI_COMPOSITOR { - false => { - "offscreen buffer saved from previous render".to_string() - } - true => offscreen_buffer.pretty_print(), - }, - }); - it - }; - write!(f, "\nGlobalData\n - {}", vec_lines.join("\n - ")) - } +impl Debug for GlobalData +where + S: Debug + Default + Clone + Sync + Send, + AS: Debug + Default + Clone + Sync + Send, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let vec_lines = { + let mut it = vec![]; + it.push(format!("window_size: {0:?}", self.window_size)); + it.push(match &self.maybe_saved_offscreen_buffer { + None => "no saved offscreen buffer".to_string(), + Some(ref offscreen_buffer) => match DEBUG_TUI_COMPOSITOR { + false => "offscreen buffer saved from previous render".to_string(), + true => offscreen_buffer.pretty_print(), + }, + }); + it + }; + write!(f, "\nGlobalData\n - {}", vec_lines.join("\n - ")) } +} - impl GlobalData +impl GlobalData +where + S: Debug + Default + Clone + Sync + Send, + AS: Debug + Default + Clone + Sync + Send, +{ + pub fn try_to_create_instance( + main_thread_channel_sender: Sender>, + state: S, + initial_size: Size, + output_device: OutputDevice, + ) -> CommonResult> where - S: Debug + Default + Clone + Sync + Send, AS: Debug + Default + Clone + Sync + Send, { - pub fn try_to_create_instance( - main_thread_channel_sender: Sender>, - state: S, - ) -> CommonResult> - where - AS: Debug + Default + Clone + Sync + Send, - { - let mut it = GlobalData { - window_size: Default::default(), - maybe_saved_offscreen_buffer: Default::default(), - state, - main_thread_channel_sender, - }; + let mut it = GlobalData { + window_size: Default::default(), + maybe_saved_offscreen_buffer: Default::default(), + state, + main_thread_channel_sender, + output_device, + }; - it.set_size(terminal_lib_operations::lookup_size()?); + it.set_size(initial_size); - Ok(it) - } + Ok(it) + } - pub fn set_size(&mut self, new_size: Size) { - self.window_size = new_size; - self.dump_to_log("main_event_loop -> Resize"); - } + pub fn set_size(&mut self, new_size: Size) { + self.window_size = new_size; + self.dump_to_log("main_event_loop -> Resize"); + } - pub fn get_size(&self) -> Size { self.window_size } + pub fn get_size(&self) -> Size { self.window_size } - pub fn dump_to_log(&self, msg: &str) { - call_if_true!(DEBUG_TUI_MOD, tracing::info!("{msg} -> {self:?}")); - } + pub fn dump_to_log(&self, msg: &str) { + call_if_true!(DEBUG_TUI_MOD, tracing::info!("{msg} -> {self:?}")); } }