Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table truncates long strings #972

Open
theqbz opened this issue Dec 19, 2024 · 2 comments
Open

Table truncates long strings #972

theqbz opened this issue Dec 19, 2024 · 2 comments

Comments

@theqbz
Copy link

theqbz commented Dec 19, 2024

Sometimes I want to display a very long string in an ftxui::Table cell. In such a case, the table reaches the maximum possible size - for example, the edge of the screen - but the long string does not form a line break, even if I store it in a ftxui::paragraph instead of text. The end of the string is simply cut off. Can you suggest a method for how such long strings could be displayed properly in an ftxui::Table?

I think it is possible that perhaps the ftxui::paragraph does not know the boundaries within the table, within which it can extend, so the line break does not apply. If so, how can I pass this information to the paragraph?

Actual result:

First column  The Very Long String
───────────────────────────────────────────────────────────────────────────────────
something     012345678901234567890123456789012345678901234567890123456789012345678

Expected result:

First column  The Very Long String
───────────────────────────────────────────────────────────────────────────────────
something     012345678901234567890123456789012345678901234567890123456789012345678
              90123456789

Sorry for this poor illustration, I hope it helps you understand my idea.

I use ftxui::Table through a wrapper class, in two ways:

  • No interactive screen loop is running, just displaying data in the terminal. (using MyTable::print())
  • Interactive screen loop is running and I embed the Table as an ftxui::Element in an ftxui::hbox. (using MyTable::getRenderer())
// In MyTable.cpp
//
// Member variables used in the following functions:
// m_table is an ftxui::Table
// m_tableContent is a std::vector<ftxui::Elements>

void MyTable::setTableContent(const std::vector<std::vector<std::string>>& p_tableContent)
{
    m_tableContent.clear();
    std::vector<ftxui::Elements> table {};
    for (const std::vector<std::string>& row : p_tableContent) {
        ftxui::Elements tableRow {};
        for (const std::string& cell : row) {
            tableRow.push_back(ftxui::hbox(ftxui::paragraph(cell)));
        }
        m_tableContent.push_back(tableRow);
    }
}

void MyTable::print()
{
    if (m_tableContent.empty()) {
        return;
    }
    m_table = m_tableContent;
    m_table.SelectAll().SeparatorVertical(ftxui::EMPTY);
    m_table.SelectRow(0).BorderBottom(ftxui::LIGHT);
    ftxui::Element table { m_table.Render() };
    ftxui::Screen screen { ftxui::Screen::Create(ftxui::Dimension::Fit(table)) };
    ftxui::Render(screen, table);
    screen.Print();
}

ftxui::Element MyTable::getRenderer()
{
    if (!m_tableContent.empty()) {
        m_table = m_tableContent;
        m_table.SelectAll().SeparatorVertical(ftxui::LIGHT);
        m_table.SelectRow(0).BorderBottom(ftxui::LIGHT);
        m_table.SelectRow(0).DecorateCells(ftxui::bold);
    }
    return m_table.Render();
}
@ArthurSonzogni
Copy link
Owner

Hello @theqbz,

See the paragraph implementation using flexbox:

namespace {
Elements Split(const std::string& the_text) {
  Elements output;
  std::stringstream ss(the_text);
  std::string word;
  while (std::getline(ss, word, ' ')) {
    output.push_back(text(word));
  }
  return output;
}
}  // namespace

/// @brief Return an element drawing the paragraph on multiple lines.
/// @ingroup dom
/// @see flexbox.
Element paragraph(const std::string& the_text) {
  return paragraphAlignLeft(the_text);
}

/// @brief Return an element drawing the paragraph on multiple lines, aligned on
/// the left.
/// @ingroup dom
/// @see flexbox.
Element paragraphAlignLeft(const std::string& the_text) {
  static const auto config = FlexboxConfig().SetGap(1, 0);
  return flexbox(Split(the_text), config);
}

You see we split the text into words, and put each word into a flexbox. As a result, you can't split elsewhere than inside a space.

What you can try:

Element SplitableText(const std::string& the_text) {
  // Split every ~8 characters.
  Elements miniwords;
  for (size_t i = 0; i < the_text.size(); i += 8) {
    miniwords.push_back(text(the_text.substr(i, 8)));
  }
  return flexbox(std::move(miniwords));
}

