1
0
Fork 0

Change: Reflow Textfile window content incrementally.

This avoids a stall when reflowing a long text file with some truetype fonts.
pull/14293/head
Peter Nelson 2025-05-14 18:06:51 +01:00 committed by Peter Nelson
parent 46b745a06a
commit 7344dfe651
3 changed files with 204 additions and 83 deletions

View File

@ -105,40 +105,25 @@ void TextfileWindow::ConstructWindow()
}
/**
* Get the total height of the content displayed in this window, if wrapping is disabled.
* @return the height in pixels
* Reset the reflow process to start on the next UI tick.
*/
uint TextfileWindow::ReflowContent()
void TextfileWindow::ReflowContent()
{
uint height = 0;
if (!this->IsTextWrapped()) {
for (auto &line : this->lines) {
line.top = height;
height++;
line.bottom = height;
}
} else {
int max_width = this->GetWidget<NWidgetCore>(WID_TF_BACKGROUND)->current_x - WidgetDimensions::scaled.frametext.Horizontal();
for (auto &line : this->lines) {
line.top = height;
height += GetStringHeight(line.text, max_width, FS_MONO) / GetCharacterHeight(FS_MONO);
line.bottom = height;
}
}
/* Minimum number of lines that will be flowed. */
if (this->num_lines == 0) this->num_lines = std::size(this->lines);
return height;
}
auto it = this->GetIteratorFromPosition(this->vscroll->GetPosition());
uint TextfileWindow::GetContentHeight()
{
if (this->lines.empty()) return 0;
return this->lines.back().bottom;
auto adapter = AlternatingView{this->lines, it};
this->reflow_iter = adapter.begin();
this->reflow_end = adapter.end();
}
/* virtual */ void TextfileWindow::UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize)
{
switch (widget) {
case WID_TF_BACKGROUND:
resize.width = GetCharacterHeight(FS_MONO); // Width is not available here as the font may not be loaded yet.
resize.height = GetCharacterHeight(FS_MONO);
size.height = 4 * resize.height + WidgetDimensions::scaled.frametext.Vertical(); // At least 4 lines are visible.
@ -148,19 +133,13 @@ uint TextfileWindow::GetContentHeight()
}
/** Set scrollbars to the right lengths. */
void TextfileWindow::SetupScrollbars(bool force_reflow)
void TextfileWindow::SetupScrollbars()
{
if (this->IsTextWrapped()) {
/* Reflow is mandatory if text wrapping is on */
uint height = this->ReflowContent();
this->vscroll->SetCount(height);
this->hscroll->SetCount(0);
} else {
uint height = force_reflow ? this->ReflowContent() : this->GetContentHeight();
this->vscroll->SetCount(height);
this->hscroll->SetCount(this->max_length);
}
this->vscroll->SetCount(this->num_lines);
this->hscroll->SetCount(this->IsTextWrapped() ? 0 : CeilDiv(this->max_width, this->resize.step_width));
this->SetWidgetDirty(WID_TF_VSCROLLBAR);
this->SetWidgetDirty(WID_TF_HSCROLLBAR);
this->SetWidgetDisabledState(WID_TF_HSCROLLBAR, this->IsTextWrapped());
}
@ -308,18 +287,17 @@ const TextfileWindow::Hyperlink *TextfileWindow::GetHyperlink(Point pt) const
/* Which line was clicked. */
const int clicked_row = this->GetRowFromWidget(pt.y, WID_TF_BACKGROUND, WidgetDimensions::scaled.frametext.top, GetCharacterHeight(FS_MONO)) + this->GetScrollbar(WID_TF_VSCROLLBAR)->GetPosition();
size_t line_index;
size_t subline;
if (this->IsTextWrapped()) {
auto it = std::ranges::find_if(this->lines, [clicked_row](const Line &l) { return l.top <= clicked_row && l.bottom > clicked_row; });
if (it == this->lines.cend()) return nullptr;
line_index = it - this->lines.cbegin();
subline = clicked_row - it->top;
Debug(misc, 4, "TextfileWindow check hyperlink: clicked_row={}, line_index={}, line.top={}, subline={}", clicked_row, line_index, it->top, subline);
} else {
line_index = clicked_row / GetCharacterHeight(FS_MONO);
subline = 0;
}
int visible_line = 0;
auto it = std::ranges::find_if(this->lines, [&visible_line, clicked_row](const Line &l) {
visible_line += l.num_lines;
return (visible_line - l.num_lines) <= clicked_row && visible_line > clicked_row;
});
if (it == this->lines.cend()) return nullptr;
size_t line_index = it - this->lines.cbegin();
size_t subline = clicked_row - (visible_line - it->num_lines);
Debug(misc, 4, "TextfileWindow check hyperlink: clicked_row={}, line_index={}, line.top={}, subline={}", clicked_row, line_index, visible_line - it->num_lines, subline);
/* Find hyperlinks in this line. */
std::vector<const Hyperlink *> found_links;
@ -329,12 +307,12 @@ const TextfileWindow::Hyperlink *TextfileWindow::GetHyperlink(Point pt) const
if (found_links.empty()) return nullptr;
/* Build line layout to figure out character position that was clicked. */
uint window_width = this->IsTextWrapped() ? this->GetWidget<NWidgetCore>(WID_TF_BACKGROUND)->current_x - WidgetDimensions::scaled.frametext.Horizontal() : INT_MAX;
Layouter layout(this->lines[line_index].text, window_width, FS_MONO);
const Line &line = this->lines[line_index];
Layouter layout(line.text, line.wrapped_width, FS_MONO);
assert(subline < layout.size());
ptrdiff_t char_index = layout.GetCharAtPosition(pt.x - WidgetDimensions::scaled.frametext.left, subline);
if (char_index < 0) return nullptr;
Debug(misc, 4, "TextfileWindow check hyperlink click: line={}, subline={}, char_index={}", line_index, subline, (int)char_index);
Debug(misc, 4, "TextfileWindow check hyperlink click: line={}, subline={}, char_index={}", line_index, subline, char_index);
/* Found character index in line, check if any links are at that position. */
for (const Hyperlink *link : found_links) {
@ -570,6 +548,14 @@ void TextfileWindow::AfterLoadMarkdown()
/* virtual */ void TextfileWindow::DrawWidget(const Rect &r, WidgetID widget) const
{
if (widget == WID_TF_CAPTION && std::size(this->lines) > 0 && this->reflow_iter != this->reflow_end) {
/* Draw a progress bar in the caption. */
Rect fr = r.Shrink(WidgetDimensions::scaled.captiontext).WithHeight(WidgetDimensions::scaled.vsep_normal, true);
size_t remaining = std::distance(this->reflow_iter, this->reflow_end);
fr = fr.WithWidth(static_cast<int>(remaining * fr.Width() / std::size(this->lines)), _current_text_dir != TD_RTL);
GfxFillRect(fr, PC_WHITE, FILLRECT_CHECKER);
}
if (widget != WID_TF_BACKGROUND) return;
Rect fr = r.Shrink(WidgetDimensions::scaled.frametext);
@ -582,18 +568,21 @@ void TextfileWindow::AfterLoadMarkdown()
fr = fr.Translate(-fr.left, -fr.top);
int line_height = GetCharacterHeight(FS_MONO);
if (!this->IsTextWrapped()) fr = ScrollRect(fr, *this->hscroll, 1);
if (!this->IsTextWrapped()) fr = ScrollRect(fr, *this->hscroll, this->resize.step_width);
int pos = this->vscroll->GetPosition();
int cap = this->vscroll->GetCapacity();
int cur_line = 0;
for (auto &line : this->lines) {
if (line.bottom < pos) continue;
if (line.top > pos + cap) break;
int top = cur_line;
cur_line += line.num_lines;
if (cur_line <= pos) continue;
if (top > pos + cap) break;
int y_offset = (line.top - pos) * line_height;
if (this->IsTextWrapped()) {
DrawStringMultiLineWithClipping(fr.left, fr.right, y_offset, y_offset + (line.bottom - line.top) * line_height, line.text, line.colour, SA_TOP | SA_LEFT, false, FS_MONO);
int y_offset = (top - pos) * line_height;
if (line.wrapped_width != 0) {
Rect tr = fr.WithWidth(line.wrapped_width, _current_text_dir == TD_RTL);
DrawStringMultiLineWithClipping(tr.left, tr.right, y_offset, y_offset + line.num_lines * line_height, line.text, line.colour, SA_TOP | SA_LEFT, false, FS_MONO);
} else {
DrawString(fr.left, fr.right, y_offset, line.text, line.colour, SA_TOP | SA_LEFT, false, FS_MONO);
}
@ -605,14 +594,31 @@ void TextfileWindow::AfterLoadMarkdown()
this->vscroll->SetCapacityFromWidget(this, WID_TF_BACKGROUND, WidgetDimensions::scaled.frametext.Vertical());
this->hscroll->SetCapacityFromWidget(this, WID_TF_BACKGROUND, WidgetDimensions::scaled.framerect.Horizontal());
this->SetupScrollbars(false);
this->UpdateVisibleIterators();
this->ReflowContent();
this->SetupScrollbars();
}
/* virtual */ void TextfileWindow::OnInit()
{
/* If font has changed we need to recalculate the maximum width. */
this->num_lines = 0;
this->max_width = 0;
for (auto &line : this->lines) {
line.max_width = -1;
line.num_lines = 1;
line.wrapped_width = 0;
}
this->ReflowContent();
}
/* virtual */ void TextfileWindow::OnInvalidateData([[maybe_unused]] int data, [[maybe_unused]] bool gui_scope)
{
if (!gui_scope) return;
this->SetupScrollbars(true);
this->ReflowContent();
this->SetupScrollbars();
}
void TextfileWindow::OnDropdownSelect(WidgetID widget, int index)
@ -622,16 +628,107 @@ void TextfileWindow::OnDropdownSelect(WidgetID widget, int index)
this->ScrollToLine(index);
}
extern bool CanContinueRealtimeTick();
TextfileWindow::ReflowState TextfileWindow::ContinueReflow()
{
if (this->reflow_iter == this->reflow_end) return ReflowState::None;
int window_width = this->GetWidget<NWidgetCore>(WID_TF_BACKGROUND)->current_x - WidgetDimensions::scaled.frametext.Horizontal();
bool wrapped = this->IsTextWrapped();
bool dirty = false;
int pos = this->vscroll->GetPosition();
for (/* nothing */; this->reflow_iter != this->reflow_end; ++this->reflow_iter) {
auto it = this->reflow_iter.Base();
Line &line = *it;
int old_lines = line.num_lines;
if (wrapped) {
if (line.wrapped_width != window_width) {
line.num_lines = GetStringHeight(line.text, window_width, FS_MONO) / GetCharacterHeight(FS_MONO);
line.wrapped_width = window_width;
}
} else {
if (line.max_width == -1) {
line.max_width = GetStringBoundingBox(line.text, FS_MONO).width;
this->max_width = std::max(this->max_width, line.max_width);
}
line.num_lines = 1;
line.wrapped_width = 0;
}
/* Adjust the total number of lines. */
this->num_lines += (line.num_lines - old_lines);
/* Maintain scroll position. */
if (this->visible_first > it) pos += (line.num_lines - old_lines);
/* Mark dirty if visible range is touched. */
if (it >= this->visible_first && it <= this->visible_last) dirty = true;
if (!CanContinueRealtimeTick()) break;
}
if (this->vscroll->SetPosition(pos)) dirty = true;
return dirty ? ReflowState::VisibleReflowed : ReflowState::Reflowed;
}
void TextfileWindow::OnRealtimeTick(uint)
{
auto r = this->ContinueReflow();
if (r == ReflowState::None) return;
this->SetupScrollbars();
if (r == ReflowState::VisibleReflowed) {
this->SetWidgetDirty(WID_TF_BACKGROUND);
this->UpdateVisibleIterators();
}
/* Caption is always dirty. */
this->SetWidgetDirty(WID_TF_CAPTION);
}
void TextfileWindow::UpdateVisibleIterators()
{
int pos = this->vscroll->GetPosition();
int cap = this->vscroll->GetCapacity();
this->visible_first = this->GetIteratorFromPosition(pos);
/* The last visible iterator ignores line wrapping so that it does not need to change when line heights change. */
this->visible_last = std::ranges::next(this->visible_first, cap + 1, std::end(this->lines));
}
void TextfileWindow::OnScrollbarScroll(WidgetID widget)
{
if (widget != WID_TF_VSCROLLBAR) return;
this->UpdateVisibleIterators();
this->ReflowContent();
}
std::vector<TextfileWindow::Line>::iterator TextfileWindow::GetIteratorFromPosition(int pos)
{
for (auto it = std::begin(this->lines); it != std::end(this->lines); ++it) {
pos -= it->num_lines;
if (pos <= 0) return it;
}
return std::end(this->lines);
}
void TextfileWindow::ScrollToLine(size_t line)
{
Scrollbar *sb = this->GetScrollbar(WID_TF_VSCROLLBAR);
int newpos;
if (this->IsTextWrapped()) {
newpos = this->lines[line].top;
} else {
newpos = static_cast<int>(line);
int newpos = 0;
for (auto it = std::begin(this->lines); it != std::end(this->lines) && line > 0; --line, ++it) {
newpos += it->num_lines;
}
sb->SetPosition(std::min(newpos, sb->GetCount() - sb->GetCapacity()));
this->UpdateVisibleIterators();
this->ReflowContent();
this->SetDirty();
}
@ -814,25 +911,17 @@ void TextfileWindow::LoadText(std::string_view buf)
/* Split the string on newlines. */
std::string_view p(text);
int row = 0;
auto next = p.find_first_of('\n');
while (next != std::string_view::npos) {
this->lines.emplace_back(row, p.substr(0, next));
this->lines.emplace_back(p.substr(0, next));
p.remove_prefix(next + 1);
row++;
next = p.find_first_of('\n');
}
this->lines.emplace_back(row, p);
/* Calculate maximum text line length. */
uint max_length = 0;
for (auto &line : this->lines) {
max_length = std::max(max_length, GetStringBoundingBox(line.text, FS_MONO).width);
}
this->max_length = max_length;
this->lines.emplace_back(p);
this->AfterLoadText();
this->ReflowContent();
CheckForMissingGlyphs(true, this);

View File

@ -10,6 +10,7 @@
#ifndef TEXTFILE_GUI_H
#define TEXTFILE_GUI_H
#include "misc/alternating_iterator.hpp"
#include "fileio_type.h"
#include "strings_func.h"
#include "textfile_type.h"
@ -28,8 +29,11 @@ struct TextfileWindow : public Window, MissingGlyphSearcher {
bool OnTooltip([[maybe_unused]] Point pt, WidgetID widget, TooltipCloseCondition close_cond) override;
void DrawWidget(const Rect &r, WidgetID widget) const override;
void OnResize() override;
void OnInit() override;
void OnInvalidateData(int data = 0, bool gui_scope = true) override;
void OnDropdownSelect(WidgetID widget, int index) override;
void OnRealtimeTick(uint delta_ms) override;
void OnScrollbarScroll(WidgetID widget) override;
void Reset() override;
FontSize DefaultSize() override;
@ -46,12 +50,13 @@ protected:
void ConstructWindow();
struct Line {
int top = 0; ///< Top scroll position in visual lines.
int bottom = 0; ///< Bottom scroll position in visual lines.
std::string text{}; ///< Contents of the line.
int num_lines = 1; ///< Number of visual lines for this line.
int wrapped_width = 0;
int max_width = -1;
TextColour colour = TC_WHITE; ///< Colour to render text line in.
std::string text{}; ///< Contents of the line.
Line(int top, std::string_view text) : top(top), bottom(top + 1), text(text) {}
Line(std::string_view text) : text(text) {}
Line() {}
};
@ -100,11 +105,29 @@ protected:
private:
uint search_iterator = 0; ///< Iterator for the font check search.
uint max_length = 0; ///< Maximum length of unwrapped text line.
int max_width = 0; ///< Maximum length of unwrapped text line.
size_t num_lines = 0; ///< Number of lines of text, taking account of wrapping.
uint ReflowContent();
uint GetContentHeight();
void SetupScrollbars(bool force_reflow);
using LineIterator = std::vector<Line>::iterator;
using ReflowIterator = AlternatingIterator<LineIterator>;
ReflowIterator reflow_iter; ///< Current iterator for reflow.
ReflowIterator reflow_end; ///< End iterator for reflow.
LineIterator visible_first; ///< Iterator to first visible element.
LineIterator visible_last; ///< Iterator to last visible element.
enum class ReflowState : uint8_t {
None, ///< Nothing has been reflowed.
Reflowed, ///< Content has been reflowed.
VisibleReflowed, ///< Visible content has been reflowed.
};
std::vector<TextfileWindow::Line>::iterator GetIteratorFromPosition(int pos);
void UpdateVisibleIterators();
void ReflowContent();
ReflowState ContinueReflow();
void SetupScrollbars();
const Hyperlink *GetHyperlink(Point pt) const;
void AfterLoadMarkdown();

View File

@ -3042,11 +3042,20 @@ void InputLoop()
HandleMouseEvents();
}
static std::chrono::time_point<std::chrono::steady_clock> _realtime_tick_start;
bool CanContinueRealtimeTick()
{
auto now = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(now - _realtime_tick_start).count() < (MILLISECONDS_PER_TICK * 3 / 4);
}
/**
* Dispatch OnRealtimeTick event over all windows
*/
void CallWindowRealtimeTickEvent(uint delta_ms)
{
_realtime_tick_start = std::chrono::steady_clock::now();
for (Window *w : Window::Iterate()) {
w->OnRealtimeTick(delta_ms);
}