1
0
mirror of https://github.com/OpenTTD/OpenTTD synced 2026-01-17 01:12:39 +01:00
Files
OpenTTD/src/goal_gui.cpp
Peter Nelson e4cf6ca0ba Fix: Mis-sized widgets due to missing widget fill. (#14370)
In most places where we calculate and set widget resize step we neglect
to set widget fill step to match. Initial widget sizing uses fill step
instead of resize step, which means the initial size may not be a
multiple of the resize step as intended. In particular this will cause
WWT_MATRIX to be misrendered.

Whether or not this matters depends on the widget type being resized and
the window layout, however for consistency always set fill step to the
same as resize step when calculating.
2025-06-17 17:40:11 +01:00

480 lines
16 KiB
C++

/*
* 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 goal_gui.cpp GUI for goals. */
#include "stdafx.h"
#include "industry.h"
#include "town.h"
#include "window_gui.h"
#include "strings_func.h"
#include "viewport_func.h"
#include "gui.h"
#include "goal_base.h"
#include "core/geometry_func.hpp"
#include "company_func.h"
#include "company_base.h"
#include "company_gui.h"
#include "story_base.h"
#include "command_func.h"
#include "string_func.h"
#include "goal_cmd.h"
#include "widgets/goal_widget.h"
#include "table/strings.h"
#include "safeguards.h"
/** Goal list columns. */
enum GoalColumn : uint8_t {
GC_GOAL = 0, ///< Goal text column.
GC_PROGRESS, ///< Goal progress column.
};
/** Window for displaying goals. */
struct GoalListWindow : public Window {
Scrollbar *vscroll = nullptr; ///< Reference to the scrollbar widget.
GoalListWindow(WindowDesc &desc, WindowNumber window_number) : Window(desc)
{
this->CreateNestedTree();
this->vscroll = this->GetScrollbar(WID_GOAL_SCROLLBAR);
this->FinishInitNested(window_number);
this->owner = this->window_number;
NWidgetStacked *wi = this->GetWidget<NWidgetStacked>(WID_GOAL_SELECT_BUTTONS);
wi->SetDisplayedPlane(window_number == CompanyID::Invalid() ? 1 : 0);
this->OnInvalidateData(0);
}
std::string GetWidgetString(WidgetID widget, StringID stringid) const override
{
if (widget != WID_GOAL_CAPTION) return this->Window::GetWidgetString(widget, stringid);
if (this->window_number == CompanyID::Invalid()) {
return GetString(STR_GOALS_SPECTATOR_CAPTION);
}
return GetString(STR_GOALS_CAPTION, this->window_number);
}
void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
{
switch (widget) {
case WID_GOAL_GLOBAL_BUTTON:
ShowGoalsList(CompanyID::Invalid());
break;
case WID_GOAL_COMPANY_BUTTON:
ShowGoalsList(_local_company);
break;
case WID_GOAL_LIST: {
int y = this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_GOAL_LIST, WidgetDimensions::scaled.framerect.top);
for (const Goal *s : Goal::Iterate()) {
if (s->company == this->window_number) {
if (y == 0) {
this->HandleClick(s);
return;
}
y--;
}
}
break;
}
default:
break;
}
}
/**
* Handle clicking at a goal.
* @param s #Goal clicked at.
*/
void HandleClick(const Goal *s)
{
/* Determine dst coordinate for goal and try to scroll to it. */
TileIndex xy;
switch (s->type) {
case GT_NONE: return;
case GT_COMPANY:
/* s->dst here is not a tile, but a CompanyID.
* Show the window with the overview of the company instead. */
ShowCompany((CompanyID)s->dst);
return;
case GT_TILE:
if (!IsValidTile(s->dst)) return;
xy = TileIndex{s->dst};
break;
case GT_INDUSTRY:
if (!Industry::IsValidID(s->dst)) return;
xy = Industry::Get(s->dst)->location.tile;
break;
case GT_TOWN:
if (!Town::IsValidID(s->dst)) return;
xy = Town::Get(s->dst)->xy;
break;
case GT_STORY_PAGE: {
if (!StoryPage::IsValidID(s->dst)) return;
/* Verify that:
* - if global goal: story page must be global.
* - if company goal: story page must be global or of the same company.
*/
CompanyID goal_company = s->company;
CompanyID story_company = StoryPage::Get(s->dst)->company;
if (goal_company == CompanyID::Invalid() ? story_company != CompanyID::Invalid() : story_company != CompanyID::Invalid() && story_company != goal_company) return;
ShowStoryBook(static_cast<CompanyID>(this->window_number), static_cast<StoryPageID>(s->dst));
return;
}
default: NOT_REACHED();
}
if (_ctrl_pressed) {
ShowExtraViewportWindow(xy);
} else {
ScrollMainWindowToTile(xy);
}
}
/**
* Count the number of lines in this window.
* @return the number of lines.
*/
uint CountLines()
{
/* Count number of (non) awarded goals. */
uint num = 0;
for (const Goal *s : Goal::Iterate()) {
if (s->company == this->window_number) num++;
}
/* Count the 'none' lines. */
if (num == 0) num = 1;
return num;
}
void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
{
if (widget != WID_GOAL_LIST) return;
Dimension d = GetStringBoundingBox(STR_GOALS_NONE);
resize.width = 1;
fill.height = resize.height = d.height;
d.height *= 5;
d.width += WidgetDimensions::scaled.framerect.Horizontal();
d.height += WidgetDimensions::scaled.framerect.Vertical();
size = maxdim(size, d);
}
/**
* Draws a given column of the goal list.
* @param column Which column to draw.
* @param wid Pointer to the goal list widget.
* @param progress_col_width Width of the progress column.
* @return max width of drawn text
*/
void DrawListColumn(GoalColumn column, NWidgetBase *wid, uint progress_col_width) const
{
/* Get column draw area. */
Rect r = wid->GetCurrentRect().Shrink(WidgetDimensions::scaled.framerect);
bool rtl = _current_text_dir == TD_RTL;
int pos = -this->vscroll->GetPosition();
const int cap = this->vscroll->GetCapacity();
uint num = 0;
for (const Goal *s : Goal::Iterate()) {
if (s->company == this->window_number) {
if (IsInsideMM(pos, 0, cap)) {
switch (column) {
case GC_GOAL: {
/* Display the goal. */
uint width_reduction = progress_col_width > 0 ? progress_col_width + WidgetDimensions::scaled.framerect.Horizontal() : 0;
DrawString(r.Indent(width_reduction, !rtl), GetString(STR_GOALS_TEXT, s->text.GetDecodedString()));
break;
}
case GC_PROGRESS:
if (!s->progress.empty()) {
StringID str = s->completed ? STR_GOALS_PROGRESS_COMPLETE : STR_GOALS_PROGRESS;
DrawString(r.WithWidth(progress_col_width, !rtl), GetString(str, s->progress.GetDecodedString()), TC_FROMSTRING, SA_RIGHT | SA_FORCE);
}
break;
}
r.top += GetCharacterHeight(FS_NORMAL);
}
pos++;
num++;
}
}
if (num == 0) {
if (column == GC_GOAL && IsInsideMM(pos, 0, cap)) {
DrawString(r, STR_GOALS_NONE);
}
}
}
void OnPaint() override
{
this->DrawWidgets();
if (this->IsShaded()) return; // Don't draw anything when the window is shaded.
/* Calculate progress column width. */
uint max_width = 0;
for (const Goal *s : Goal::Iterate()) {
if (!s->progress.empty()) {
StringID str = s->completed ? STR_GOALS_PROGRESS_COMPLETE : STR_GOALS_PROGRESS;
uint str_width = GetStringBoundingBox(GetString(str, s->progress.GetDecodedString())).width;
if (str_width > max_width) max_width = str_width;
}
}
NWidgetBase *wid = this->GetWidget<NWidgetBase>(WID_GOAL_LIST);
uint progress_col_width = std::min(max_width, wid->current_x);
/* Draw goal list. */
this->DrawListColumn(GC_PROGRESS, wid, progress_col_width);
this->DrawListColumn(GC_GOAL, wid, progress_col_width);
}
void OnResize() override
{
this->vscroll->SetCapacityFromWidget(this, WID_GOAL_LIST, WidgetDimensions::scaled.framerect.Vertical());
}
/**
* Some data on this window has become invalid.
* @param data Information about the changed data.
* @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
*/
void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
{
if (!gui_scope) return;
this->vscroll->SetCount(this->CountLines());
this->SetWidgetDisabledState(WID_GOAL_COMPANY_BUTTON, _local_company == COMPANY_SPECTATOR);
this->SetWidgetDirty(WID_GOAL_COMPANY_BUTTON);
this->SetWidgetDirty(WID_GOAL_LIST);
}
};
/** Widgets of the #GoalListWindow. */
static constexpr NWidgetPart _nested_goals_list_widgets[] = {
NWidget(NWID_HORIZONTAL),
NWidget(WWT_CLOSEBOX, COLOUR_BROWN),
NWidget(WWT_CAPTION, COLOUR_BROWN, WID_GOAL_CAPTION),
NWidget(NWID_SELECTION, INVALID_COLOUR, WID_GOAL_SELECT_BUTTONS),
NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_GOAL_GLOBAL_BUTTON), SetMinimalSize(50, 0), SetStringTip(STR_GOALS_GLOBAL_BUTTON, STR_GOALS_GLOBAL_BUTTON_HELPTEXT),
NWidget(WWT_PUSHTXTBTN, COLOUR_BROWN, WID_GOAL_COMPANY_BUTTON), SetMinimalSize(50, 0), SetStringTip(STR_GOALS_COMPANY_BUTTON, STR_GOALS_COMPANY_BUTTON_HELPTEXT),
EndContainer(),
NWidget(WWT_SHADEBOX, COLOUR_BROWN),
NWidget(WWT_DEFSIZEBOX, COLOUR_BROWN),
NWidget(WWT_STICKYBOX, COLOUR_BROWN),
EndContainer(),
NWidget(NWID_HORIZONTAL),
NWidget(WWT_PANEL, COLOUR_BROWN, WID_GOAL_LIST), SetToolTip(STR_GOALS_TOOLTIP_CLICK_ON_SERVICE_TO_CENTER), SetScrollbar(WID_GOAL_SCROLLBAR), SetResize(1, 1), SetMinimalTextLines(2, 0),
EndContainer(),
NWidget(NWID_VERTICAL),
NWidget(NWID_VSCROLLBAR, COLOUR_BROWN, WID_GOAL_SCROLLBAR),
NWidget(WWT_RESIZEBOX, COLOUR_BROWN),
EndContainer(),
EndContainer(),
};
static WindowDesc _goals_list_desc(
WDP_AUTO, "list_goals", 500, 127,
WC_GOALS_LIST, WC_NONE,
{},
_nested_goals_list_widgets
);
/**
* Open a goal list window.
* @param company %Company to display the goals for, use #CompanyID::Invalid() to display global goals.
*/
void ShowGoalsList(CompanyID company)
{
if (!Company::IsValidID(company)) company = (CompanyID)CompanyID::Invalid();
AllocateWindowDescFront<GoalListWindow>(_goals_list_desc, company);
}
/** Ask a question about a goal. */
struct GoalQuestionWindow : public Window {
EncodedString question{}; ///< Question to ask (private copy).
int buttons = 0; ///< Number of valid buttons in #button.
std::array<int, 3> button{}; ///< Buttons to display.
TextColour colour{}; ///< Colour of the question text.
GoalQuestionWindow(WindowDesc &desc, WindowNumber window_number, TextColour colour, uint32_t button_mask, const EncodedString &question) : Window(desc), colour(colour)
{
this->question = question;
/* Figure out which buttons we have to enable. */
int n = 0;
for (uint bit : SetBitIterator(button_mask)) {
if (bit >= GOAL_QUESTION_BUTTON_COUNT) break;
this->button[n++] = bit;
if (n == 3) break;
}
this->buttons = n;
assert(this->buttons < 4);
this->CreateNestedTree();
if (this->buttons == 0) {
this->GetWidget<NWidgetStacked>(WID_GQ_BUTTONS)->SetDisplayedPlane(SZSP_HORIZONTAL);
} else {
this->GetWidget<NWidgetStacked>(WID_GQ_BUTTONS)->SetDisplayedPlane(this->buttons - 1);
}
this->FinishInitNested(window_number);
}
std::string GetWidgetString(WidgetID widget, StringID stringid) const override
{
switch (widget) {
case WID_GQ_BUTTON_1:
return GetString(STR_GOAL_QUESTION_BUTTON_CANCEL + this->button[0]);
case WID_GQ_BUTTON_2:
return GetString(STR_GOAL_QUESTION_BUTTON_CANCEL + this->button[1]);
case WID_GQ_BUTTON_3:
return GetString(STR_GOAL_QUESTION_BUTTON_CANCEL + this->button[2]);
default:
return this->Window::GetWidgetString(widget, stringid);
}
}
void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
{
switch (widget) {
case WID_GQ_BUTTON_1:
Command<CMD_GOAL_QUESTION_ANSWER>::Post(this->window_number, this->button[0]);
this->Close();
break;
case WID_GQ_BUTTON_2:
Command<CMD_GOAL_QUESTION_ANSWER>::Post(this->window_number, this->button[1]);
this->Close();
break;
case WID_GQ_BUTTON_3:
Command<CMD_GOAL_QUESTION_ANSWER>::Post(this->window_number, this->button[2]);
this->Close();
break;
}
}
void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
{
if (widget != WID_GQ_QUESTION) return;
size.height = GetStringHeight(this->question.GetDecodedString(), size.width);
}
void DrawWidget(const Rect &r, WidgetID widget) const override
{
if (widget != WID_GQ_QUESTION) return;
DrawStringMultiLine(r, this->question.GetDecodedString(), this->colour, SA_TOP | SA_HOR_CENTER);
}
};
/**
* Widgets of the goal question window.
* @tparam bg_colour Background colour.
* @tparam btn_colour Button colour.
* @tparam caption Window caption string.
*/
template <Colours bg_colour, Colours btn_colour, StringID caption>
struct NestedGoalWidgets {
static constexpr auto widgetparts = {
NWidget(NWID_HORIZONTAL),
NWidget(WWT_CLOSEBOX, bg_colour),
NWidget(WWT_CAPTION, bg_colour, WID_GQ_CAPTION), SetStringTip(STR_GOAL_QUESTION_CAPTION_QUESTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
EndContainer(),
NWidget(WWT_PANEL, bg_colour),
NWidget(NWID_VERTICAL), SetPadding(WidgetDimensions::unscaled.modalpopup), SetPIP(0, WidgetDimensions::unscaled.vsep_wide, 0),
NWidget(WWT_EMPTY, INVALID_COLOUR, WID_GQ_QUESTION), SetMinimalSize(300, 0), SetFill(1, 0),
NWidget(NWID_SELECTION, INVALID_COLOUR, WID_GQ_BUTTONS),
NWidget(NWID_HORIZONTAL, NWidContainerFlag::EqualSize), SetPIP(85, WidgetDimensions::unscaled.hsep_wide, 85),
NWidget(WWT_PUSHTXTBTN, btn_colour, WID_GQ_BUTTON_1), SetFill(1, 0),
EndContainer(),
NWidget(NWID_HORIZONTAL, NWidContainerFlag::EqualSize), SetPIP(65, WidgetDimensions::unscaled.hsep_wide, 65),
NWidget(WWT_PUSHTXTBTN, btn_colour, WID_GQ_BUTTON_1), SetFill(1, 0),
NWidget(WWT_PUSHTXTBTN, btn_colour, WID_GQ_BUTTON_2), SetFill(1, 0),
EndContainer(),
NWidget(NWID_HORIZONTAL, NWidContainerFlag::EqualSize), SetPIP(25, WidgetDimensions::unscaled.hsep_wide, 25),
NWidget(WWT_PUSHTXTBTN, btn_colour, WID_GQ_BUTTON_1), SetFill(1, 0),
NWidget(WWT_PUSHTXTBTN, btn_colour, WID_GQ_BUTTON_2), SetFill(1, 0),
NWidget(WWT_PUSHTXTBTN, btn_colour, WID_GQ_BUTTON_3), SetFill(1, 0),
EndContainer(),
EndContainer(),
EndContainer(),
EndContainer(),
};
};
static constexpr auto _nested_goal_question_widgets_question = NestedGoalWidgets<COLOUR_LIGHT_BLUE, COLOUR_LIGHT_BLUE, STR_GOAL_QUESTION_CAPTION_QUESTION>::widgetparts;
static constexpr auto _nested_goal_question_widgets_info = NestedGoalWidgets<COLOUR_LIGHT_BLUE, COLOUR_LIGHT_BLUE, STR_GOAL_QUESTION_CAPTION_INFORMATION>::widgetparts;
static constexpr auto _nested_goal_question_widgets_warning = NestedGoalWidgets<COLOUR_YELLOW, COLOUR_YELLOW, STR_GOAL_QUESTION_CAPTION_WARNING>::widgetparts;
static constexpr auto _nested_goal_question_widgets_error = NestedGoalWidgets<COLOUR_RED, COLOUR_YELLOW, STR_GOAL_QUESTION_CAPTION_ERROR>::widgetparts;
static WindowDesc _goal_question_list_desc[] = {
{
WDP_CENTER, {}, 0, 0,
WC_GOAL_QUESTION, WC_NONE,
WindowDefaultFlag::Construction,
_nested_goal_question_widgets_question,
},
{
WDP_CENTER, {}, 0, 0,
WC_GOAL_QUESTION, WC_NONE,
WindowDefaultFlag::Construction,
_nested_goal_question_widgets_info,
},
{
WDP_CENTER, {}, 0, 0,
WC_GOAL_QUESTION, WC_NONE,
WindowDefaultFlag::Construction,
_nested_goal_question_widgets_warning,
},
{
WDP_CENTER, {}, 0, 0,
WC_GOAL_QUESTION, WC_NONE,
WindowDefaultFlag::Construction,
_nested_goal_question_widgets_error,
},
};
/**
* Display a goal question.
* @param id Window number to use.
* @param type Type of question.
* @param button_mask Buttons to display.
* @param question Question to ask.
*/
void ShowGoalQuestion(uint16_t id, uint8_t type, uint32_t button_mask, const EncodedString &question)
{
assert(type < GQT_END);
new GoalQuestionWindow(_goal_question_list_desc[type], id, type == 3 ? TC_WHITE : TC_BLACK, button_mask, question);
}