I hope this will be working for you, but note that flexbox doesn't work flawlessly.
Note that flexbox is not working flawlessly. I hope

@theqbz
Copy link
Author

theqbz commented Jan 7, 2025

Thank you very much for your answer!

You helped me a lot to understand the operation of ftxui more precisely and based on this I was able to write a solution that flexibly adapts to the full width of the table and converts the contents of the cells into multiple rows if necessary at runtime.

I'll include my solution here, which is not perfect, but may provide a good starting point for those who face a similar problem.

//----------
// in MyTable class declaration:
// 
// ftxui::Table                          m_table;
// static ftxui::FlexboxConfig           m_flexboxConfig;
// std::vector<std::vector<std::string>> m_stringTable;
// std::vector<unsigned int>             m_columnWidths;
// 
// using ElementTableRow = ftxui::Elements;
// using ElementTable    = std::vector<ElementTableRow>;
// 
// enum class ColumnSeparators : bool
// {
//     INCLUDED,
//     NOT_INCLUDED
// };
//

//------------
// MyTable.cpp
//

/// There is obviously a better solution than this constrainTo function implementation!
template <typename TOutput, typename TInput>
    requires std::is_integral_v<TOutput> && std::is_integral_v<TInput>
inline static const TOutput constrainTo(TInput p_value)
{
    using CommonType            = std::common_type_t<TOutput, TInput>;
    constexpr CommonType outMin = static_cast<CommonType>(std::numeric_limits<TOutput>::min());
    constexpr CommonType outMax = static_cast<CommonType>(std::numeric_limits<TOutput>::max());
    if (p_value < outMin) {
        return static_cast<TOutput>(outMin);
    } else if (p_value > outMax) {
        return static_cast<TOutput>(outMax);
    }
    return static_cast<TOutput>(p_value);
}

const ftxui::FlexboxConfig MyTable::m_flexboxConfig {
    ftxui::FlexboxConfig()
        .Set(ftxui::FlexboxConfig::Direction::Column)
        .Set(ftxui::FlexboxConfig::Wrap::Wrap)
        .Set(ftxui::FlexboxConfig::JustifyContent::FlexStart)
        .Set(ftxui::FlexboxConfig::AlignItems::FlexStart)
        .Set(ftxui::FlexboxConfig::AlignContent::FlexStart)
};

void MyTable::computeTheSpaceRequirementOfColumns()
{
    std::vector<unsigned int> largestColumnWidths {};
    for (const Result::Row& row : m_stringTable) {
        for (unsigned int colIdx = 0U; colIdx < row.size(); ++colIdx) {
            const unsigned int cellWidth { constrainTo<unsigned int>(row[colIdx].length()) };
            if (colIdx >= largestColumnWidths.size()) {
                largestColumnWidths.push_back(cellWidth);
            } else if (cellWidth > largestColumnWidths[colIdx]) {
                largestColumnWidths[colIdx] = cellWidth;
            }
        }
    }
    m_columnWidths = largestColumnWidths;
}

void MyTable::setTableContent(const std::vector<std::vector<std::string>>& p_tableContent)
{
    m_stringTable.assign(p_tableContent.begin(), p_tableContent.end());
    computeTheSpaceRequirementOfColumns();
}

static const unsigned int computeWrapPos(const std::string& p_content, const unsigned int& p_lastWrapPos, const unsigned int& p_cellWidthLimit)
{
    const unsigned int length = constrainTo<unsigned int>(p_content.length());
    if (length <= p_lastWrapPos || length <= p_cellWidthLimit) {
        return length;
    }
    const unsigned int remainingLength = length - p_lastWrapPos;
    if (remainingLength < p_cellWidthLimit) {
        return length;
    }
    const unsigned int searchPos         = p_lastWrapPos + p_cellWidthLimit;
    const size_t lastSpacePosWithinLimit = p_content.rfind(' ', searchPos);
    const bool noSpaceInTheCurrentPart   = lastSpacePosWithinLimit == std::string::npos || lastSpacePosWithinLimit <= static_cast<size_t>(p_lastWrapPos);
    const unsigned int wrapPos           = noSpaceInTheCurrentPart ? searchPos : constrainTo<unsigned int>(lastSpacePosWithinLimit);
    return wrapPos;
}

