diff --git a/src/network/network_gui.cpp b/src/network/network_gui.cpp index 61975b476a..c1d1dfbf10 100644 --- a/src/network/network_gui.cpp +++ b/src/network/network_gui.cpp @@ -1381,6 +1381,159 @@ public: using CompanyButton = Button; using ClientButton = Button; +/** + * Base interface for a network client list line. + */ +class ButtonLine { +public: + std::vector> buttons{}; ///< Buttons for this line. + + virtual ~ButtonLine() = default; + + /** + * Draw the button line. + * @param r Rect to draw within. + */ + virtual void Draw(Rect r) const = 0; + + template + T &AddButton(TArgs &&... args) + { + auto &button = this->buttons.emplace_back(std::make_unique(std::forward(args)...)); + return static_cast(*button); + } + + /** + * Get the button at a given point on the line. + * @param r Rect of line. + * @param pt Point of interest. + * @return Button at point, or \c nullptr if button isn't pressed. + */ + ButtonCommon *GetButton(Rect r, const Point &pt) const + { + bool rtl = _current_text_dir == TD_RTL; + for (auto &button : this->buttons) { + if (r.WithWidth(button->width, !rtl).Contains(pt)) return button.get(); + r = r.Indent(button->width + WidgetDimensions::scaled.hsep_normal, !rtl); + } + return nullptr; + } + + /** + * Get tooptip for a given point on the line. + * @param r Rect of line. + * @param pt Point of interest. + * @return EncodedString of tooltip, or \c std::nullopt if none. + */ + virtual std::optional GetTooltip(Rect r, const Point &pt) const + { + ButtonCommon *button = this->GetButton(r, pt); + if (button == nullptr) return {}; + return GetEncodedString(button->tooltip); + } + +protected: + /** + * Draw the buttons for this line. + * @param r Rect to draw within. + * @return Rect of remaining space. + */ + Rect DrawButtons(Rect r) const + { + bool rtl = _current_text_dir == TD_RTL; + for (auto &button : buttons) { + Rect br = r.CentreTo(r.Width(), button->height).WithWidth(button->width, !rtl); + DrawFrameRect(br, button->colour, {}); + DrawSpriteIgnorePadding(button->sprite, PAL_NONE, br, SA_CENTER); + if (button->disabled) { + GfxFillRect(br.Shrink(WidgetDimensions::scaled.bevel), GetColourGradient(button->colour, SHADE_DARKER), FILLRECT_CHECKER); + } + r = r.Indent(button->width + WidgetDimensions::scaled.hsep_normal, !rtl); + } + return r; + } +}; + +class CompanyButtonLine : public ButtonLine { +public: + CompanyButtonLine(CompanyID company_id) : company_id(company_id) {} + + void Draw(Rect r) const override + { + bool rtl = _current_text_dir == TD_RTL; + r = this->DrawButtons(r); + + Dimension d = GetSpriteSize(SPR_COMPANY_ICON); + PaletteID pal = Company::IsValidID(this->company_id) ? GetCompanyPalette(this->company_id) : PALETTE_TO_GREY; + DrawSpriteIgnorePadding(SPR_COMPANY_ICON, pal, r.WithWidth(d.width, rtl), SA_CENTER); + + Rect tr = r.CentreTo(r.Width(), GetCharacterHeight(FS_NORMAL)).Indent(d.width + WidgetDimensions::scaled.hsep_normal, rtl); + if (this->company_id == COMPANY_SPECTATOR) { + DrawString(tr, STR_NETWORK_CLIENT_LIST_SPECTATORS, TC_SILVER); + } else if (this->company_id == COMPANY_NEW_COMPANY) { + DrawString(tr, STR_NETWORK_CLIENT_LIST_NEW_COMPANY, TC_WHITE); + } else { + DrawString(tr, GetString(STR_COMPANY_NAME, this->company_id, this->company_id), TC_SILVER); + } + }; + +private: + CompanyID company_id; +}; + +class ClientButtonLine : public ButtonLine { +public: + ClientButtonLine(ClientPoolID client_pool_id) : client_pool_id(client_pool_id) {} + + void Draw(Rect r) const override + { + const NetworkClientInfo *ci = NetworkClientInfo::GetIfValid(this->client_pool_id); + if (ci == nullptr) return; + + bool rtl = _current_text_dir == TD_RTL; + r = this->DrawButtons(r); + + Rect tr = r.CentreTo(r.Width(), GetCharacterHeight(FS_NORMAL)); + + SpriteID player_icon = 0; + if (ci->client_id == _network_own_client_id) { + player_icon = SPR_PLAYER_SELF; + } else if (ci->client_id == CLIENT_ID_SERVER) { + player_icon = SPR_PLAYER_HOST; + } + + if (player_icon != 0) { + Dimension d = GetSpriteSize(player_icon); + DrawSpriteIgnorePadding(player_icon, PALETTE_TO_GREY, r.WithWidth(d.width, rtl), SA_CENTER); + tr = tr.Indent(d.width + WidgetDimensions::scaled.hsep_normal, rtl); + } + + DrawString(tr, GetString(STR_JUST_RAW_STRING, ci->client_name), TC_BLACK); + } + + std::optional GetTooltip(Rect r, const Point &pt) const override + { + bool rtl = _current_text_dir == TD_RTL; + Dimension d = GetSpriteSize(SPR_PLAYER_SELF); + + if (r.WithWidth(d.width, rtl).Contains(pt)) { + const NetworkClientInfo *ci = NetworkClientInfo::GetIfValid(this->client_pool_id); + if (ci != nullptr) { + if (ci->client_id == _network_own_client_id) { + return GetEncodedString(STR_NETWORK_CLIENT_LIST_PLAYER_ICON_SELF_TOOLTIP); + } else if (ci->client_id == CLIENT_ID_SERVER) { + return GetEncodedString(STR_NETWORK_CLIENT_LIST_PLAYER_ICON_HOST_TOOLTIP); + } + } + } + + return this->ButtonLine::GetTooltip(r, pt); + } + +private: + ClientPoolID client_pool_id; +}; + /** * Main handle for clientlist */ @@ -1393,12 +1546,9 @@ private: Scrollbar *vscroll = nullptr; ///< Vertical scrollbar of this window. uint line_height = 0; ///< Current lineheight of each entry in the matrix. - uint line_count = 0; ///< Amount of lines in the matrix. int hover_index = -1; ///< Index of the current line we are hovering over, or -1 if none. - int player_self_index = -1; ///< The line the current player is on. - int player_host_index = -1; ///< The line the host is on. - std::map>> buttons{}; ///< Per line which buttons are available. + std::vector> buttons{}; ///< Per line which buttons are available. /** * Chat button on a Company is clicked. @@ -1504,34 +1654,24 @@ private: */ void RebuildListCompany(CompanyID company_id, CompanyID client_playas, bool can_join_company) { - ButtonCommon *chat_button = new CompanyButton(SPR_CHAT, company_id == COMPANY_SPECTATOR ? STR_NETWORK_CLIENT_LIST_CHAT_SPECTATOR_TOOLTIP : STR_NETWORK_CLIENT_LIST_CHAT_COMPANY_TOOLTIP, COLOUR_ORANGE, company_id, &NetworkClientListWindow::OnClickCompanyChat); - - if (_network_server) this->buttons[line_count].push_back(std::make_unique(SPR_ADMIN, STR_NETWORK_CLIENT_LIST_ADMIN_COMPANY_TOOLTIP, COLOUR_RED, company_id, &NetworkClientListWindow::OnClickCompanyAdmin, company_id == COMPANY_SPECTATOR)); - this->buttons[line_count].emplace_back(chat_button); - if (can_join_company) this->buttons[line_count].push_back(std::make_unique(SPR_JOIN, STR_NETWORK_CLIENT_LIST_JOIN_TOOLTIP, COLOUR_ORANGE, company_id, &NetworkClientListWindow::OnClickCompanyJoin, company_id != COMPANY_SPECTATOR && Company::Get(company_id)->is_ai)); - - this->line_count += 1; + ButtonLine &company_line = *this->buttons.emplace_back(std::make_unique(company_id)); + if (_network_server) company_line.AddButton(SPR_ADMIN, STR_NETWORK_CLIENT_LIST_ADMIN_COMPANY_TOOLTIP, COLOUR_RED, company_id, &NetworkClientListWindow::OnClickCompanyAdmin, company_id == COMPANY_SPECTATOR); + ButtonCommon &chat_button = company_line.AddButton(SPR_CHAT, company_id == COMPANY_SPECTATOR ? STR_NETWORK_CLIENT_LIST_CHAT_SPECTATOR_TOOLTIP : STR_NETWORK_CLIENT_LIST_CHAT_COMPANY_TOOLTIP, COLOUR_ORANGE, company_id, &NetworkClientListWindow::OnClickCompanyChat); + if (can_join_company) company_line.AddButton(SPR_JOIN, STR_NETWORK_CLIENT_LIST_JOIN_TOOLTIP, COLOUR_ORANGE, company_id, &NetworkClientListWindow::OnClickCompanyJoin, company_id != COMPANY_SPECTATOR && Company::Get(company_id)->is_ai); bool has_players = false; for (const NetworkClientInfo *ci : NetworkClientInfo::Iterate()) { if (ci->client_playas != company_id) continue; has_players = true; - if (_network_server) this->buttons[line_count].push_back(std::make_unique(SPR_ADMIN, STR_NETWORK_CLIENT_LIST_ADMIN_CLIENT_TOOLTIP, COLOUR_RED, ci->client_id, &NetworkClientListWindow::OnClickClientAdmin, _network_own_client_id == ci->client_id)); - if (_network_own_client_id != ci->client_id) this->buttons[line_count].push_back(std::make_unique(SPR_CHAT, STR_NETWORK_CLIENT_LIST_CHAT_CLIENT_TOOLTIP, COLOUR_ORANGE, ci->client_id, &NetworkClientListWindow::OnClickClientChat)); - if (_network_own_client_id != ci->client_id && client_playas != COMPANY_SPECTATOR && !ci->CanJoinCompany(client_playas)) this->buttons[line_count].push_back(std::make_unique(SPR_JOIN, STR_NETWORK_CLIENT_LIST_COMPANY_AUTHORIZE_TOOLTIP, COLOUR_GREEN, ci->client_id, &NetworkClientListWindow::OnClickClientAuthorize)); - - if (ci->client_id == _network_own_client_id) { - this->player_self_index = this->line_count; - } else if (ci->client_id == CLIENT_ID_SERVER) { - this->player_host_index = this->line_count; - } - - this->line_count += 1; + ButtonLine &line = *this->buttons.emplace_back(std::make_unique(ci->index)); + if (_network_server) line.AddButton(SPR_ADMIN, STR_NETWORK_CLIENT_LIST_ADMIN_CLIENT_TOOLTIP, COLOUR_RED, ci->client_id, &NetworkClientListWindow::OnClickClientAdmin, _network_own_client_id == ci->client_id); + if (_network_own_client_id != ci->client_id) line.AddButton(SPR_CHAT, STR_NETWORK_CLIENT_LIST_CHAT_CLIENT_TOOLTIP, COLOUR_ORANGE, ci->client_id, &NetworkClientListWindow::OnClickClientChat); + if (_network_own_client_id != ci->client_id && client_playas != COMPANY_SPECTATOR && !ci->CanJoinCompany(client_playas)) line.AddButton(SPR_JOIN, STR_NETWORK_CLIENT_LIST_COMPANY_AUTHORIZE_TOOLTIP, COLOUR_GREEN, ci->client_id, &NetworkClientListWindow::OnClickClientAuthorize); } /* Disable the chat button when there are players in this company. */ - chat_button->disabled = !has_players; + chat_button.disabled = !has_players; } /** @@ -1543,14 +1683,11 @@ private: CompanyID client_playas = own_ci == nullptr ? COMPANY_SPECTATOR : own_ci->client_playas; this->buttons.clear(); - this->line_count = 0; - this->player_host_index = -1; - this->player_self_index = -1; /* As spectator, show a line to create a new company. */ if (client_playas == COMPANY_SPECTATOR && !NetworkMaxCompaniesReached()) { - this->buttons[line_count].push_back(std::make_unique(SPR_JOIN, STR_NETWORK_CLIENT_LIST_NEW_COMPANY_TOOLTIP, COLOUR_ORANGE, COMPANY_SPECTATOR, &NetworkClientListWindow::OnClickCompanyNew)); - this->line_count += 1; + ButtonLine &line = *this->buttons.emplace_back(std::make_unique(COMPANY_NEW_COMPANY)); + line.AddButton(SPR_JOIN, STR_NETWORK_CLIENT_LIST_NEW_COMPANY_TOOLTIP, COLOUR_ORANGE, COMPANY_SPECTATOR, &NetworkClientListWindow::OnClickCompanyNew); } if (client_playas != COMPANY_SPECTATOR) { @@ -1567,40 +1704,7 @@ private: /* Spectators */ this->RebuildListCompany(COMPANY_SPECTATOR, client_playas, client_playas != COMPANY_SPECTATOR); - this->vscroll->SetCount(this->line_count); - } - - /** - * Get the button at a specific point on the WID_CL_MATRIX. - * @param pt The point to look for a button. - * @return The button or a nullptr if there was none. - */ - ButtonCommon *GetButtonAtPoint(Point pt) - { - uint index = this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_CL_MATRIX); - Rect matrix = this->GetWidget(WID_CL_MATRIX)->GetCurrentRect().Shrink(WidgetDimensions::scaled.framerect); - - bool rtl = _current_text_dir == TD_RTL; - uint x = rtl ? matrix.left : matrix.right; - - /* Find the buttons for this row. */ - auto button_find = this->buttons.find(index); - if (button_find == this->buttons.end()) return nullptr; - - /* Check if we want to display a tooltip for any of the buttons. */ - for (auto &button : button_find->second) { - uint left = rtl ? x : x - button->width; - uint right = rtl ? x + button->width : x; - - if (IsInsideMM(pt.x, left, right)) { - return button.get(); - } - - int width = button->width + WidgetDimensions::scaled.framerect.Horizontal(); - x += rtl ? width : -width; - } - - return nullptr; + this->vscroll->SetCount(this->buttons.size()); } public: @@ -1614,7 +1718,7 @@ public: void OnInit() override { - RebuildList(); + this->RebuildList(); } void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override @@ -1718,7 +1822,11 @@ public: break; case WID_CL_MATRIX: { - ButtonCommon *button = this->GetButtonAtPoint(pt); + auto it = this->vscroll->GetScrolledItemFromWidget(this->buttons, pt.y, this, WID_CL_MATRIX); + if (it == std::end(this->buttons)) break; + + Rect r = this->GetWidget(WID_CL_MATRIX)->GetCurrentRect().Shrink(WidgetDimensions::scaled.framerect); + ButtonCommon *button = (*it)->GetButton(r, pt); if (button == nullptr) break; button->OnClick(this, pt); @@ -1731,34 +1839,14 @@ public: { switch (widget) { case WID_CL_MATRIX: { - int index = this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_CL_MATRIX); + auto it = this->vscroll->GetScrolledItemFromWidget(this->buttons, pt.y, this, WID_CL_MATRIX); + if (it == std::end(this->buttons)) break; - bool rtl = _current_text_dir == TD_RTL; - Rect matrix = this->GetWidget(WID_CL_MATRIX)->GetCurrentRect().Shrink(WidgetDimensions::scaled.framerect); + Rect r = this->GetWidget(WID_CL_MATRIX)->GetCurrentRect().Shrink(WidgetDimensions::scaled.framerect); + auto tooltip = (*it)->GetTooltip(r, pt); + if (!tooltip.has_value()) break; - Dimension d = GetSpriteSize(SPR_COMPANY_ICON); - uint text_left = matrix.left + (rtl ? 0 : d.width + WidgetDimensions::scaled.hsep_wide); - uint text_right = matrix.right - (rtl ? d.width + WidgetDimensions::scaled.hsep_wide : 0); - - Dimension d2 = GetSpriteSize(SPR_PLAYER_SELF); - uint offset_x = WidgetDimensions::scaled.hsep_indent - d2.width - ScaleGUITrad(3); - - uint player_icon_x = rtl ? text_right - offset_x - d2.width : text_left + offset_x; - - if (IsInsideMM(pt.x, player_icon_x, player_icon_x + d2.width)) { - if (index == this->player_self_index) { - GuiShowTooltips(this, GetEncodedString(STR_NETWORK_CLIENT_LIST_PLAYER_ICON_SELF_TOOLTIP), close_cond); - return true; - } else if (index == this->player_host_index) { - GuiShowTooltips(this, GetEncodedString(STR_NETWORK_CLIENT_LIST_PLAYER_ICON_HOST_TOOLTIP), close_cond); - return true; - } - } - - ButtonCommon *button = this->GetButtonAtPoint(pt); - if (button == nullptr) return false; - - GuiShowTooltips(this, GetEncodedString(button->tooltip), close_cond); + GuiShowTooltips(this, std::move(*tooltip), close_cond); return true; }; } @@ -1852,154 +1940,23 @@ public: } } - /** - * Draw the buttons for a single line in the matrix. - * - * The x-position in RTL is the most left or otherwise the most right pixel - * we can draw the buttons from. - * - * @param x The x-position to start with the buttons. Updated during this function. - * @param y The y-position to start with the buttons. - * @param buttons The buttons to draw. - */ - void DrawButtons(int &x, uint y, const std::vector> &buttons) const - { - Rect r; - - for (auto &button : buttons) { - bool rtl = _current_text_dir == TD_RTL; - - int offset = (this->line_height - button->height) / 2; - r.left = rtl ? x : x - button->width + 1; - r.right = rtl ? x + button->width - 1 : x; - r.top = y + offset; - r.bottom = r.top + button->height - 1; - - DrawFrameRect(r, button->colour, {}); - DrawSprite(button->sprite, PAL_NONE, r.left + WidgetDimensions::scaled.framerect.left, r.top + WidgetDimensions::scaled.framerect.top); - if (button->disabled) { - GfxFillRect(r.Shrink(WidgetDimensions::scaled.bevel), GetColourGradient(button->colour, SHADE_DARKER), FILLRECT_CHECKER); - } - - int width = button->width + WidgetDimensions::scaled.hsep_normal; - x += rtl ? width : -width; - } - } - - /** - * Draw a company and its clients on the matrix. - * @param company_id The company to draw. - * @param r The rect to draw within. - * @param line The Nth line we are drawing. Updated during this function. - */ - void DrawCompany(CompanyID company_id, const Rect &r, uint &line) const - { - bool rtl = _current_text_dir == TD_RTL; - int text_y_offset = CentreBounds(0, this->line_height, GetCharacterHeight(FS_NORMAL)); - - Dimension d = GetSpriteSize(SPR_COMPANY_ICON); - int offset = CentreBounds(0, this->line_height, d.height); - - uint line_start = this->vscroll->GetPosition(); - uint line_end = line_start + this->vscroll->GetCapacity(); - - uint y = r.top + (this->line_height * (line - line_start)); - - /* Draw the company line (if in range of scrollbar). */ - if (IsInsideMM(line, line_start, line_end)) { - int icon_left = r.WithWidth(d.width, rtl).left; - Rect tr = r.Indent(d.width + WidgetDimensions::scaled.hsep_normal, rtl); - int &x = rtl ? tr.left : tr.right; - - /* If there are buttons for this company, draw them. */ - auto button_find = this->buttons.find(line); - if (button_find != this->buttons.end()) { - this->DrawButtons(x, y, button_find->second); - } - - if (company_id == COMPANY_SPECTATOR) { - DrawSprite(SPR_COMPANY_ICON, PALETTE_TO_GREY, icon_left, y + offset); - DrawString(tr.left, tr.right, y + text_y_offset, STR_NETWORK_CLIENT_LIST_SPECTATORS, TC_SILVER); - } else if (company_id == COMPANY_NEW_COMPANY) { - DrawSprite(SPR_COMPANY_ICON, PALETTE_TO_GREY, icon_left, y + offset); - DrawString(tr.left, tr.right, y + text_y_offset, STR_NETWORK_CLIENT_LIST_NEW_COMPANY, TC_WHITE); - } else { - DrawCompanyIcon(company_id, icon_left, y + offset); - - DrawString(tr.left, tr.right, y + text_y_offset, GetString(STR_COMPANY_NAME, company_id, company_id), TC_SILVER); - } - } - - y += this->line_height; - line++; - - for (const NetworkClientInfo *ci : NetworkClientInfo::Iterate()) { - if (ci->client_playas != company_id) continue; - - /* Draw the player line (if in range of scrollbar). */ - if (IsInsideMM(line, line_start, line_end)) { - Rect tr = r.Indent(WidgetDimensions::scaled.hsep_indent, rtl); - - /* If there are buttons for this client, draw them. */ - auto button_find = this->buttons.find(line); - if (button_find != this->buttons.end()) { - int &x = rtl ? tr.left : tr.right; - this->DrawButtons(x, y, button_find->second); - } - - SpriteID player_icon = 0; - if (ci->client_id == _network_own_client_id) { - player_icon = SPR_PLAYER_SELF; - } else if (ci->client_id == CLIENT_ID_SERVER) { - player_icon = SPR_PLAYER_HOST; - } - - if (player_icon != 0) { - Dimension d2 = GetSpriteSize(player_icon); - int offset_y = CentreBounds(0, this->line_height, d2.height); - DrawSprite(player_icon, PALETTE_TO_GREY, rtl ? tr.right - d2.width : tr.left, y + offset_y); - tr = tr.Indent(d2.width + WidgetDimensions::scaled.hsep_normal, rtl); - } - - DrawString(tr.left, tr.right, y + text_y_offset, GetString(STR_JUST_RAW_STRING, ci->client_name), TC_BLACK); - } - - y += this->line_height; - line++; - } - } - void DrawWidget(const Rect &r, WidgetID widget) const override { switch (widget) { case WID_CL_MATRIX: { - Rect ir = r.Shrink(WidgetDimensions::scaled.framerect, RectPadding::zero); - uint line = 0; + Rect ir = r.WithHeight(this->line_height).Shrink(WidgetDimensions::scaled.framerect, RectPadding::zero); if (this->hover_index >= 0) { Rect br = r.WithHeight(this->line_height).Translate(0, this->hover_index * this->line_height); GfxFillRect(br.Shrink(WidgetDimensions::scaled.bevel), GREY_SCALE(9)); } - NetworkClientInfo *own_ci = NetworkClientInfo::GetByClientID(_network_own_client_id); - CompanyID client_playas = own_ci == nullptr ? COMPANY_SPECTATOR : own_ci->client_playas; - - if (client_playas == COMPANY_SPECTATOR && !NetworkMaxCompaniesReached()) { - this->DrawCompany(COMPANY_NEW_COMPANY, ir, line); + auto [first, last] = this->vscroll->GetVisibleRangeIterators(this->buttons); + for (auto it = first; it != last; ++it) { + (*it)->Draw(ir); + ir = ir.Translate(0, this->line_height); } - if (client_playas != COMPANY_SPECTATOR) { - this->DrawCompany(client_playas, ir, line); - } - - for (const Company *c : Company::Iterate()) { - if (client_playas == c->index) continue; - this->DrawCompany(c->index, ir, line); - } - - /* Spectators */ - this->DrawCompany(COMPANY_SPECTATOR, ir, line); - break; } }