[ratatui] How to make a popup that has width proportional to window and height to fit content

Suppose you would like to display some word-wrapped text in a block in a popup. How can one setup a layout such that the popup's width is 50% of the window, but the popups height is calculated based on the height of the contents, ie the number of lines in the wrapped text at that particular width? Is such a thing possible?

We can’t help without knowing what framework/platform you are building this UI in.

1 Like

Apologies, in my late night haste I assumed this was the ratatui discourse. I am working with ratatui. I have updated the post title.

Maye you need this:

use ratatui::crossterm::event;
use ratatui::{
    Frame,
    layout::{Alignment, Constraint, Layout},
    style::{Color, Style},
    text::{Line, Text},
    widgets::{Block, Borders, Paragraph, Wrap},
};

const POPUP_WIDTH_PERCENTAGE: u16 = 50;
const TEXT_CONTENT: &str = "Ratatui is a Rust library for building terminal user interfaces. \
It provides a set of widgets and utilities for creating rich and interactive TUI applications. \
This text will automatically wrap to fit within the popup width.";

fn render_popup(frame: &mut Frame) {
    let terminal_area = frame.area();

    // Render some background content to show position relationship
    let background_text = vec![
        Line::from("Background Content - Line 1"),
        Line::from("Background Content - Line 2"),
        Line::from("Background Content - Line 3"),
        Line::from(""),
        Line::from("This is the main terminal content."),
        Line::from("The popup should appear centered above this text."),
        Line::from(""),
        Line::from("More background content..."),
    ]
    .into_iter()
    .cycle()
    .take(30)
    .collect::<Vec<_>>();

    let background = Paragraph::new(background_text).style(Style::new().fg(Color::Green));
    frame.render_widget(background, terminal_area);

    // Calculate popup dimensions based on content
    let popup_width = terminal_area.width * POPUP_WIDTH_PERCENTAGE / 100;
    let inner_width = popup_width.saturating_sub(2); // Account for borders

    // Estimate required height based on text content
    let text_lines = TEXT_CONTENT.len() as f32 / (inner_width as f32 * 0.8); // Rough estimation
    let estimated_height = text_lines.ceil() as u16 + 2; // Add borders
    let popup_height = estimated_height.min(terminal_area.height.saturating_sub(4)); // Limit to terminal height

    // Use ratatui's Layout system for proper centering (both horizontal and vertical)
    let vertical_layout = Layout::vertical([
        Constraint::Percentage((100 - popup_height * 100 / terminal_area.height) / 2),
        Constraint::Length(popup_height),
        Constraint::Percentage((100 - popup_height * 100 / terminal_area.height) / 2),
    ])
    .flex(ratatui::layout::Flex::Legacy)
    .split(terminal_area);

    let popup_area = Layout::horizontal([
        Constraint::Percentage((100 - POPUP_WIDTH_PERCENTAGE) / 2),
        Constraint::Percentage(POPUP_WIDTH_PERCENTAGE),
        Constraint::Percentage((100 - POPUP_WIDTH_PERCENTAGE) / 2),
    ])
    .flex(ratatui::layout::Flex::Legacy)
    .split(vertical_layout[1])[1];

    // Create the paragraph with automatic text wrapping
    let paragraph = Paragraph::new(Text::from(TEXT_CONTENT))
        .block(
            Block::bordered()
                .title(Line::from("Popup Title").centered())
                .borders(Borders::ALL)
                .border_style(Style::new().fg(Color::Yellow))
                .style(Style::new().bg(Color::Black).fg(Color::White)),
        )
        .alignment(Alignment::Left)
        .wrap(Wrap { trim: true });

    // Render the popup on top
    frame.render_widget(paragraph, popup_area);
}

fn main() -> std::io::Result<()> {
    let mut terminal = ratatui::init();
    let mut should_quit = false;

    while !should_quit {
        terminal.draw(|frame| {
            render_popup(frame);
        })?;

        // Check for quit input (Ctrl+C)
        if event::poll(std::time::Duration::from_millis(100))? {
            if let event::Event::Key(key) = event::read()? {
                if key.kind == event::KeyEventKind::Press && key.code == event::KeyCode::Char('q') {
                    should_quit = true;
                }
            }
        }
    }

    ratatui::restore();
    Ok(())
}

Show: