mirror of https://github.com/OpenTTD/OpenTTD
Change: Reflow Textfile window content incrementally.
This avoids a stall when reflowing a long text file with some truetype fonts.pull/14293/head
parent
46b745a06a
commit
7344dfe651
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue