tui-lipan 0.1.0

Opinionated, component-based TUI framework for Rust - declarative components, reconciliation, layout engine, focus, overlays, and rich widgets on top of ratatui.
Documentation
use ratatui::buffer::Cell as BufferCell;
use ratatui::style::Color as RColor;
use ratatui::widgets::Block;

use crate::backend::ratatui_backend::common::{
    apply_effect_style_clipped, from_ratatui_color, paint_to_ratatui_bg, to_ratatui_rect,
};
use crate::core::node::NodeKind;
use crate::style::{ColorTransform, Rect, Style};

use super::RenderState;

#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum OverlayClearRestoreMode {
    PreserveForeground,
    PreserveBackgroundOnly,
}

pub(super) fn scale_transform_for_opacity(
    transform: ColorTransform,
    overlay_opacity: f32,
) -> ColorTransform {
    let overlay_opacity = overlay_opacity.clamp(0.0, 1.0);
    match transform {
        ColorTransform::Dim(amount) => ColorTransform::Dim(amount * overlay_opacity),
        ColorTransform::Lighten(amount) => ColorTransform::Lighten(amount * overlay_opacity),
        ColorTransform::Opacity(opacity) => {
            let washout = (1.0 - opacity).clamp(0.0, 1.0) * overlay_opacity;
            ColorTransform::Opacity(1.0 - washout)
        }
        ColorTransform::OpacityToward { factor, target } => {
            let washout = (1.0 - factor).clamp(0.0, 1.0) * overlay_opacity;
            ColorTransform::OpacityToward {
                factor: 1.0 - washout,
                target,
            }
        }
        ColorTransform::Tint(color, alpha) => ColorTransform::Tint(color, alpha * overlay_opacity),
    }
}

pub(crate) fn clip_overlay_clear_rect(
    content_rect: Rect,
    overlay_rect: Rect,
) -> ratatui::layout::Rect {
    let absolute_overlay_rect = Rect {
        x: content_rect.x.saturating_add(overlay_rect.x),
        y: content_rect.y.saturating_add(overlay_rect.y),
        w: overlay_rect.w,
        h: overlay_rect.h,
    };
    to_ratatui_rect(absolute_overlay_rect.intersection(&content_rect))
}

pub(crate) fn render_overlay_backdrop(
    state: &mut RenderState<'_, '_, '_>,
    content_rect: Rect,
    style: Style,
    overlay_opacity: f32,
) {
    if style.is_empty() || overlay_opacity <= 0.0 {
        return;
    }

    if overlay_opacity >= 1.0 {
        if let Some(bg) = style.bg
            && let Some(bg) = paint_to_ratatui_bg(bg, state.ctx.terminal_bg.map(from_ratatui_color))
        {
            let block = Block::default().style(ratatui::style::Style::default().bg(bg));
            state.f.render_widget(block, state.content);
        }
        apply_effect_style_clipped(state.f, content_rect, style, None, state.ctx.terminal_bg);
        return;
    }

    if let Some(bg) = style.bg
        && !bg.is_transparent_paint()
        && !bg.is_backdrop_sentinel()
    {
        apply_effect_style_clipped(
            state.f,
            content_rect,
            Style::new().transform_bg(ColorTransform::Tint(bg.color(), overlay_opacity)),
            None,
            state.ctx.terminal_bg,
        );
    }
    if let Some(fg) = style.fg
        && !fg.is_transparent_paint()
        && !fg.is_backdrop_sentinel()
    {
        apply_effect_style_clipped(
            state.f,
            content_rect,
            Style::new().transform_fg(ColorTransform::Tint(fg.color(), overlay_opacity)),
            None,
            state.ctx.terminal_bg,
        );
    }

    let mut effect_style = style;
    effect_style.fg = None;
    effect_style.bg = None;
    effect_style.fg_transform = effect_style
        .fg_transform
        .map(|transform| scale_transform_for_opacity(transform, overlay_opacity));
    effect_style.bg_transform = effect_style
        .bg_transform
        .map(|transform| scale_transform_for_opacity(transform, overlay_opacity));
    effect_style.dim_amount = effect_style
        .dim_amount
        .map(|amount| amount * overlay_opacity);
    effect_style.tint = effect_style
        .tint
        .map(|(color, alpha)| (color, alpha * overlay_opacity));
    apply_effect_style_clipped(
        state.f,
        content_rect,
        effect_style,
        None,
        state.ctx.terminal_bg,
    );
}
pub(crate) fn is_clear_equivalent(cell: &BufferCell) -> bool {
    cell.symbol() == " "
        && cell.bg == RColor::Reset
        && cell.underline_color == RColor::Reset
        && cell.modifier.is_empty()
}

