1
0
mirror of https://github.com/OpenTTD/OpenTTD.git synced 2025-08-22 05:59:10 +00:00

Compare commits

...

5 Commits

Author SHA1 Message Date
7344dfe651 Change: Reflow Textfile window content incrementally.
This avoids a stall when reflowing a long text file with some truetype fonts.
2025-05-23 07:42:10 +01:00
46b745a06a Codechange: Use a helper method to test if text file window is wrapped. 2025-05-23 07:42:10 +01:00
940071a5f2 Codechange: Add alternating iterator to take elements from middle of range. 2025-05-23 07:42:10 +01:00
780c26237f Codechange: Add OnScrollbarScroll window event, called when a scrollbar position changes. 2025-05-23 07:42:10 +01:00
translators
321f7e8683 Update: Translations from eints
swedish: 3 changes by joeax910
2025-05-23 04:45:57 +00:00
10 changed files with 433 additions and 88 deletions

View File

@@ -2195,6 +2195,8 @@ STR_VIDEO_DRIVER_ERROR_NO_HARDWARE_ACCELERATION :{WHITE}... Inge
STR_VIDEO_DRIVER_ERROR_HARDWARE_ACCELERATION_CRASH :{WHITE}... GPU-drivrutinen kraschade spelet. Maskinvaruacceleration inaktiverad
# Intro window
STR_INTRO_CAPTION :{WHITE}OpenTTD
STR_INTRO_VERSION :OpenTTD {REV}
STR_INTRO_NEW_GAME :{BLACK}Nytt spel
STR_INTRO_LOAD_GAME :{BLACK}Ladda spel
@@ -4759,6 +4761,7 @@ STR_TIMETABLE_TOOLTIP :{BLACK}Tidtabel
STR_TIMETABLE_NO_TRAVEL :Ingen resa
STR_TIMETABLE_NOT_TIMETABLEABLE :Restid (automatisk; tidtabellen baseras på nästa manuella order)
STR_TIMETABLE_TRAVEL_NOT_TIMETABLED :Restid (inte angiven)
STR_TIMETABLE_TRAVEL_NOT_TIMETABLED_SPEED :Res (utan tidtabell) i högst {VELOCITY}
STR_TIMETABLE_TRAVEL_FOR :Res i {STRING}
STR_TIMETABLE_TRAVEL_FOR_SPEED :Res i {STRING} med högsta hastighet {VELOCITY}
STR_TIMETABLE_TRAVEL_FOR_ESTIMATED :Resor (för {STRING}, ej schemalagd)

View File

@@ -1,4 +1,5 @@
add_files(
alternating_iterator.hpp
binaryheap.hpp
dbg_helpers.cpp
dbg_helpers.h

View File

@@ -0,0 +1,145 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file alternating_iterator.hpp Iterator adaptor that takes items alternating from a middle position. */
#ifndef ALTERNATING_ITERATOR_HPP
#define ALTERNATING_ITERATOR_HPP
#include <ranges>
/**
* Iterator that alternately takes from the "middle" of a range.
* @tparam Titer Type of iterator.
*/
template <typename Titer>
class AlternatingIterator {
public:
using value_type = typename Titer::value_type;
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
using pointer = typename Titer::pointer;
using reference = typename Titer::reference;
AlternatingIterator() = default;
/**
* Construct an AlternatingIterator.
* @param first Iterator to first element.
* @param last Iterator to last element.
* @param middle Iterator to "middle" element, from where to start.
* @param begin Whether this iterator points to the first or last elements.
*/
AlternatingIterator(Titer first, Titer last, Titer middle, bool begin) : first(first), last(last), middle(middle)
{
/* Starting from the end is not supported, unless the range is empty. */
assert(first == last || middle != last);
this->position = begin ? 0 : std::distance(this->first, this->last);
this->before = middle;
this->after = middle;
this->next_state = this->before == this->first;
this->state = this->next_state;
}
bool operator==(const AlternatingIterator &rhs) const
{
assert(this->first == rhs.first);
assert(this->last == rhs.last);
assert(this->middle == rhs.middle);
return this->position == rhs.position;
}
std::strong_ordering operator<=>(const AlternatingIterator &rhs) const
{
assert(this->first == rhs.first);
assert(this->last == rhs.last);
assert(this->middle == rhs.middle);
return this->position <=> rhs.position;
}
inline reference operator*() const
{
return *this->Base();
}
AlternatingIterator &operator++()
{
size_t size = static_cast<size_t>(std::distance(this->first, this->last));
assert(this->position < size);
++this->position;
if (this->position < size) this->Next();
return *this;
}
AlternatingIterator operator++(int)
{
AlternatingIterator result = *this;
++*this;
return result;
}
inline Titer Base() const
{
return this->state ? this->after : this->before;
}
private:
Titer first; ///< Initial first iterator.
Titer last; ///< Initial last iterator.
Titer middle; ///< Initial middle iterator.
Titer after; ///< Current iterator after the middle.
Titer before; ///< Current iterator before the middle.
size_t position; ///< Position within the entire range.
bool next_state; ///< Next state for advancing iterator. If true take from after middle, otherwise take from before middle.
bool state; ///< Current state for reading iterator. If true take from after middle, otherwise take from before middle.
void Next()
{
this->state = this->next_state;
if (this->next_state) {
assert(this->after != this->last);
++this->after;
this->next_state = this->before == this->first;
} else {
assert(this->before != this->first);
--this->before;
this->next_state = std::next(this->after) != this->last;
}
}
};
template <typename Titer>
class AlternatingView : public std::ranges::view_interface<AlternatingIterator<Titer>> {
public:
AlternatingView(std::ranges::viewable_range auto &&range, Titer middle) :
first(std::ranges::begin(range)), last(std::ranges::end(range)), middle(middle)
{
}
auto begin() const
{
return AlternatingIterator{first, last, middle, true};
}
auto end() const
{
return AlternatingIterator{first, last, middle, false};
}
private:
Titer first; ///< Iterator to first element.
Titer last; ///< Iterator to last element.
Titer middle; ///< Iterator to middle element.
};
#endif /* ALTERNATING_ITERATOR_HPP */

View File

@@ -1,4 +1,5 @@
add_test_files(
alternating_iterator.cpp
bitmath_func.cpp
enum_over_optimisation.cpp
flatset_type.cpp

View File

@@ -0,0 +1,48 @@
/*
* This file is part of OpenTTD.
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file bitmath_func.cpp Test functionality from core/bitmath_func. */
#include "../stdafx.h"
#include "../3rdparty/catch2/catch.hpp"
#include "../misc/alternating_iterator.hpp"
#include "../safeguards.h"
TEST_CASE("AlternatingIterator tests")
{
auto test_case = [&](auto input, std::initializer_list<int> expected) {
return std::ranges::equal(input, expected);
};
/* Sequence includes sentinel markers to detect out-of-bounds reads without relying on UB. */
std::initializer_list<const int> raw_sequence_even = {INT_MAX, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, INT_MAX};
const std::span<const int> sequence_even = std::span{raw_sequence_even.begin() + 1, raw_sequence_even.end() - 1};
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 0), { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }));
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 1), { 1, 0, 2, 3, 4, 5, 6, 7, 8, 9 }));
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 2), { 2, 1, 3, 0, 4, 5, 6, 7, 8, 9 }));
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 3), { 3, 2, 4, 1, 5, 0, 6, 7, 8, 9 }));
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 4), { 4, 3, 5, 2, 6, 1, 7, 0, 8, 9 }));
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 5), { 5, 4, 6, 3, 7, 2, 8, 1, 9, 0 }));
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 6), { 6, 5, 7, 4, 8, 3, 9, 2, 1, 0 }));
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 7), { 7, 6, 8, 5, 9, 4, 3, 2, 1, 0 }));
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 8), { 8, 7, 9, 6, 5, 4, 3, 2, 1, 0 }));
CHECK(test_case(AlternatingView(sequence_even, sequence_even.begin() + 9), { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }));
/* Sequence includes sentinel markers to detect out-of-bounds reads without relying on UB. */
std::initializer_list<const int> raw_sequence_odd = {INT_MAX, 0, 1, 2, 3, 4, INT_MAX};
const std::span<const int> sequence_odd = std::span{raw_sequence_odd.begin() + 1, raw_sequence_odd.end() - 1};
CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 0), { 0, 1, 2, 3, 4 }));
CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 1), { 1, 0, 2, 3, 4 }));
CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 2), { 2, 1, 3, 0, 4 }));
CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 3), { 3, 2, 4, 1, 0 }));
CHECK(test_case(AlternatingView(sequence_odd, sequence_odd.begin() + 4), { 4, 3, 2, 1, 0 }));
}

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 (!IsWidgetLowered(WID_TF_WRAPTEXT)) {
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,20 +133,14 @@ uint TextfileWindow::GetContentHeight()
}
/** Set scrollbars to the right lengths. */
void TextfileWindow::SetupScrollbars(bool force_reflow)
void TextfileWindow::SetupScrollbars()
{
if (IsWidgetLowered(WID_TF_WRAPTEXT)) {
/* 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->SetWidgetDisabledState(WID_TF_HSCROLLBAR, IsWidgetLowered(WID_TF_WRAPTEXT));
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 (IsWidgetLowered(WID_TF_WRAPTEXT)) {
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 = IsWidgetLowered(WID_TF_WRAPTEXT) ? 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 (!IsWidgetLowered(WID_TF_WRAPTEXT)) 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 (IsWidgetLowered(WID_TF_WRAPTEXT)) {
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,19 +628,115 @@ 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->IsWidgetLowered(WID_TF_WRAPTEXT)) {
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();
}
bool TextfileWindow::IsTextWrapped() const
{
return this->IsWidgetLowered(WID_TF_WRAPTEXT);
}
/* virtual */ void TextfileWindow::Reset()
{
this->search_iterator = 0;
@@ -809,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;
@@ -37,6 +41,7 @@ struct TextfileWindow : public Window, MissingGlyphSearcher {
bool Monospace() override;
void SetFontNames(FontCacheSettings *settings, std::string_view font_name, const void *os_data) override;
void ScrollToLine(size_t line);
bool IsTextWrapped() const;
virtual void LoadTextfile(const std::string &textfile, Subdirectory dir);
@@ -45,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() {}
};
@@ -99,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

@@ -240,6 +240,7 @@ static void ScrollbarClickPositioning(Window *w, NWidgetScrollbar *sb, int x, in
if (changed) {
/* Position changed so refresh the window */
w->OnScrollbarScroll(sb->GetIndex());
w->SetDirty();
} else {
/* No change so only refresh this scrollbar */

View File

@@ -818,7 +818,10 @@ static void DispatchMouseWheelEvent(Window *w, NWidgetCore *nwid, int wheel)
if (nwid->type == NWID_VSCROLLBAR) {
NWidgetScrollbar *sb = static_cast<NWidgetScrollbar *>(nwid);
if (sb->GetCount() > sb->GetCapacity()) {
if (sb->UpdatePosition(wheel)) w->SetDirty();
if (sb->UpdatePosition(wheel)) {
w->OnScrollbarScroll(nwid->GetIndex());
w->SetDirty();
}
}
return;
}
@@ -826,7 +829,10 @@ static void DispatchMouseWheelEvent(Window *w, NWidgetCore *nwid, int wheel)
/* Scroll the widget attached to the scrollbar. */
Scrollbar *sb = (nwid->GetScrollbarIndex() >= 0 ? w->GetScrollbar(nwid->GetScrollbarIndex()) : nullptr);
if (sb != nullptr && sb->GetCount() > sb->GetCapacity()) {
if (sb->UpdatePosition(wheel)) w->SetDirty();
if (sb->UpdatePosition(wheel)) {
w->OnScrollbarScroll(nwid->GetScrollbarIndex());
w->SetDirty();
}
}
}
@@ -2342,7 +2348,10 @@ static void HandleScrollbarScrolling(Window *w)
if (sb->disp_flags.Any({NWidgetDisplayFlag::ScrollbarUp, NWidgetDisplayFlag::ScrollbarDown})) {
if (_scroller_click_timeout == 1) {
_scroller_click_timeout = 3;
if (sb->UpdatePosition(rtl == sb->disp_flags.Test(NWidgetDisplayFlag::ScrollbarUp) ? 1 : -1)) w->SetDirty();
if (sb->UpdatePosition(rtl == sb->disp_flags.Test(NWidgetDisplayFlag::ScrollbarUp) ? 1 : -1)) {
w->OnScrollbarScroll(w->mouse_capture_widget);
w->SetDirty();
}
}
return;
}
@@ -2353,7 +2362,10 @@ static void HandleScrollbarScrolling(Window *w)
int pos = RoundDivSU((i + _scrollbar_start_pos) * range, std::max(1, _scrollbar_size));
if (rtl) pos = range - pos;
if (sb->SetPosition(pos)) w->SetDirty();
if (sb->SetPosition(pos)) {
w->OnScrollbarScroll(w->mouse_capture_widget);
w->SetDirty();
}
}
/**
@@ -3030,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);
}

View File

@@ -711,6 +711,13 @@ public:
*/
virtual void OnScroll([[maybe_unused]] Point delta) {}
/**
* Notify window that a scrollbar position has been updated.
* @note Only called when the user scrolls, not if a window moves its scrollbar.
* @param widget the scrollbar widget index.
*/
virtual void OnScrollbarScroll([[maybe_unused]] WidgetID widget) {}
/**
* The mouse is currently moving over the window or has just moved outside
* of the window. In the latter case pt is (-1, -1).