r3bl_tui 0.7.0

TUI library to build modern apps inspired by React, Elm, with Flexbox, CSS, editor component, emoji support, and more
Documentation
/*
 *   Copyright (c) 2022-2025 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, Formatter, Result},
          ops::{AddAssign, Deref, DerefMut}};

use super::TERMINAL_LIB_BACKEND;
use crate::{ok,
            CrosstermDebugFormatRenderOp,
            InlineString,
            InlineVec,
            LockedOutputDevice,
            PaintRenderOp,
            Pos,
            RenderOpImplCrossterm,
            Size,
            TerminalLibBackend,
            TuiColor,
            TuiStyle};

/// Here's an example. Refer to [RenderOps] for more details.
///
/// ```rust
/// use r3bl_tui::*;
///
/// let mut render_ops = render_ops!(
///   @new
///   RenderOp::ClearScreen, RenderOp::ResetColor
/// );
/// let len = render_ops.len();
/// let iter = render_ops.iter();
/// ```
#[macro_export]
macro_rules! render_ops {
  // Empty.
  () => {
    $crate::RenderOps::default()
  };

  // @new: Create a RenderOps. If any ($arg_render_op)* are passed, then add it to its list. Finally
  // return it.
  (
    @new
    $(                      /* Start a repetition. */
      $arg_render_op: expr  /* Expression. */
    )                       /* End repetition. */
    ,                       /* Comma separated. */
    *                       /* Zero or more times. */
    $(,)*                   /* Optional trailing comma https://stackoverflow.com/a/43143459/2085356. */
  ) => {
    /* Enclose the expansion in a block so that we can use multiple statements. */
    {
      let mut render_ops = $crate::RenderOps::default();
      /* Start a repetition. */
      $(
        /* Each repeat will contain the following statement, with $arg_render_op replaced. */
        render_ops.list.push($arg_render_op);
      )*
      render_ops
    }
  };

  // @add_to: If any ($arg_render_op)* are passed, then add to it.
  (
    @add_to
    $arg_render_ops: expr
    =>
    $(                      /* Start a repetition. */
      $arg_render_op: expr  /* Expression. */
    )                       /* End repetition. */
    ,                       /* Comma separated. */
    *                       /* Zero or more times. */
    $(,)*                   /* Optional trailing comma https://stackoverflow.com/a/43143459/2085356. */
  ) => {
    /* Enclose the expansion in a block so that we can use multiple statements. */
    {
      /* Start a repetition. */
      $(
        /* Each repeat will contain the following statement, with $arg_render_op replaced. */
        $arg_render_ops.list.push($arg_render_op);
      )*
    }
  };

  // @render_into: If any ($arg_render_op)* are passed, then add to it.
  (
    @render_styled_texts_into
    $arg_render_ops: expr
    =>
    $(                       /* Start a repetition. */
      $arg_styled_texts: expr/* Expression. */
    )                        /* End repetition. */
    ,                        /* Comma separated. */
    *                        /* Zero or more times. */
    $(,)*                    /* Optional trailing comma https://stackoverflow.com/a/43143459/2085356. */
  ) => {
    /* Enclose the expansion in a block so that we can use multiple statements. */
    {
      /* Start a repetition. */
      $(
        /* Each repeat will contain the following statement, with $arg_render_op replaced. */
        $crate::render_tui_styled_texts_into(&$arg_styled_texts, &mut $arg_render_ops);
      )*
    }
  };
}

/// For ease of use, please use the [render_ops!] macro.
///
/// It is a collection of *atomic* paint operations (aka [`RenderOps`] at various
/// [`super::ZOrder`]s); each [`RenderOps`] is made up of a [vec] of [`RenderOp`]. It
/// contains `Map<ZOrder, Vec<RenderOps>>`, eg:
/// - [`super::ZOrder::Normal`] => vec![[`RenderOp::ResetColor`],
///   [`RenderOp::MoveCursorPositionAbs`], [`RenderOp::PaintTextWithAttributes`]]
/// - [`super::ZOrder::Glass`] => vec![[`RenderOp::ResetColor`],
///   [`RenderOp::MoveCursorPositionAbs`], [`RenderOp::PaintTextWithAttributes`]]
/// - etc.
///
/// # What is an atomic paint operation?
/// 1. It moves the cursor using:
///     1. [`RenderOp::MoveCursorPositionAbs`]
///     2. [`RenderOp::MoveCursorPositionRelTo`]
/// 2. And it does not assume that the cursor is in the correct position from some other
///    previously executed operation!
/// 3. So there are no side effects when re-ordering or omitting painting an atomic paint
///    operation (eg in the case where it has already been painted before).
///
/// Here's an example. Consider using the macro for convenience (see [render_ops!]).
///
/// ```rust
/// use r3bl_tui::*;
///
/// let mut render_ops = RenderOps::default();
/// render_ops.push(RenderOp::ClearScreen);
/// render_ops.push(RenderOp::ResetColor);
/// let len = render_ops.len();
/// let iter = render_ops.iter();
/// ```
///
/// # Paint optimization
/// Due to the compositor [super::OffscreenBuffer], there is no need to optimize the
/// individual paint operations. You don't have to manage your own whitespace or doing
/// clear before paint! 🎉 The compositor takes care of that for you!
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct RenderOps {
    pub list: InlineVec<RenderOp>,
}

impl Default for RenderOps {
    fn default() -> Self {
        Self {
            list: InlineVec::new(),
        }
    }
}

#[derive(Default, Debug)]
pub struct RenderOpsLocalData {
    pub cursor_pos: Pos,
}

pub mod render_ops_impl {
    use super::*;

    impl RenderOps {
        pub fn execute_all(
            &self,
            skip_flush: &mut bool,
            window_size: Size,
            locked_output_device: LockedOutputDevice<'_>,
            is_mock: bool,
        ) {
            let mut local_data = RenderOpsLocalData::default();
            for render_op in self.list.iter() {
                RenderOps::route_paint_render_op_to_backend(
                    &mut local_data,
                    skip_flush,
                    render_op,
                    window_size,
                    locked_output_device,
                    is_mock,
                );
            }
        }

        pub fn route_paint_render_op_to_backend(
            local_data: &mut RenderOpsLocalData,
            skip_flush: &mut bool,
            render_op: &RenderOp,
            window_size: Size,
            locked_output_device: LockedOutputDevice<'_>,
            is_mock: bool,
        ) {
            match TERMINAL_LIB_BACKEND {
                TerminalLibBackend::Crossterm => {
                    RenderOpImplCrossterm {}.paint(
                        skip_flush,
                        render_op,
                        window_size,
                        local_data,
                        locked_output_device,
                        is_mock,
                    );
                }
                TerminalLibBackend::Termion => unimplemented!(),
            }
        }
    }

    impl Deref for RenderOps {
        type Target = InlineVec<RenderOp>;

        fn deref(&self) -> &Self::Target { &self.list }
    }

    impl DerefMut for RenderOps {
        fn deref_mut(&mut self) -> &mut Self::Target { &mut self.list }
    }

    impl AddAssign<RenderOp> for RenderOps {
        fn add_assign(&mut self, rhs: RenderOp) { self.list.push(rhs); }
    }

    impl Debug for RenderOps {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let mut iter = self.iter();

            _ = write!(f, "RenderOps.len(): {}", self.list.len());

            const DELIM: &str = "\n  - ";

            // First line.
            if let Some(first) = iter.next() {
                _ = write!(f, "[{first:?}]");
            }

            // Subsequent lines.
            for item in iter {
                _ = write!(f, "{DELIM}[{item:?}]");
            }

            ok!()
        }
    }
}

#[derive(Clone, PartialEq, Eq, Hash)]
pub enum RenderOp {
    EnterRawMode,

    ExitRawMode,

    /// This is always painted on top. [Pos] is the absolute column and row on the
    /// terminal screen. This uses [super::sanitize_and_save_abs_pos] to clean up the
    /// given [Pos].
    MoveCursorPositionAbs(/* absolute position */ Pos),

    /// This is always painted on top. 1st [Pos] is the origin column and row, and the
    /// 2nd [Pos] is the offset column and row. They are added together to move the
    /// absolute position on the terminal screen. Then
    /// [RenderOp::MoveCursorPositionAbs] is used.
    MoveCursorPositionRelTo(
        /* origin position */ Pos,
        /* relative position */ Pos,
    ),

    ClearScreen,

    /// Directly set the fg color for crossterm w/out using [TuiStyle].
    SetFgColor(TuiColor),

    /// Directly set the bg color for crossterm w/out using [TuiStyle].
    SetBgColor(TuiColor),

    ResetColor,

    /// Translate [TuiStyle] into fg and bg colors for crossterm. Note that this does not
    /// apply attributes (bold, italic, underline, strikethrough, etc). If you need to
    /// apply attributes, use [RenderOp::PaintTextWithAttributes] instead.
    ApplyColors(Option<TuiStyle>),

    /// Translate [TuiStyle] into *only* attributes for crossterm (bold, italic,
    /// underline, strikethrough, etc) and not colors. If you need to apply color, use
    /// [RenderOp::ApplyColors] instead.
    ///
    /// 1. If the [InlineString] argument is plain text (no ANSI sequences) then it will
    ///    be clipped available width of the terminal screen).
    ///
    /// 2. If the [InlineString] argument contains ANSI sequences then it will be printed
    ///    as-is. You are responsible for handling clipping of the text to the bounds of
    ///    the terminal screen.
    PaintTextWithAttributes(InlineString, Option<TuiStyle>),

    /// This is **not** meant for use directly by apps. It is to be used only by the
    /// [super::OffscreenBuffer]. This operation skips the checks for content width
    /// padding & clipping, and window bounds clipping. These are not needed when the
    /// compositor is painting an offscreen buffer, since when the offscreen buffer was
    /// created the two render ops above were used which already handle the clipping and
    /// padding.
    CompositorNoClipTruncPaintTextWithAttributes(InlineString, Option<TuiStyle>),

    /// For [Default] impl.
    Noop,
}

mod render_op_impl {
    use super::*;

    impl Default for RenderOp {
        fn default() -> Self { Self::Noop }
    }

    impl Debug for RenderOp {
        /// When [crate::RenderPipeline] is printed as debug, each [RenderOp] is printed
        /// using this method. Also [crate::queue_render_op!] does not use this; it has
        /// its own way of logging output.
        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
            match TERMINAL_LIB_BACKEND {
                TerminalLibBackend::Crossterm => {
                    CrosstermDebugFormatRenderOp {}.fmt_debug(self, f)
                }
                TerminalLibBackend::Termion => unimplemented!(),
            }
        }
    }
}

mod render_op_impl_trait_flush {
    use super::*;

    impl Flush for RenderOp {
        fn flush(&mut self, locked_output_device: LockedOutputDevice<'_>) {
            match TERMINAL_LIB_BACKEND {
                TerminalLibBackend::Crossterm => {
                    RenderOpImplCrossterm {}.flush(locked_output_device);
                }
                TerminalLibBackend::Termion => unimplemented!(),
            }
        }

        fn clear_before_flush(&mut self, locked_output_device: LockedOutputDevice<'_>) {
            match TERMINAL_LIB_BACKEND {
                TerminalLibBackend::Crossterm => {
                    RenderOpImplCrossterm {}.clear_before_flush(locked_output_device);
                }
                TerminalLibBackend::Termion => unimplemented!(),
            }
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum FlushKind {
    JustFlush,
    ClearBeforeFlush,
}

pub trait Flush {
    fn flush(&mut self, locked_output_device: LockedOutputDevice<'_>);

    fn clear_before_flush(&mut self, locked_output_device: LockedOutputDevice<'_>);
}

pub trait DebugFormatRenderOp {
    fn fmt_debug(&self, this: &RenderOp, f: &mut Formatter<'_>) -> Result;
}