pub(crate) struct AnimatedRestoreSnapshot {
    rect: ratatui::layout::Rect,
    cells: Vec<BufferCell>,
}

impl AnimatedRestoreSnapshot {
    pub(crate) fn cell_at(&self, x: u16, y: u16) -> Option<&BufferCell> {
        if x < self.rect.x
            || y < self.rect.y
            || x >= self.rect.x.saturating_add(self.rect.width)
            || y >= self.rect.y.saturating_add(self.rect.height)
        {
            return None;
        }

        let dx = x.saturating_sub(self.rect.x) as usize;
        let dy = y.saturating_sub(self.rect.y) as usize;
        let index = dy
            .saturating_mul(self.rect.width as usize)
            .saturating_add(dx);
        self.cells.get(index)
    }
}

pub(crate) fn snapshot_animated_restore_rect(
    f: &mut ratatui::Frame<'_>,
    rect: Rect,
    clip_rect: Option<Rect>,
) -> Option<AnimatedRestoreSnapshot> {
    let mut draw_rect = rect;
    if let Some(clip) = clip_rect {
        draw_rect = draw_rect.intersection(&clip);
    }
    if draw_rect.is_empty() {
        return None;
    }

    let r_rect = to_ratatui_rect(draw_rect);
    let intersection = f.area().intersection(r_rect);
    if intersection.width == 0 || intersection.height == 0 {
        return None;
    }

    let buf = f.buffer_mut();
    let mut cells = Vec::with_capacity(intersection.width as usize * intersection.height as usize);
    for y in intersection.y..intersection.y + intersection.height {
        for x in intersection.x..intersection.x + intersection.width {
            cells.push(buf.cell((x, y)).cloned().unwrap_or(BufferCell::EMPTY));
        }
    }

    Some(AnimatedRestoreSnapshot {
        rect: intersection,
        cells,
    })
}

pub(crate) fn restore_fully_transparent_animated(
    f: &mut ratatui::Frame<'_>,
    snapshot: AnimatedRestoreSnapshot,
    fg_only: bool,
) {
    let buf = f.buffer_mut();
    for dy in 0..snapshot.rect.height {
        for dx in 0..snapshot.rect.width {
            let index = dy as usize * snapshot.rect.width as usize + dx as usize;
            let saved = &snapshot.cells[index];
            let x = snapshot.rect.x + dx;
            let y = snapshot.rect.y + dy;
            let Some(cell) = buf.cell_mut((x, y)) else {
                continue;
            };

            if fg_only {
                let rendered_bg = cell.bg;
                *cell = saved.clone();
                if rendered_bg != RColor::Reset {
                    cell.bg = rendered_bg;
                }
            } else {
                *cell = saved.clone();
            }
        }
    }
}

pub(crate) fn overlay_clear_restore_mode(
    node: &crate::core::node::Node,
) -> OverlayClearRestoreMode {
    let bg = match &node.kind {
        NodeKind::Frame(frame) => frame.style.bg,
        NodeKind::Center(center) => center.style.bg,
        NodeKind::CenterPin(center) => center.style.bg,
        NodeKind::StatusBarLayout(layout) => layout.style.bg,
        NodeKind::ZStack(stack) => stack.style.bg,
        NodeKind::VStack(stack) => stack.props.style.bg,
        NodeKind::HStack(stack) => stack.props.style.bg,
        NodeKind::Grid(grid) => grid.props.style.bg,
        NodeKind::Flow(flow) => flow.style.bg,
        NodeKind::Animated(_) => None,
        _ => None,
    };

    if matches!(bg, Some(paint) if paint.is_transparent_sentinel()) {
        OverlayClearRestoreMode::PreserveForeground
    } else {
        OverlayClearRestoreMode::PreserveBackgroundOnly
    }
}