/// createCell function splits the incoming text into smaller units, if it is longer
/// than the specified cell width limit. The splitting is done with respect to
/// word boundaries, but if a word is longer than the limit, it will be
/// truncated at the limit. Finally, it puts the text pieces into a flexbox
/// element, which creates a single table cell.
const ftxui::Element MyTable::createCell(const std::string& p_content, const unsigned int& p_cellWidthLimit) const
{
    if (p_cellWidthLimit < 1U) {
        return ftxui::emptyElement();
    }
    ftxui::Elements result {};
    unsigned int lastWrapPos { 0U };
    while (lastWrapPos < p_content.length()) {
        const unsigned int wrapPos { computeWrapPos(p_content, lastWrapPos, p_cellWidthLimit) };
        if (wrapPos <= lastWrapPos) {
            break;
        }
        const unsigned int wrapLength { wrapPos - lastWrapPos };
        const std::string chunk { p_content.substr(lastWrapPos, wrapLength) };
        if (!chunk.empty()) {
            result.push_back(ftxui::text(chunk));
        }
        lastWrapPos = wrapPos; // I do not want to move past the current wrap position.
    }
    return ftxui::flexbox(result, m_flexboxConfig);
}

/// computeCellWidthLimit function calculates the maximum width that each column can have,
/// based on the specified maximum table width. It takes into account the
/// width of the column separators if they are included.
const unsigned int MyTable::computeCellWidthLimit(const unsigned int& p_maxTableWidth, const ColumnSeparators p_columnSeparators) const
{
    if (m_columnWidths.empty()) {
        return 0U;
    }
    const unsigned int columnCount    = constrainTo<unsigned int>(m_columnWidths.size());
    const unsigned int separatorWidth = p_columnSeparators == ColumnSeparators::INCLUDED ? (columnCount - 1U) : 0U;
    if (p_maxTableWidth < (columnCount + separatorWidth)) {
        return 0U;
    }
    const std::multiset<unsigned int> columnWidths(m_columnWidths.begin(), m_columnWidths.end());
    const unsigned int availableSpace = p_maxTableWidth - separatorWidth;
    const unsigned int upperBound     = availableSpace / columnCount;
    const std::multiset<unsigned int>::const_iterator firstOutOfBound { columnWidths.upper_bound(upperBound) };
    if (firstOutOfBound == columnWidths.end()) {
        return *columnWidths.crbegin();
    }
    const unsigned int widthBelowBound = std::accumulate(columnWidths.begin(), firstOutOfBound, 0U);
    if (availableSpace <= widthBelowBound) {
        return upperBound;
    }
    const unsigned int remainingWidth   = availableSpace - widthBelowBound;
    const unsigned int remainingColumns = constrainTo<unsigned int>(std::distance(firstOutOfBound, columnWidths.end()));
    const unsigned int columnWidthLimit = remainingWidth / remainingColumns;
    return columnWidthLimit;
}

const MyTable::ElementTable MyTable::createTable(const unsigned int& p_maxTableWidth, const ColumnSeparators p_columnSeparators) const
{
    if (m_stringTable.empty()) {
        return { { ftxui::emptyElement() } };
    }
    const unsigned int cellWidthLimit { computeCellWidthLimit(p_maxTableWidth, p_columnSeparators) };
    if (cellWidthLimit < 1U) {
        return { { ftxui::emptyElement() } };
    }
    MyTable::ElementTable result {};
    for (const Result::Row& row : m_stringTable) {
        MyTable::ElementTableRow tableRow {};
        for (const std::string& cell : row) {
            tableRow.push_back(ftxui::hbox(createCell(cell, cellWidthLimit), ftxui::filler()));
        }
        result.push_back(tableRow);
    }
    return result;
}

void MyTable::createNormalTable(const MyApp::data::Dimensions& p_dimensions)
{
    if (!m_stringTable.empty()) {
        m_table = createTable(p_dimensions.getWidth(), ColumnSeparators::INCLUDED);
        m_table.SelectAll().SeparatorVertical(ftxui::LIGHT);
        m_table.SelectRow(0).BorderBottom(ftxui::LIGHT);
        m_table.SelectRow(0).DecorateCells(ftxui::bold);
    }
}

ftxui::Element MyTable::getRenderer(const MyApp::data::Dimensions& p_dimensions)
{
    createNormalTable(p_dimensions);
    return m_table.Render();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants