mirror of
https://github.com/OpenTTD/OpenTTD.git
synced 2025-08-22 05:59:10 +00:00
Compare commits
5 Commits
1064309ecf
...
7344dfe651
Author | SHA1 | Date | |
---|---|---|---|
7344dfe651 | |||
46b745a06a | |||
940071a5f2 | |||
780c26237f | |||
|
321f7e8683 |
@@ -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)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
add_files(
|
||||
alternating_iterator.hpp
|
||||
binaryheap.hpp
|
||||
dbg_helpers.cpp
|
||||
dbg_helpers.h
|
||||
|
145
src/misc/alternating_iterator.hpp
Normal file
145
src/misc/alternating_iterator.hpp
Normal 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 */
|
@@ -1,4 +1,5 @@
|
||||
add_test_files(
|
||||
alternating_iterator.cpp
|
||||
bitmath_func.cpp
|
||||
enum_over_optimisation.cpp
|
||||
flatset_type.cpp
|
||||
|
48
src/tests/alternating_iterator.cpp
Normal file
48
src/tests/alternating_iterator.cpp
Normal 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 }));
|
||||
}
|
@@ -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);
|
||||
|
||||
|
@@ -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();
|
||||
|
@@ -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 */
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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).
|
||||
|
Reference in New Issue
Block a user