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 crate::app::context::{SurfaceMode, ViewportMode};
use crate::app::input::convert::translate_mouse_to_viewport;
use crate::core::event::MouseEvent;
use crate::style::Rect;

use super::ViewportMetrics;

#[derive(Default)]
pub(crate) struct InlineSurfaceState {
    pub(crate) viewport_metrics: ViewportMetrics,
    pub(crate) inline_cursor_offset: u16,
    pub(crate) last_terminal_size: (u16, u16),
    pub(crate) transcript_expanded: bool,
    pub(crate) transcript_reset_pending: bool,
    pub(crate) expanded_live_viewport_height: u16,
}

pub(crate) struct SurfaceDriver {
    mode: SurfaceMode,
    pub(crate) inline: InlineSurfaceState,
}

impl SurfaceDriver {
    pub(crate) fn new(mode: SurfaceMode) -> Self {
        let normalized = mode.normalized();
        Self {
            mode: normalized,
            inline: InlineSurfaceState::default(),
        }
    }

    pub(crate) fn mode(&self) -> SurfaceMode {
        self.mode
    }

    pub(crate) fn is_inline(&self) -> bool {
        self.mode.is_inline()
    }

    pub(crate) fn is_transcript(&self) -> bool {
        matches!(self.mode, SurfaceMode::InlineTranscript { .. })
    }

    pub(crate) fn content_bounds(&self, width: u16, height: u16) -> Rect {
        match self.mode {
            SurfaceMode::Fullscreen => Rect {
                x: 0,
                y: 0,
                w: width,
                h: height,
            },
            SurfaceMode::InlineTranscript {
                height: inline_height,
                ..
            } => {
                if self.inline.transcript_expanded {
                    let layout_h = if self.inline.expanded_live_viewport_height > 0 {
                        self.inline.expanded_live_viewport_height.min(height).max(1)
                    } else {
                        inline_height.min(height).max(1)
                    };
                    Rect {
                        x: 0,
                        y: 0,
                        w: width.saturating_sub(1).max(1),
                        h: layout_h,
                    }
                } else {
                    Rect {
                        x: 0,
                        y: 0,
                        w: width.saturating_sub(1).max(1),
                        h: inline_height.min(height).max(1),
                    }
                }
            }
            SurfaceMode::InlineEphemeral {
                height: inline_height,
            } => Rect {
                x: 0,
                y: 0,
                w: width.saturating_sub(1).max(1),
                h: inline_height.min(height).max(1),
            },
        }
    }

    pub(crate) fn convert_mouse_event(
        &self,
        mouse: MouseEvent,
        viewport_metrics: ViewportMetrics,
    ) -> Option<MouseEvent> {
        match self.mode.viewport_mode() {
            ViewportMode::Fullscreen => Some(mouse),
            ViewportMode::Inline { .. } => translate_mouse_to_viewport(
                mouse,
                viewport_metrics.x,
                viewport_metrics.y,
                viewport_metrics.width,
                viewport_metrics.height,
            ),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::SurfaceDriver;
    use crate::app::context::{InlineStartupPolicy, SurfaceMode};
    use crate::core::event::{KeyMods, MouseEvent, MouseKind};
    use crate::style::Rect;

    #[test]
    fn inline_surface_state_single_owner() {
        let driver = SurfaceDriver::new(SurfaceMode::InlineEphemeral { height: 3 });
        assert_eq!(driver.inline.inline_cursor_offset, 0);
        assert_eq!(driver.inline.last_terminal_size, (0, 0));
        assert!(!driver.inline.transcript_expanded);
        assert!(!driver.inline.transcript_reset_pending);
        assert_eq!(driver.inline.expanded_live_viewport_height, 0);
        assert_eq!(driver.inline.viewport_metrics.width, 0);
        assert_eq!(driver.inline.viewport_metrics.height, 0);
    }

    #[test]
    fn inline_surface_driver_routes_runner_state() {
        let fullscreen = SurfaceDriver::new(SurfaceMode::Fullscreen);
        let inline = SurfaceDriver::new(SurfaceMode::InlineTranscript {
            height: 4,
            startup: InlineStartupPolicy::PreserveHost,
        });

        assert!(!fullscreen.is_inline());
        assert_eq!(
            fullscreen.content_bounds(80, 24),
            Rect {
                x: 0,
                y: 0,
                w: 80,
                h: 24,
            }
        );

        assert!(inline.is_inline());
        assert_eq!(
            inline.content_bounds(80, 24),
            Rect {
                x: 0,
                y: 0,
                w: 79,
                h: 4,
            }
        );
        assert_eq!(
            inline.content_bounds(1, 1),
            Rect {
                x: 0,
                y: 0,
                w: 1,
                h: 1,
            }
        );

        let mouse = MouseEvent {
            x: 2,
            y: 5,
            kind: MouseKind::Moved,
            mods: KeyMods::default(),
        };
        assert_eq!(
            inline.convert_mouse_event(
                mouse,
                crate::app::runner::ViewportMetrics {
                    x: 0,
                    y: 4,
                    width: 80,
                    height: 4,
                },
            ),
            Some(MouseEvent {
                x: 2,
                y: 1,
                kind: MouseKind::Moved,
                mods: KeyMods::default(),
            }),
        );
    }

    #[test]
    fn inline_transcript_expanded_uses_live_viewport_height_not_screen() {
        let mut driver = SurfaceDriver::new(SurfaceMode::InlineTranscript {
            height: 4,
            startup: InlineStartupPolicy::PreserveHost,
        });
        driver.inline.transcript_expanded = true;
        assert_eq!(
            driver.content_bounds(80, 24),
            Rect {
                x: 0,
                y: 0,
                w: 79,
                h: 4,
            }
        );
        driver.inline.expanded_live_viewport_height = 14;
        assert_eq!(
            driver.content_bounds(80, 24),
            Rect {
                x: 0,
                y: 0,
                w: 79,
                h: 14,
            }
        );
    }
}