/* $Id$ */ /** @file widget.cpp Handling of the default/simple widgets. */ #include "stdafx.h" #include "company_func.h" #include "gfx_func.h" #include "window_gui.h" #include "debug.h" #include "strings_func.h" #include "table/sprites.h" #include "table/strings.h" static const char *UPARROW = "\xEE\x8A\xA0"; ///< String containing an upwards pointing arrow. static const char *DOWNARROW = "\xEE\x8A\xAA"; ///< String containing a downwards pointing arrow. /** * Compute the vertical position of the draggable part of scrollbar * @param sb Scrollbar list data * @param top Top position of the scrollbar (top position of the up-button) * @param bottom Bottom position of the scrollbar (bottom position of the down-button) * @return A Point, with x containing the top coordinate of the draggable part, and * y containing the bottom coordinate of the draggable part */ static Point HandleScrollbarHittest(const Scrollbar *sb, int top, int bottom) { Point pt; int height, count, pos, cap; top += 10; // top points to just below the up-button bottom -= 9; // bottom points to top of the down-button height = (bottom - top); pos = sb->pos; count = sb->count; cap = sb->cap; if (count != 0) top += height * pos / count; if (cap > count) cap = count; if (count != 0) bottom -= (count - pos - cap) * height / count; pt.x = top; pt.y = bottom - 1; return pt; } /** Special handling for the scrollbar widget type. * Handles the special scrolling buttons and other * scrolling. * @param w Window on which a scroll was performed. * @param wi Pointer to the scrollbar widget. * @param x The X coordinate of the mouse click. * @param y The Y coordinate of the mouse click. */ void ScrollbarClickHandler(Window *w, const Widget *wi, int x, int y) { int mi, ma, pos; Scrollbar *sb; switch (wi->type) { case WWT_SCROLLBAR: /* vertical scroller */ w->flags4 &= ~WF_HSCROLL; w->flags4 &= ~WF_SCROLL2; mi = wi->top; ma = wi->bottom; pos = y; sb = &w->vscroll; break; case WWT_SCROLL2BAR: /* 2nd vertical scroller */ w->flags4 &= ~WF_HSCROLL; w->flags4 |= WF_SCROLL2; mi = wi->top; ma = wi->bottom; pos = y; sb = &w->vscroll2; break; case WWT_HSCROLLBAR: /* horizontal scroller */ w->flags4 &= ~WF_SCROLL2; w->flags4 |= WF_HSCROLL; mi = wi->left; ma = wi->right; pos = x; sb = &w->hscroll; break; default: NOT_REACHED(); } if (pos <= mi + 9) { /* Pressing the upper button? */ w->flags4 |= WF_SCROLL_UP; if (_scroller_click_timeout == 0) { _scroller_click_timeout = 6; if (sb->pos != 0) sb->pos--; } _left_button_clicked = false; } else if (pos >= ma - 10) { /* Pressing the lower button? */ w->flags4 |= WF_SCROLL_DOWN; if (_scroller_click_timeout == 0) { _scroller_click_timeout = 6; if ((byte)(sb->pos + sb->cap) < sb->count) sb->pos++; } _left_button_clicked = false; } else { Point pt = HandleScrollbarHittest(sb, mi, ma); if (pos < pt.x) { sb->pos = max(sb->pos - sb->cap, 0); } else if (pos > pt.y) { sb->pos = min( sb->pos + sb->cap, max(sb->count - sb->cap, 0) ); } else { _scrollbar_start_pos = pt.x - mi - 9; _scrollbar_size = ma - mi - 23; w->flags4 |= WF_SCROLL_MIDDLE; _scrolling_scrollbar = true; _cursorpos_drag_start = _cursor.pos; } } w->SetDirty(); } /** Returns the index for the widget located at the given position * relative to the window. It includes all widget-corner pixels as well. * @param *w Window to look inside * @param x The Window client X coordinate * @param y The Window client y coordinate * @return A widget index, or -1 if no widget was found. */ int GetWidgetFromPos(const Window *w, int x, int y) { uint index; int found_index = -1; /* Go through the widgets and check if we find the widget that the coordinate is * inside. */ for (index = 0; index < w->widget_count; index++) { const Widget *wi = &w->widget[index]; if (wi->type == WWT_EMPTY || wi->type == WWT_FRAME) continue; if (x >= wi->left && x <= wi->right && y >= wi->top && y <= wi->bottom && !w->IsWidgetHidden(index)) { found_index = index; } } return found_index; } /** * Draw frame rectangle. * @param left Left edge of the frame * @param top Top edge of the frame * @param right Right edge of the frame * @param bottom Bottom edge of the frame * @param colour Colour table to use. @see _colour_gradient * @param flags Flags controlling how to draw the frame. @see FrameFlags */ void DrawFrameRect(int left, int top, int right, int bottom, Colours colour, FrameFlags flags) { uint dark = _colour_gradient[colour][3]; uint medium_dark = _colour_gradient[colour][5]; uint medium_light = _colour_gradient[colour][6]; uint light = _colour_gradient[colour][7]; if (flags & FR_TRANSPARENT) { GfxFillRect(left, top, right, bottom, PALETTE_TO_TRANSPARENT, FILLRECT_RECOLOUR); } else { uint interior; if (flags & FR_LOWERED) { GfxFillRect(left, top, left, bottom, dark); GfxFillRect(left + 1, top, right, top, dark); GfxFillRect(right, top + 1, right, bottom - 1, light); GfxFillRect(left + 1, bottom, right, bottom, light); interior = (flags & FR_DARKENED ? medium_dark : medium_light); } else { GfxFillRect(left, top, left, bottom - 1, light); GfxFillRect(left + 1, top, right - 1, top, light); GfxFillRect(right, top, right, bottom - 1, dark); GfxFillRect(left, bottom, right, bottom, dark); interior = medium_dark; } if (!(flags & FR_BORDERONLY)) { GfxFillRect(left + 1, top + 1, right - 1, bottom - 1, interior); } } } /** * Draw an image button. * @param r Rectangle of the button. * @param type Widget type (#WWT_IMGBTN or #WWT_IMGBTN_2). * @param colour Colour of the button. * @param clicked Button is lowered. * @param img Sprite to draw. */ static inline void DrawImageButtons(const Rect &r, WidgetType type, Colours colour, bool clicked, SpriteID img) { assert(img != 0); DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, (clicked) ? FR_LOWERED : FR_NONE); /* show different image when clicked for WWT_IMGBTN_2 */ if ((type & WWT_MASK) == WWT_IMGBTN_2 && clicked) img++; DrawSprite(img, PAL_NONE, r.left + 1 + clicked, r.top + 1 + clicked); } /** * Draw the label-part of a widget. * @param r Rectangle of the label background. * @param type Widget type (#WWT_TEXTBTN, #WWT_TEXTBTN_2, or #WWT_LABEL). * @param clicked Label is rendered lowered. * @param str Text to draw. */ static inline void DrawLabel(const Rect &r, WidgetType type, bool clicked, StringID str) { if ((type & WWT_MASK) == WWT_TEXTBTN_2 && clicked) str++; DrawString(r.left + clicked, r.right + clicked, ((r.top + r.bottom + 1) >> 1) - 5 + clicked, str, TC_FROMSTRING, SA_CENTER); } /** * Draw text. * @param r Rectangle of the background. * @param colour Colour of the text. * @param str Text to draw. */ static inline void DrawText(const Rect &r, TextColour colour, StringID str) { if (str != STR_NULL) DrawString(r.left, r.right, r.top, str, colour); } /** * Draw an inset widget. * @param r Rectangle of the background. * @param colour Colour of the inset. * @param str Text to draw. */ static inline void DrawInset(const Rect &r, Colours colour, StringID str) { DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, FR_LOWERED | FR_DARKENED); if (str != STR_NULL) DrawString(r.left + 2, r.right - 2, r.top + 1, str); } /** * Draw a matrix widget. * @param r Rectangle of the matrix background. * @param colour Colour of the background. * @param clicked Matrix is rendered lowered. * @param data Data of the widget. */ static inline void DrawMatrix(const Rect &r, Colours colour, bool clicked, uint16 data) { DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, (clicked) ? FR_LOWERED : FR_NONE); int c = GB(data, 0, 8); int amt1 = (r.right - r.left + 1) / c; int d = GB(data, 8, 8); int amt2 = (r.bottom - r.top + 1) / d; int col = _colour_gradient[colour & 0xF][6]; int x = r.left; for (int ctr = c; ctr > 1; ctr--) { x += amt1; GfxFillRect(x, r.top + 1, x, r.bottom - 1, col); } x = r.top; for (int ctr = d; ctr > 1; ctr--) { x += amt2; GfxFillRect(r.left + 1, x, r.right - 1, x, col); } col = _colour_gradient[colour & 0xF][4]; x = r.left - 1; for (int ctr = c; ctr > 1; ctr--) { x += amt1; GfxFillRect(x, r.top + 1, x, r.bottom - 1, col); } x = r.top - 1; for (int ctr = d; ctr > 1; ctr--) { x += amt2; GfxFillRect(r.left + 1, x, r.right - 1, x, col); } } /** * Draw a vertical scrollbar. * @param r Rectangle of the scrollbar widget. * @param colour Colour of the scrollbar widget. * @param up_clicked Up-arrow is clicked. * @param bar_dragged Bar is dragged. * @param down_clicked Down-arrow is clicked. * @param scrollbar Scrollbar size, offset, and capacity information. */ static inline void DrawVerticalScrollbar(const Rect &r, Colours colour, bool up_clicked, bool bar_dragged, bool down_clicked, const Scrollbar *scrollbar) { assert(r.right - r.left == 11); // To ensure the same sizes are used everywhere! /* draw up/down buttons */ DrawFrameRect(r.left, r.top, r.right, r.top + 9, colour, (up_clicked) ? FR_LOWERED : FR_NONE); DrawString(r.left + up_clicked, r.right + up_clicked, r.top + up_clicked, UPARROW, TC_BLACK, SA_CENTER); DrawFrameRect(r.left, r.bottom - 9, r.right, r.bottom, colour, (down_clicked) ? FR_LOWERED : FR_NONE); DrawString(r.left + down_clicked, r.right + down_clicked, r.bottom - 9 + down_clicked, DOWNARROW, TC_BLACK, SA_CENTER); int c1 = _colour_gradient[colour & 0xF][3]; int c2 = _colour_gradient[colour & 0xF][7]; /* draw "shaded" background */ GfxFillRect(r.left, r.top + 10, r.right, r.bottom - 10, c2); GfxFillRect(r.left, r.top + 10, r.right, r.bottom - 10, c1, FILLRECT_CHECKER); /* draw shaded lines */ GfxFillRect(r.left + 2, r.top + 10, r.left + 2, r.bottom - 10, c1); GfxFillRect(r.left + 3, r.top + 10, r.left + 3, r.bottom - 10, c2); GfxFillRect(r.left + 7, r.top + 10, r.left + 7, r.bottom - 10, c1); GfxFillRect(r.left + 8, r.top + 10, r.left + 8, r.bottom - 10, c2); Point pt = HandleScrollbarHittest(scrollbar, r.top, r.bottom); DrawFrameRect(r.left, pt.x, r.right, pt.y, colour, bar_dragged ? FR_LOWERED : FR_NONE); } /** * Draw a horizontal scrollbar. * @param r Rectangle of the scrollbar widget. * @param colour Colour of the scrollbar widget. * @param left_clicked Left-arrow is clicked. * @param bar_dragged Bar is dragged. * @param right_clicked Right-arrow is clicked. * @param scrollbar Scrollbar size, offset, and capacity information. */ static inline void DrawHorizontalScrollbar(const Rect &r, Colours colour, bool left_clicked, bool bar_dragged, bool right_clicked, const Scrollbar *scrollbar) { assert(r.bottom - r.top == 11); // To ensure the same sizes are used everywhere! DrawFrameRect(r.left, r.top, r.left + 9, r.bottom, colour, left_clicked ? FR_LOWERED : FR_NONE); DrawSprite(SPR_ARROW_LEFT, PAL_NONE, r.left + 1 + left_clicked, r.top + 1 + left_clicked); DrawFrameRect(r.right - 9, r.top, r.right, r.bottom, colour, right_clicked ? FR_LOWERED : FR_NONE); DrawSprite(SPR_ARROW_RIGHT, PAL_NONE, r.right - 8 + right_clicked, r.top + 1 + right_clicked); int c1 = _colour_gradient[colour & 0xF][3]; int c2 = _colour_gradient[colour & 0xF][7]; /* draw "shaded" background */ GfxFillRect(r.left + 10, r.top, r.right - 10, r.bottom, c2); GfxFillRect(r.left + 10, r.top, r.right - 10, r.bottom, c1, FILLRECT_CHECKER); /* draw shaded lines */ GfxFillRect(r.left + 10, r.top + 2, r.right - 10, r.top + 2, c1); GfxFillRect(r.left + 10, r.top + 3, r.right - 10, r.top + 3, c2); GfxFillRect(r.left + 10, r.top + 7, r.right - 10, r.top + 7, c1); GfxFillRect(r.left + 10, r.top + 8, r.right - 10, r.top + 8, c2); /* draw actual scrollbar */ Point pt = HandleScrollbarHittest(scrollbar, r.left, r.right); DrawFrameRect(pt.x, r.top, pt.y, r.bottom, colour, bar_dragged ? FR_LOWERED : FR_NONE); } /** * Draw a frame widget. * @param r Rectangle of the frame. * @param colour Colour of the frame. * @param str Text of the frame. */ static inline void DrawFrame(const Rect &r, Colours colour, StringID str) { int x2 = r.left; // by default the left side is the left side of the widget if (str != STR_NULL) x2 = DrawString(r.left + 6, r.right - 6, r.top, str); int c1 = _colour_gradient[colour][3]; int c2 = _colour_gradient[colour][7]; if (_dynlang.text_dir == TD_LTR) { /* Line from upper left corner to start of text */ GfxFillRect(r.left, r.top + 4, r.left + 4, r.top + 4, c1); GfxFillRect(r.left + 1, r.top + 5, r.left + 4, r.top + 5, c2); /* Line from end of text to upper right corner */ GfxFillRect(x2, r.top + 4, r.right - 1, r.top + 4, c1); GfxFillRect(x2, r.top + 5, r.right - 2, r.top + 5, c2); } else { /* Line from upper left corner to start of text */ GfxFillRect(r.left, r.top + 4, x2 - 2, r.top + 4, c1); GfxFillRect(r.left + 1, r.top + 5, x2 - 2, r.top + 5, c2); /* Line from end of text to upper right corner */ GfxFillRect(r.right - 5, r.top + 4, r.right - 1, r.top + 4, c1); GfxFillRect(r.right - 5, r.top + 5, r.right - 2, r.top + 5, c2); } /* Line from upper left corner to bottom left corner */ GfxFillRect(r.left, r.top + 5, r.left, r.bottom - 1, c1); GfxFillRect(r.left + 1, r.top + 6, r.left + 1, r.bottom - 2, c2); /* Line from upper right corner to bottom right corner */ GfxFillRect(r.right - 1, r.top + 5, r.right - 1, r.bottom - 2, c1); GfxFillRect(r.right, r.top + 4, r.right, r.bottom - 1, c2); GfxFillRect(r.left + 1, r.bottom - 1, r.right - 1, r.bottom - 1, c1); GfxFillRect(r.left, r.bottom, r.right, r.bottom, c2); } /** * Draw a sticky box. * @param r Rectangle of the box. * @param colour Colour of the sticky box. * @param clicked Box is lowered. */ static inline void DrawStickyBox(const Rect &r, Colours colour, bool clicked) { assert(r.right - r.left == 11); // To ensure the same sizes are used everywhere! DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, (clicked) ? FR_LOWERED : FR_NONE); DrawSprite((clicked) ? SPR_PIN_UP : SPR_PIN_DOWN, PAL_NONE, r.left + 2 + clicked, r.top + 3 + clicked); } /** * Draw a resize box. * @param r Rectangle of the box. * @param colour Colour of the resize box. * @param at_left Resize box is at left-side of the window, * @param clicked Box is lowered. */ static inline void DrawResizeBox(const Rect &r, Colours colour, bool at_left, bool clicked) { assert(r.right - r.left == 11); // To ensure the same sizes are used everywhere! DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, (clicked) ? FR_LOWERED : FR_NONE); if (at_left) { DrawSprite(SPR_WINDOW_RESIZE_LEFT, PAL_NONE, r.left + 2, r.top + 3 + clicked); } else { DrawSprite(SPR_WINDOW_RESIZE_RIGHT, PAL_NONE, r.left + 3 + clicked, r.top + 3 + clicked); } } /** * Draw a close box. * @param r Rectangle of the box. * @param colour Colour of the close box. * @param str Cross to draw (#STR_BLACK_CROSS or #STR_SILVER_CROSS). */ static inline void DrawCloseBox(const Rect &r, Colours colour, StringID str) { assert(str == STR_BLACK_CROSS || str == STR_SILVER_CROSS); // black or silver cross assert(r.right - r.left == 10); // To ensure the same sizes are used everywhere DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, FR_NONE); DrawString(r.left, r.right, r.top + 2, str, TC_FROMSTRING, SA_CENTER); } /** * Draw a caption bar. * @param r Rectangle of the bar. * @param colour Colour of the window. * @param owner 'Owner' of the window. * @param str Text to draw in the bar. */ static inline void DrawCaption(const Rect &r, Colours colour, Owner owner, StringID str) { assert(r.bottom - r.top == 13); // To ensure the same sizes are used everywhere! DrawFrameRect(r.left, r.top, r.right, r.bottom, colour, FR_BORDERONLY); DrawFrameRect(r.left + 1, r.top + 1, r.right - 1, r.bottom - 1, colour, (owner == INVALID_OWNER) ? FR_LOWERED | FR_DARKENED : FR_LOWERED | FR_DARKENED | FR_BORDERONLY); if (owner != INVALID_OWNER) { GfxFillRect(r.left + 2, r.top + 2, r.right - 2, r.bottom - 2, _colour_gradient[_company_colours[owner]][4]); } DrawString(r.left + 2, r.right - 2, r.top + 2, str, TC_FROMSTRING, SA_CENTER); } static inline void DrawDropdown(const Rect &r, Colours colour, bool clicked, StringID str) { assert(r.bottom - r.top == 11); // ensure consistent size if (_dynlang.text_dir == TD_LTR) { DrawFrameRect(r.left, r.top, r.right - 12, r.bottom, colour, FR_NONE); DrawFrameRect(r.right - 11, r.top, r.right, r.bottom, colour, clicked ? FR_LOWERED : FR_NONE); DrawString(r.right - (clicked ? 10 : 11), r.right, r.top + (clicked ? 2 : 1), STR_ARROW_DOWN, TC_BLACK, SA_CENTER); if (str != STR_NULL) DrawString(r.left + 2, r.right - 14, r.top + 1, str, TC_BLACK); } else { DrawFrameRect(r.left + 12, r.top, r.right, r.bottom, colour, FR_NONE); DrawFrameRect(r.left, r.top, r.left + 11, r.bottom, colour, clicked ? FR_LOWERED : FR_NONE); DrawString(r.left + clicked, r.left + 11, r.top + (clicked ? 2 : 1), STR_ARROW_DOWN, TC_BLACK, SA_CENTER); if (str != STR_NULL) DrawString(r.left + 14, r.right - 2, r.top + 1, str, TC_BLACK); } } /** * Paint all widgets of a window. */ void Window::DrawWidgets() const { const DrawPixelInfo *dpi = _cur_dpi; for (uint i = 0; i < this->widget_count; i++) { const Widget *wi = &this->widget[i]; bool clicked = this->IsWidgetLowered(i); Rect r; if (dpi->left > (r.right = wi->right) || dpi->left + dpi->width <= (r.left = wi->left) || dpi->top > (r.bottom = wi->bottom) || dpi->top + dpi->height <= (r.top = wi->top) || this->IsWidgetHidden(i)) { continue; } switch (wi->type & WWT_MASK) { case WWT_IMGBTN: case WWT_IMGBTN_2: DrawImageButtons(r, wi->type,wi->colour, clicked, wi->data); break; case WWT_PANEL: assert(wi->data == 0); DrawFrameRect(r.left, r.top, r.right, r.bottom, wi->colour, (clicked) ? FR_LOWERED : FR_NONE); break; case WWT_EDITBOX: DrawFrameRect(r.left, r.top, r.right, r.bottom, wi->colour, FR_LOWERED | FR_DARKENED); break; case WWT_TEXTBTN: case WWT_TEXTBTN_2: DrawFrameRect(r.left, r.top, r.right, r.bottom, wi->colour, (clicked) ? FR_LOWERED : FR_NONE); /* FALL THROUGH */ case WWT_LABEL: DrawLabel(r, wi->type, clicked, wi->data); break; case WWT_TEXT: DrawText(r, (TextColour)wi->colour, wi->data); break; case WWT_INSET: DrawInset(r, wi->colour, wi->data); break; case WWT_MATRIX: DrawMatrix(r, wi->colour, clicked, wi->data); break; /* vertical scrollbar */ case WWT_SCROLLBAR: assert(wi->data == 0); DrawVerticalScrollbar(r, wi->colour, (this->flags4 & (WF_SCROLL_UP | WF_HSCROLL | WF_SCROLL2)) == WF_SCROLL_UP, (this->flags4 & (WF_SCROLL_MIDDLE | WF_HSCROLL | WF_SCROLL2)) == WF_SCROLL_MIDDLE, (this->flags4 & (WF_SCROLL_DOWN | WF_HSCROLL | WF_SCROLL2)) == WF_SCROLL_DOWN, &this->vscroll); break; case WWT_SCROLL2BAR: assert(wi->data == 0); DrawVerticalScrollbar(r, wi->colour, (this->flags4 & (WF_SCROLL_UP | WF_HSCROLL | WF_SCROLL2)) == (WF_SCROLL_UP | WF_SCROLL2), (this->flags4 & (WF_SCROLL_MIDDLE | WF_HSCROLL | WF_SCROLL2)) == (WF_SCROLL_MIDDLE | WF_SCROLL2), (this->flags4 & (WF_SCROLL_DOWN | WF_HSCROLL | WF_SCROLL2)) == (WF_SCROLL_DOWN | WF_SCROLL2), &this->vscroll2); break; /* horizontal scrollbar */ case WWT_HSCROLLBAR: assert(wi->data == 0); DrawHorizontalScrollbar(r, wi->colour, (this->flags4 & (WF_SCROLL_UP | WF_HSCROLL)) == (WF_SCROLL_UP | WF_HSCROLL), (this->flags4 & (WF_SCROLL_MIDDLE | WF_HSCROLL)) == (WF_SCROLL_MIDDLE | WF_HSCROLL), (this->flags4 & (WF_SCROLL_DOWN | WF_HSCROLL)) == (WF_SCROLL_DOWN | WF_HSCROLL), &this->hscroll); break; case WWT_FRAME: DrawFrame(r, wi->colour, wi->data); break; case WWT_STICKYBOX: assert(wi->data == 0); DrawStickyBox(r, wi->colour, !!(this->flags4 & WF_STICKY)); break; case WWT_RESIZEBOX: assert(wi->data == 0); DrawResizeBox(r, wi->colour, wi->left < (this->width / 2), !!(this->flags4 & WF_SIZING)); break; case WWT_CLOSEBOX: DrawCloseBox(r, wi->colour, wi->data); break; case WWT_CAPTION: DrawCaption(r, wi->colour, this->owner, wi->data); break; case WWT_DROPDOWN: DrawDropdown(r, wi->colour, clicked, wi->data); break; } if (this->IsWidgetDisabled(i)) { GfxFillRect(r.left + 1, r.top + 1, r.right - 1, r.bottom - 1, _colour_gradient[wi->colour & 0xF][2], FILLRECT_CHECKER); } } if (this->flags4 & WF_WHITE_BORDER_MASK) { DrawFrameRect(0, 0, this->width - 1, this->height - 1, COLOUR_WHITE, FR_BORDERONLY); } } /** * Evenly distribute the combined horizontal length of two consecutive widgets. * @param w Window containing the widgets. * @param a Left widget to resize. * @param b Right widget to resize. * @note Widgets are assumed to lie against each other. */ static void ResizeWidgets(Window *w, byte a, byte b) { int16 offset = w->widget[a].left; int16 length = w->widget[b].right - offset; w->widget[a].right = (length / 2) + offset; w->widget[b].left = w->widget[a].right + 1; } /** * Evenly distribute the combined horizontal length of three consecutive widgets. * @param w Window containing the widgets. * @param a Left widget to resize. * @param b Middle widget to resize. * @param c Right widget to resize. * @note Widgets are assumed to lie against each other. */ static void ResizeWidgets(Window *w, byte a, byte b, byte c) { int16 offset = w->widget[a].left; int16 length = w->widget[c].right - offset; w->widget[a].right = length / 3; w->widget[b].right = w->widget[a].right * 2; w->widget[a].right += offset; w->widget[b].right += offset; /* Now the right side of the buttons are set. We will now set the left sides next to them */ w->widget[b].left = w->widget[a].right + 1; w->widget[c].left = w->widget[b].right + 1; } /** Evenly distribute some widgets when resizing horizontally (often a button row) * When only two arguments are given, the widgets are presumed to be on a line and only the ends are given * @param w Window to modify * @param left The leftmost widget to resize * @param right The rightmost widget to resize. Since right side of it is used, remember to set it to RESIZE_RIGHT */ void ResizeButtons(Window *w, byte left, byte right) { int16 num_widgets = right - left + 1; if (num_widgets < 2) NOT_REACHED(); switch (num_widgets) { case 2: ResizeWidgets(w, left, right); break; case 3: ResizeWidgets(w, left, left + 1, right); break; default: { /* Looks like we got more than 3 widgets to resize * Now we will find the middle of the space desinated for the widgets * and place half of the widgets on each side of it and call recursively. * Eventually we will get down to blocks of 2-3 widgets and we got code to handle those cases */ int16 offset = w->widget[left].left; int16 length = w->widget[right].right - offset; byte widget = ((num_widgets - 1)/ 2) + left; // rightmost widget of the left side /* Now we need to find the middle of the widgets. * It will not always be the middle because if we got an uneven number of widgets, * we will need it to be 2/5, 3/7 and so on * To get this, we multiply with num_widgets/num_widgets. Since we calculate in int, we will get: * * num_widgets/2 (rounding down) * --------------- * num_widgets * * as multiplier to length. We just multiply before divide to that we stay in the int area though */ int16 middle = ((length * num_widgets) / (2 * num_widgets)) + offset; /* Set left and right on the widgets, that's next to our "middle" */ w->widget[widget].right = middle; w->widget[widget + 1].left = w->widget[widget].right + 1; /* Now resize the left and right of the middle */ ResizeButtons(w, left, widget); ResizeButtons(w, widget + 1, right); } } } /** Resize a widget and shuffle other widgets around to fit. */ void ResizeWindowForWidget(Window *w, uint widget, int delta_x, int delta_y) { int right = w->widget[widget].right; int bottom = w->widget[widget].bottom; for (uint i = 0; i < w->widget_count; i++) { if (w->widget[i].left >= right && i != widget) w->widget[i].left += delta_x; if (w->widget[i].right >= right) w->widget[i].right += delta_x; if (w->widget[i].top >= bottom && i != widget) w->widget[i].top += delta_y; if (w->widget[i].bottom >= bottom) w->widget[i].bottom += delta_y; } /* A hidden widget has bottom == top or right == left, we need to make it * one less to fit in its new gap. */ if (right == w->widget[widget].left) w->widget[widget].right--; if (bottom == w->widget[widget].top) w->widget[widget].bottom--; if (w->widget[widget].left > w->widget[widget].right) w->widget[widget].right = w->widget[widget].left; if (w->widget[widget].top > w->widget[widget].bottom) w->widget[widget].bottom = w->widget[widget].top; w->width += delta_x; w->height += delta_y; w->resize.width += delta_x; w->resize.height += delta_y; } /** * Draw a sort button's up or down arrow symbol. * @param widget Sort button widget * @param state State of sort button */ void Window::DrawSortButtonState(int widget, SortButtonState state) const { if (state == SBS_OFF) return; int offset = this->IsWidgetLowered(widget) ? 1 : 0; int base = offset + (_dynlang.text_dir == TD_LTR ? this->widget[widget].right - 11 : this->widget[widget].left); DrawString(base, base + 11, this->widget[widget].top + 1 + offset, state == SBS_DOWN ? DOWNARROW : UPARROW, TC_BLACK, SA_CENTER); } /** * @defgroup NestedWidgets Hierarchical widgets. * Hierarchical widgets, also known as nested widgets, are widgets stored in a tree. At the leafs of the tree are (mostly) the 'real' widgets * visible to the user. At higher levels, widgets get organized in container widgets, until all widgets of the window are merged. * * \section nestedwidgetkinds Hierarchical widget kinds * A leaf widget is one of * * The purpose of a leaf widget is to provide interaction with the user by displaying settings, and/or allowing changing the settings. * * A container widget is one of * * The purpose of a container widget is to structure its leafs and sub-containers to allow proper resizing. * * \section nestedwidgetscomputations Hierarchical widget computations * The first 'computation' is the creation of the nested widgets tree by calling the constructors of the widgets listed above and calling \c Add() for every child, * or by means of specifying the tree as a collection of nested widgets parts and instantiating the tree from the array. * * After the creation step, * - The leafs have their own minimal size (\e min_x, \e min_y), filling (\e fill_x, \e fill_y), and resize steps (\e resize_x, \e resize_y). * - Containers only know what their children are, \e fill_x, \e fill_y, \e resize_x, and \e resize_y are not initialized. * * Computations in the nested widgets take place as follows: *
    *
  1. A bottom-up sweep by recursively calling NWidgetBase::SetupSmallestSize() to initialize the smallest size (\e smallest_x, \e smallest_y) and * to propagate filling and resize steps upwards to the root of the tree. *
  2. A top-down sweep by recursively calling NWidgetBase::AssignSizePosition() to make the smallest sizes consistent over the entire tree, and to assign * the top-left (\e pos_x, \e pos_y) position of each widget in the tree. This step uses \e fill_x and \e fill_y at each node in the tree to decide how to * fill each widget towards consistent sizes. * For generating a widget array, resize step sizes are made consistent. *
* * @see NestedWidgetParts */ /** * Base class constructor. * @param tp Nested widget type. */ NWidgetBase::NWidgetBase(WidgetType tp) : ZeroedMemoryAllocator() { this->type = tp; } /* ~NWidgetContainer() takes care of #next and #prev data members. */ /** * @fn int NWidgetBase::SetupSmallestSize() * @brief Compute smallest size needed by the widget. * * The smallest size of a widget is the smallest size that a widget needs to * display itself properly. * In addition, filling and resizing of the widget are computed. * @return Biggest index in the widget array of all child widgets. * * @note After the computation, the results can be queried by accessing the data members of the widget. */ /** * @fn void NWidgetBase::AssignSizePosition(uint x, uint y, uint given_width, uint given_height, bool allow_resize_x, bool allow_resize_y, bool rtl) * @brief Assign size and position to the widget. * @param x Horizontal offset of the widget relative to the left edge of the window. * @param y Vertical offset of the widget relative to the top edge of the window. * @param given_width Width allocated to the widget. * @param given_height Height allocated to the widget. * @param allow_resize_x Horizontal resizing is allowed. * @param allow_resize_y Vertical resizing is allowed. * @param rtl Adapt for right-to-left languages (position contents of horizontal containers backwards). */ /** * @fn void NWidgetBase::StoreWidgets(Widget *widgets, int length, bool left_moving, bool top_moving, bool rtl) * @brief Store all child widgets with a valid index into the widget array. * @param widgets Widget array to store the nested widgets in. * @param length Length of the array. * @param left_moving Left edge of the widget may move due to resizing (right edge if \a rtl). * @param top_moving Top edge of the widget may move due to resizing. * @param rtl Adapt for right-to-left languages (position contents of horizontal containers backwards). * * @note When storing a nested widget, the function should check first that the type in the \a widgets array is #WWT_LAST. * This is used to detect double widget allocations as well as holes in the widget array. */ /** * Constructor for resizable nested widgets. * @param tp Nested widget type. * @param fill_x Allow horizontal filling from initial size. * @param fill_y Allow vertical filling from initial size. */ NWidgetResizeBase::NWidgetResizeBase(WidgetType tp, bool fill_x, bool fill_y) : NWidgetBase(tp) { this->fill_x = fill_x; this->fill_y = fill_y; } /** * Set minimal size of the widget. * @param min_x Horizontal minimal size of the widget. * @param min_y Vertical minimal size of the widget. */ void NWidgetResizeBase::SetMinimalSize(uint min_x, uint min_y) { this->min_x = min_x; this->min_y = min_y; } /** * Set the filling of the widget from initial size. * @param fill_x Allow horizontal filling from initial size. * @param fill_y Allow vertical filling from initial size. */ void NWidgetResizeBase::SetFill(bool fill_x, bool fill_y) { this->fill_x = fill_x; this->fill_y = fill_y; } /** * Set resize step of the widget. * @param resize_x Resize step in horizontal direction, value \c 0 means no resize, otherwise the step size in pixels. * @param resize_y Resize step in vertical direction, value \c 0 means no resize, otherwise the step size in pixels. */ void NWidgetResizeBase::SetResize(uint resize_x, uint resize_y) { this->resize_x = resize_x; this->resize_y = resize_y; } void NWidgetResizeBase::AssignSizePosition(uint x, uint y, uint given_width, uint given_height, bool allow_resize_x, bool allow_resize_y, bool rtl) { this->pos_x = x; this->pos_y = y; this->smallest_x = given_width; this->smallest_y = given_height; if (!allow_resize_x) this->resize_x = 0; if (!allow_resize_y) this->resize_y = 0; } /** * Initialization of a 'real' widget. * @param tp Type of the widget. * @param colour Colour of the widget. * @param fill_x Default horizontal filling. * @param fill_y Default vertical filling. * @param widget_data Data component of the widget. @see Widget::data * @param tool_tip Tool tip of the widget. @see Widget::tootips */ NWidgetCore::NWidgetCore(WidgetType tp, Colours colour, bool fill_x, bool fill_y, uint16 widget_data, StringID tool_tip) : NWidgetResizeBase(tp, fill_x, fill_y) { this->colour = colour; this->index = -1; this->widget_data = widget_data; this->tool_tip = tool_tip; } /** * Set index of the nested widget in the widget array. * @param index Index to use. */ void NWidgetCore::SetIndex(int index) { assert(index >= 0); this->index = index; } /** * Set data and tool tip of the nested widget. * @param widget_data Data to use. * @param tool_tip Tool tip string to use. */ void NWidgetCore::SetDataTip(uint16 widget_data, StringID tool_tip) { this->widget_data = widget_data; this->tool_tip = tool_tip; } int NWidgetCore::SetupSmallestSize() { this->smallest_x = this->min_x; this->smallest_y = this->min_y; /* All other data is already at the right place. */ return this->index; } void NWidgetCore::StoreWidgets(Widget *widgets, int length, bool left_moving, bool top_moving, bool rtl) { if (this->index < 0) return; assert(this->index < length); Widget *w = widgets + this->index; assert(w->type == WWT_LAST); DisplayFlags flags = RESIZE_NONE; // resize flags. /* Compute vertical resizing. */ if (top_moving) { flags |= RESIZE_TB; // Only 1 widget can resize in the widget array. } else if(this->resize_y > 0) { flags |= RESIZE_BOTTOM; } /* Compute horizontal resizing. */ if (left_moving) { flags |= RESIZE_LR; // Only 1 widget can resize in the widget array. } else if (this->resize_x > 0) { flags |= RESIZE_RIGHT; } /* Copy nested widget data into its widget array entry. */ w->type = this->type; w->display_flags = flags; w->colour = this->colour; w->left = this->pos_x; w->right = this->pos_x + this->smallest_x - 1; w->top = this->pos_y; w->bottom = this->pos_y + this->smallest_y - 1; w->data = this->widget_data; w->tooltips = this->tool_tip; } /** * Constructor container baseclass. * @param tp Type of the container. */ NWidgetContainer::NWidgetContainer(WidgetType tp) : NWidgetBase(tp) { this->head = NULL; this->tail = NULL; } NWidgetContainer::~NWidgetContainer() { while (this->head != NULL) { NWidgetBase *wid = this->head->next; delete this->head; this->head = wid; } this->tail = NULL; } /** * Append widget \a wid to container. * @param wid Widget to append. */ void NWidgetContainer::Add(NWidgetBase *wid) { assert(wid->next == NULL && wid->prev == NULL); if (this->head == NULL) { this->head = wid; this->tail = wid; } else { assert(this->tail != NULL); assert(this->tail->next == NULL); this->tail->next = wid; wid->prev = this->tail; this->tail = wid; } } /** * Return the biggest possible size of a nested widget. * @param base Base size of the widget. * @param max_space Available space for the widget. * @param step Stepsize of the widget. * @return Biggest possible size of the widget, assuming that \a base may only be incremented by \a step size steps. */ static inline uint ComputeMaxSize(uint base, uint max_space, uint step) { if (base >= max_space || step == 0) return base; if (step == 1) return max_space; int increment = max_space - base; increment -= increment % step; return base + increment; } /** * Compute the offset of a widget due to not entirely using the available space. * @param space Space used by the widget. * @param max_space Available space for the widget. * @return Offset for centering widget. */ static inline uint ComputeOffset(uint space, uint max_space) { if (space >= max_space) return 0; return (max_space - space) / 2; } /** * Widgets stacked on top of each other. * @param tp Kind of stacking, must be either #NWID_SELECTION or #NWID_LAYERED. */ NWidgetStacked::NWidgetStacked(WidgetType tp) : NWidgetContainer(tp) { } int NWidgetStacked::SetupSmallestSize() { /* First sweep, recurse down and compute minimal size and filling. */ int biggest_index = -1; this->smallest_x = 0; this->smallest_y = 0; this->fill_x = (this->head != NULL); this->fill_y = (this->head != NULL); this->resize_x = (this->head != NULL) ? 1 : 0; this->resize_y = (this->head != NULL) ? 1 : 0; for (NWidgetBase *child_wid = this->head; child_wid != NULL; child_wid = child_wid->next) { int idx = child_wid->SetupSmallestSize(); biggest_index = max(biggest_index, idx); this->smallest_x = max(this->smallest_x, child_wid->smallest_x + child_wid->padding_left + child_wid->padding_right); this->smallest_y = max(this->smallest_y, child_wid->smallest_y + child_wid->padding_top + child_wid->padding_bottom); this->fill_x &= child_wid->fill_x; this->fill_y &= child_wid->fill_y; this->resize_x = LeastCommonMultiple(this->resize_x, child_wid->resize_x); this->resize_y = LeastCommonMultiple(this->resize_y, child_wid->resize_y); } return biggest_index; } void NWidgetStacked::AssignSizePosition(uint x, uint y, uint given_width, uint given_height, bool allow_resize_x, bool allow_resize_y, bool rtl) { assert(given_width >= this->smallest_x && given_height >= this->smallest_y); this->pos_x = x; this->pos_y = y; this->smallest_x = given_width; this->smallest_y = given_height; if (!allow_resize_x) this->resize_x = 0; if (!allow_resize_y) this->resize_y = 0; for (NWidgetBase *child_wid = this->head; child_wid != NULL; child_wid = child_wid->next) { uint hor_step = child_wid->GetHorizontalStepSize(); uint child_width = ComputeMaxSize(child_wid->smallest_x, given_width - child_wid->padding_left - child_wid->padding_right, hor_step); uint child_pos_x = (rtl ? child_wid->padding_right : child_wid->padding_left) + ComputeOffset(child_width, given_width - child_wid->padding_left - child_wid->padding_right); uint vert_step = child_wid->GetVerticalStepSize(); uint child_height = ComputeMaxSize(child_wid->smallest_y, given_height - child_wid->padding_top - child_wid->padding_bottom, vert_step); uint child_pos_y = child_wid->padding_top + ComputeOffset(child_height, given_height - child_wid->padding_top - child_wid->padding_bottom); child_wid->AssignSizePosition(x + child_pos_x, y + child_pos_y, child_width, child_height, (this->resize_x > 0), (this->resize_y > 0), rtl); } } void NWidgetStacked::StoreWidgets(Widget *widgets, int length, bool left_moving, bool top_moving, bool rtl) { for (NWidgetBase *child_wid = this->head; child_wid != NULL; child_wid = child_wid->next) { child_wid->StoreWidgets(widgets, length, left_moving, top_moving, rtl); } } NWidgetPIPContainer::NWidgetPIPContainer(WidgetType tp) : NWidgetContainer(tp) { } /** * Set additional pre/inter/post space for the container. * * @param pip_pre Additional space in front of the first child widget (above * for the vertical container, at the left for the horizontal container). * @param pip_inter Additional space between two child widgets. * @param pip_post Additional space after the last child widget (below for the * vertical container, at the right for the horizontal container). */ void NWidgetPIPContainer::SetPIP(uint8 pip_pre, uint8 pip_inter, uint8 pip_post) { this->pip_pre = pip_pre; this->pip_inter = pip_inter; this->pip_post = pip_post; } /** Horizontal container widget. */ NWidgetHorizontal::NWidgetHorizontal() : NWidgetPIPContainer(NWID_HORIZONTAL) { } int NWidgetHorizontal::SetupSmallestSize() { int biggest_index = -1; this->smallest_x = 0; // Sum of minimal size of all childs. this->smallest_y = 0; // Biggest child. this->fill_x = false; // true if at least one child allows fill_x. this->fill_y = true; // true if all childs allow fill_y. this->resize_x = 0; // smallest non-zero child widget resize step. this->resize_y = 1; // smallest common child resize step if (this->head != NULL) this->head->padding_left += this->pip_pre; for (NWidgetBase *child_wid = this->head; child_wid != NULL; child_wid = child_wid->next) { int idx = child_wid->SetupSmallestSize(); biggest_index = max(biggest_index, idx); if (child_wid->next != NULL) { child_wid->padding_right += this->pip_inter; } else { child_wid->padding_right += this->pip_post; } this->smallest_x += child_wid->smallest_x + child_wid->padding_left + child_wid->padding_right; this->smallest_y = max(this->smallest_y, child_wid->smallest_y + child_wid->padding_top + child_wid->padding_bottom); this->fill_x |= child_wid->fill_x; this->fill_y &= child_wid->fill_y; if (child_wid->resize_x > 0) { if (this->resize_x == 0 || this->resize_x > child_wid->resize_x) this->resize_x = child_wid->resize_x; } this->resize_y = LeastCommonMultiple(this->resize_y, child_wid->resize_y); } /* We need to zero the PIP settings so we can re-initialize the tree. */ this->pip_pre = this->pip_inter = this->pip_post = 0; return biggest_index; } void NWidgetHorizontal::AssignSizePosition(uint x, uint y, uint given_width, uint given_height, bool allow_resize_x, bool allow_resize_y, bool rtl) { assert(given_width >= this->smallest_x && given_height >= this->smallest_y); uint additional_length = given_width - this->smallest_x; // Additional width given to us. this->pos_x = x; this->pos_y = y; this->smallest_x = given_width; this->smallest_y = given_height; if (!allow_resize_x) this->resize_x = 0; if (!allow_resize_y) this->resize_y = 0; /* Count number of childs that would like a piece of the pie. */ int num_changing_childs = 0; // Number of childs that can change size. NWidgetBase *child_wid; for (child_wid = this->head; child_wid != NULL; child_wid = child_wid->next) { uint hor_step = child_wid->GetHorizontalStepSize(); if (hor_step > 0) num_changing_childs++; } /* Fill and position the child widgets. */ uint position = 0; // Place to put next child relative to origin of the container. allow_resize_x = (this->resize_x > 0); child_wid = rtl ? this->tail : this->head; while (child_wid != NULL) { assert(given_height >= child_wid->smallest_y + child_wid->padding_top + child_wid->padding_bottom); uint vert_step = child_wid->GetVerticalStepSize(); uint child_height = ComputeMaxSize(child_wid->smallest_y, given_height - child_wid->padding_top - child_wid->padding_bottom, vert_step); uint child_pos_y = child_wid->padding_top + ComputeOffset(child_height, given_height - child_wid->padding_top - child_wid->padding_bottom); /* Decide about horizontal sizing of the child. */ uint hor_step = child_wid->GetHorizontalStepSize(); uint child_width = child_wid->smallest_x; if (hor_step > 0 && num_changing_childs > 0) { /* Hand out a piece of the pie while compensating for rounding errors. */ uint increment = additional_length / num_changing_childs; if (hor_step > 1) increment -= increment % hor_step; additional_length -= increment; num_changing_childs--; child_width += increment; } child_wid->AssignSizePosition(x + position + (rtl ? child_wid->padding_right : child_wid->padding_left), y + child_pos_y, child_width, child_height, allow_resize_x, (this->resize_y > 0), rtl); position += child_width + child_wid->padding_right + child_wid->padding_left; if (child_wid->resize_x > 0) allow_resize_x = false; // Widget array allows only one child resizing child_wid = rtl ? child_wid->prev : child_wid->next; } } void NWidgetHorizontal::StoreWidgets(Widget *widgets, int length, bool left_moving, bool top_moving, bool rtl) { NWidgetBase *child_wid = rtl ? this->tail : this->head; while (child_wid != NULL) { child_wid->StoreWidgets(widgets, length, left_moving, top_moving, rtl); left_moving |= (child_wid->resize_x > 0); child_wid = rtl ? child_wid->prev : child_wid->next; } } /** Horizontal left-to-right container widget. */ NWidgetHorizontalLTR::NWidgetHorizontalLTR() : NWidgetHorizontal() { this->type = NWID_HORIZONTAL_LTR; } void NWidgetHorizontalLTR::AssignSizePosition(uint x, uint y, uint given_width, uint given_height, bool allow_resize_x, bool allow_resize_y, bool rtl) { NWidgetHorizontal::AssignSizePosition(x, y, given_width, given_height, allow_resize_x, allow_resize_y, false); } void NWidgetHorizontalLTR::StoreWidgets(Widget *widgets, int length, bool left_moving, bool top_moving, bool rtl) { NWidgetHorizontal::StoreWidgets(widgets, length, left_moving, top_moving, false); } /** Vertical container widget. */ NWidgetVertical::NWidgetVertical() : NWidgetPIPContainer(NWID_VERTICAL) { } int NWidgetVertical::SetupSmallestSize() { int biggest_index = -1; this->smallest_x = 0; // Biggest child. this->smallest_y = 0; // Sum of minimal size of all childs. this->fill_x = true; // true if all childs allow fill_x. this->fill_y = false; // true if at least one child allows fill_y. this->resize_x = 1; // smallest common child resize step this->resize_y = 0; // smallest non-zero child widget resize step. if (this->head != NULL) this->head->padding_top += this->pip_pre; for (NWidgetBase *child_wid = this->head; child_wid != NULL; child_wid = child_wid->next) { int idx = child_wid->SetupSmallestSize(); biggest_index = max(biggest_index, idx); if (child_wid->next != NULL) { child_wid->padding_bottom += this->pip_inter; } else { child_wid->padding_bottom += this->pip_post; } this->smallest_y += child_wid->smallest_y + child_wid->padding_top + child_wid->padding_bottom; this->smallest_x = max(this->smallest_x, child_wid->smallest_x + child_wid->padding_left + child_wid->padding_right); this->fill_y |= child_wid->fill_y; this->fill_x &= child_wid->fill_x; if (child_wid->resize_y > 0) { if (this->resize_y == 0 || this->resize_y > child_wid->resize_y) this->resize_y = child_wid->resize_y; } this->resize_x = LeastCommonMultiple(this->resize_x, child_wid->resize_x); } /* We need to zero the PIP settings so we can re-initialize the tree. */ this->pip_pre = this->pip_inter = this->pip_post = 0; return biggest_index; } void NWidgetVertical::AssignSizePosition(uint x, uint y, uint given_width, uint given_height, bool allow_resize_x, bool allow_resize_y, bool rtl) { assert(given_width >= this->smallest_x && given_height >= this->smallest_y); int additional_length = given_height - this->smallest_y; // Additional height given to us. this->pos_x = x; this->pos_y = y; this->smallest_x = given_width; this->smallest_y = given_height; if (!allow_resize_x) this->resize_x = 0; if (!allow_resize_y) this->resize_y = 0; /* count number of childs that would like a piece of the pie. */ int num_changing_childs = 0; // Number of childs that can change size. for (NWidgetBase *child_wid = this->head; child_wid != NULL; child_wid = child_wid->next) { uint vert_step = child_wid->GetVerticalStepSize(); if (vert_step > 0) num_changing_childs++; } /* Fill and position the child widgets. */ uint position = 0; // Place to put next child relative to origin of the container. allow_resize_y = (this->resize_y > 0); for (NWidgetBase *child_wid = this->head; child_wid != NULL; child_wid = child_wid->next) { assert(given_width >= child_wid->smallest_x + child_wid->padding_left + child_wid->padding_right); uint hor_step = child_wid->GetHorizontalStepSize(); uint child_width = ComputeMaxSize(child_wid->smallest_x, given_width - child_wid->padding_left - child_wid->padding_right, hor_step); uint child_pos_x = (rtl ? child_wid->padding_right : child_wid->padding_left) + ComputeOffset(child_width, given_width - child_wid->padding_left - child_wid->padding_right); /* Decide about vertical filling of the child. */ uint vert_step = child_wid->GetVerticalStepSize(); uint child_height = child_wid->smallest_y; if (vert_step > 0 && num_changing_childs > 0) { /* Hand out a piece of the pie while compensating for rounding errors. */ uint increment = additional_length / num_changing_childs; if (vert_step > 1) increment -= increment % vert_step; additional_length -= increment; num_changing_childs--; child_height += increment; } child_wid->AssignSizePosition(x + child_pos_x, y + position + child_wid->padding_top, child_width, child_height, (this->resize_x > 0), allow_resize_y, rtl); position += child_height + child_wid->padding_top + child_wid->padding_bottom; if (child_wid->resize_y > 0) allow_resize_y = false; // Widget array allows only one child resizing } } void NWidgetVertical::StoreWidgets(Widget *widgets, int length, bool left_moving, bool top_moving, bool rtl) { for (NWidgetBase *child_wid = this->head; child_wid != NULL; child_wid = child_wid->next) { child_wid->StoreWidgets(widgets, length, left_moving, top_moving, rtl); top_moving |= (child_wid->resize_y > 0); } } /** * Generic spacer widget. * @param length Horizontal size of the spacer widget. * @param height Vertical size of the spacer widget. */ NWidgetSpacer::NWidgetSpacer(int length, int height) : NWidgetResizeBase(NWID_SPACER, false, false) { this->SetMinimalSize(length, height); this->SetResize(0, 0); } int NWidgetSpacer::SetupSmallestSize() { this->smallest_x = this->min_x; this->smallest_y = this->min_y; return -1; } void NWidgetSpacer::StoreWidgets(Widget *widgets, int length, bool left_moving, bool top_moving, bool rtl) { /* Spacer widgets are never stored in the widget array. */ } /** * Constructor parent nested widgets. * @param tp Type of parent widget. * @param colour Colour of the parent widget. * @param index Index in the widget array used by the window system. * @param child Child container widget (if supplied). If not supplied, a * vertical container will be inserted while adding the first * child widget. */ NWidgetBackground::NWidgetBackground(WidgetType tp, Colours colour, int index, NWidgetPIPContainer *child) : NWidgetCore(tp, colour, true, true, 0x0, STR_NULL) { this->SetIndex(index); assert(tp == WWT_PANEL || tp == WWT_INSET || tp == WWT_FRAME); assert(index >= 0); this->child = child; } NWidgetBackground::~NWidgetBackground() { if (this->child != NULL) delete this->child; } /** * Add a child to the parent. * @param nwid Nested widget to add to the background widget. * * Unless a child container has been given in the constructor, a parent behaves as a vertical container. * You can add several childs to it, and they are put underneath each other. */ void NWidgetBackground::Add(NWidgetBase *nwid) { if (this->child == NULL) { this->child = new NWidgetVertical(); } this->child->Add(nwid); } /** * Set additional pre/inter/post space for the background widget. * * @param pip_pre Additional space in front of the first child widget (above * for the vertical container, at the left for the horizontal container). * @param pip_inter Additional space between two child widgets. * @param pip_post Additional space after the last child widget (below for the * vertical container, at the right for the horizontal container). * @note Using this function implies that the widget has (or will have) child widgets. */ void NWidgetBackground::SetPIP(uint8 pip_pre, uint8 pip_inter, uint8 pip_post) { if (this->child == NULL) { this->child = new NWidgetVertical(); } this->child->SetPIP(pip_pre, pip_inter, pip_post); } int NWidgetBackground::SetupSmallestSize() { int biggest_index = this->index; if (this->child != NULL) { int idx = this->child->SetupSmallestSize(); biggest_index = max(biggest_index, idx); this->smallest_x = this->child->smallest_x; this->smallest_y = this->child->smallest_y; this->fill_x = this->child->fill_x; this->fill_y = this->child->fill_y; this->resize_x = this->child->resize_x; this->resize_y = this->child->resize_y; } else { this->smallest_x = this->min_x; this->smallest_y = this->min_y; } return biggest_index; } void NWidgetBackground::AssignSizePosition(uint x, uint y, uint given_width, uint given_height, bool allow_resize_x, bool allow_resize_y, bool rtl) { this->pos_x = x; this->pos_y = y; this->smallest_x = given_width; this->smallest_y = given_height; if (!allow_resize_x) this->resize_x = 0; if (!allow_resize_y) this->resize_y = 0; if (this->child != NULL) { uint x_offset = (rtl ? this->child->padding_right : this->child->padding_left); uint width = given_width - this->child->padding_right - this->child->padding_left; uint height = given_height - this->child->padding_top - this->child->padding_bottom; this->child->AssignSizePosition(x + x_offset, y + this->child->padding_top, width, height, (this->resize_x > 0), (this->resize_y > 0), rtl); } } void NWidgetBackground::StoreWidgets(Widget *widgets, int length, bool left_moving, bool top_moving, bool rtl) { NWidgetCore::StoreWidgets(widgets, length, left_moving, top_moving, rtl); if (this->child != NULL) this->child->StoreWidgets(widgets, length, left_moving, top_moving, rtl); } /** * Nested leaf widget. * @param tp Type of leaf widget. * @param colour Colour of the leaf widget. * @param index Index in the widget array used by the window system. * @param data Data of the widget. * @param tip Tooltip of the widget. */ NWidgetLeaf::NWidgetLeaf(WidgetType tp, Colours colour, int index, uint16 data, StringID tip) : NWidgetCore(tp, colour, true, true, data, tip) { this->SetIndex(index); this->SetMinimalSize(0, 0); this->SetResize(0, 0); switch (tp) { case WWT_EMPTY: break; case WWT_PUSHBTN: this->SetFill(false, false); break; case WWT_IMGBTN: case WWT_PUSHIMGBTN: case WWT_IMGBTN_2: this->SetFill(false, false); break; case WWT_TEXTBTN: case WWT_PUSHTXTBTN: case WWT_TEXTBTN_2: case WWT_LABEL: case WWT_TEXT: case WWT_MATRIX: case WWT_EDITBOX: this->SetFill(false, false); break; case WWT_SCROLLBAR: case WWT_SCROLL2BAR: this->SetFill(false, true); this->SetResize(0, 1); this->min_x = 12; this->SetDataTip(0x0, STR_TOOLTIP_VSCROLL_BAR_SCROLLS_LIST); break; case WWT_CAPTION: this->SetFill(true, false); this->SetResize(1, 0); this->min_y = 14; this->SetDataTip(data, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS); break; case WWT_HSCROLLBAR: this->SetFill(true, false); this->SetResize(1, 0); this->min_y = 12; this->SetDataTip(0x0, STR_TOOLTIP_HSCROLL_BAR_SCROLLS_LIST); break; case WWT_STICKYBOX: this->SetFill(false, false); this->SetMinimalSize(12, 14); this->SetDataTip(STR_NULL, STR_STICKY_BUTTON); break; case WWT_RESIZEBOX: this->SetFill(false, false); this->SetMinimalSize(12, 12); this->SetDataTip(STR_NULL, STR_RESIZE_BUTTON); break; case WWT_CLOSEBOX: this->SetFill(false, false); this->SetMinimalSize(11, 14); this->SetDataTip(STR_BLACK_CROSS, STR_TOOLTIP_CLOSE_WINDOW); break; case WWT_DROPDOWN: this->SetFill(false, false); this->min_y = 12; break; default: NOT_REACHED(); } } /** * Intialize nested widget tree and convert to widget array. * @param nwid Nested widget tree. * @param rtl Direction of the language. * @return Widget array with the converted widgets. * @note Caller should release returned widget array with \c free(widgets). * @ingroup NestedWidgets */ Widget *InitializeNWidgets(NWidgetBase *nwid, bool rtl) { /* Initialize nested widgets. */ int biggest_index = nwid->SetupSmallestSize(); nwid->AssignSizePosition(0, 0, nwid->smallest_x, nwid->smallest_y, (nwid->resize_x > 0), (nwid->resize_y > 0), rtl); /* Construct a local widget array and initialize all its types to #WWT_LAST. */ Widget *widgets = MallocT(biggest_index + 2); int i; for (i = 0; i < biggest_index + 2; i++) { widgets[i].type = WWT_LAST; } /* Store nested widgets in the array. */ nwid->StoreWidgets(widgets, biggest_index + 1, false, false, rtl); /* Check that all widgets are used. */ for (i = 0; i < biggest_index + 2; i++) { if (widgets[i].type == WWT_LAST) break; } assert(i == biggest_index + 1); /* Fill terminating widget */ static const Widget last_widget = {WIDGETS_END}; widgets[biggest_index + 1] = last_widget; return widgets; } /** * Compare two widget arrays with each other, and report differences. * @param orig Pointer to original widget array. * @param gen Pointer to generated widget array (from the nested widgets). * @param report Report differences to 'misc' debug stream. * @return Both widget arrays are equal. */ bool CompareWidgetArrays(const Widget *orig, const Widget *gen, bool report) { #define CHECK(var, prn) \ if (ow->var != gw->var) { \ same = false; \ if (report) DEBUG(misc, 1, "index %d, \"" #var "\" field: original " prn ", generated " prn, idx, ow->var, gw->var); \ } #define CHECK_COORD(var) \ if (ow->var != gw->var) { \ same = false; \ if (report) DEBUG(misc, 1, "index %d, \"" #var "\" field: original %d, generated %d, (difference %d)", idx, ow->var, gw->var, ow->var - gw->var); \ } bool same = true; for(int idx = 0; ; idx++) { const Widget *ow = orig + idx; const Widget *gw = gen + idx; CHECK(type, "%d") CHECK(display_flags, "0x%x") CHECK(colour, "%d") CHECK_COORD(left) CHECK_COORD(right) CHECK_COORD(top) CHECK_COORD(bottom) CHECK(data, "%u") CHECK(tooltips, "%u") if (ow->type == WWT_LAST || gw->type == WWT_LAST) break; } return same; #undef CHECK #undef CHECK_COORD } /* == Conversion code from NWidgetPart array to NWidgetBase* tree == */ /** * Construct a single nested widget in \a *dest from its parts. * * Construct a NWidgetBase object from a #NWidget function, and apply all * settings that follow it, until encountering a #EndContainer, another * #NWidget, or the end of the parts array. * * @param parts Array with parts of the nested widget. * @param count Length of the \a parts array. * @param dest Address of pointer to use for returning the composed widget. * @param fill_dest Fill the composed widget with child widgets. * @return Number of widget part elements used to compose the widget. */ static int MakeNWidget(const NWidgetPart *parts, int count, NWidgetBase **dest, bool *fill_dest) { int num_used = 0; *dest = NULL; *fill_dest = false; while (count > num_used) { switch (parts->type) { case NWID_SPACER: if (*dest != NULL) return num_used; *dest = new NWidgetSpacer(0, 0); break; case NWID_HORIZONTAL: if (*dest != NULL) return num_used; *dest = new NWidgetHorizontal(); *fill_dest = true; break; case NWID_HORIZONTAL_LTR: if (*dest != NULL) return num_used; *dest = new NWidgetHorizontalLTR(); *fill_dest = true; break; case WWT_PANEL: case WWT_INSET: case WWT_FRAME: if (*dest != NULL) return num_used; *dest = new NWidgetBackground(parts->type, parts->u.widget.colour, parts->u.widget.index); *fill_dest = true; break; case NWID_VERTICAL: if (*dest != NULL) return num_used; *dest = new NWidgetVertical(); *fill_dest = true; break; case WPT_FUNCTION: if (*dest != NULL) return num_used; *dest = parts->u.func_ptr(); *fill_dest = false; break; case NWID_SELECTION: case NWID_LAYERED: if (*dest != NULL) return num_used; *dest = new NWidgetStacked(parts->type); *fill_dest = true; break; case WPT_RESIZE: { NWidgetResizeBase *nwrb = dynamic_cast(*dest); if (nwrb != NULL) { assert(parts->u.xy.x >= 0 && parts->u.xy.y >= 0); nwrb->SetResize(parts->u.xy.x, parts->u.xy.y); } break; } case WPT_RESIZE_PTR: { NWidgetResizeBase *nwrb = dynamic_cast(*dest); if (nwrb != NULL) { assert(parts->u.xy_ptr->x >= 0 && parts->u.xy_ptr->y >= 0); nwrb->SetResize(parts->u.xy_ptr->x, parts->u.xy_ptr->y); } break; } case WPT_MINSIZE: { NWidgetResizeBase *nwrb = dynamic_cast(*dest); if (nwrb != NULL) { assert(parts->u.xy.x >= 0 && parts->u.xy.y >= 0); nwrb->SetMinimalSize(parts->u.xy.x, parts->u.xy.y); } break; } case WPT_MINSIZE_PTR: { NWidgetResizeBase *nwrb = dynamic_cast(*dest); if (nwrb != NULL) { assert(parts->u.xy_ptr->x >= 0 && parts->u.xy_ptr->y >= 0); nwrb->SetMinimalSize((uint)(parts->u.xy_ptr->x), (uint)(parts->u.xy_ptr->y)); } break; } case WPT_FILL: { NWidgetResizeBase *nwrb = dynamic_cast(*dest); if (nwrb != NULL) nwrb->SetFill(parts->u.xy.x != 0, parts->u.xy.y != 0); break; } case WPT_DATATIP: { NWidgetCore *nwc = dynamic_cast(*dest); if (nwc != NULL) { nwc->widget_data = parts->u.data_tip.data; nwc->tool_tip = parts->u.data_tip.tooltip; } break; } case WPT_DATATIP_PTR: { NWidgetCore *nwc = dynamic_cast(*dest); if (nwc != NULL) { nwc->widget_data = parts->u.datatip_ptr->data; nwc->tool_tip = parts->u.datatip_ptr->tooltip; } break; } case WPT_PADDING: if (*dest != NULL) (*dest)->SetPadding(parts->u.padding.top, parts->u.padding.right, parts->u.padding.bottom, parts->u.padding.left); break; case WPT_PIPSPACE: { NWidgetPIPContainer *nwc = dynamic_cast(*dest); if (nwc != NULL) nwc->SetPIP(parts->u.pip.pre, parts->u.pip.inter, parts->u.pip.post); NWidgetBackground *nwb = dynamic_cast(*dest); if (nwb != NULL) nwb->SetPIP(parts->u.pip.pre, parts->u.pip.inter, parts->u.pip.post); break; } case WPT_ENDCONTAINER: return num_used; default: if (*dest != NULL) return num_used; assert((parts->type & WWT_MASK) < NWID_HORIZONTAL); *dest = new NWidgetLeaf(parts->type, parts->u.widget.colour, parts->u.widget.index, 0x0, STR_NULL); break; } num_used++; parts++; } return num_used; } /** * Build a nested widget tree by recursively filling containers with nested widgets read from their parts. * @param parts Array with parts of the nested widgets. * @param count Length of the \a parts array. * @param parent Container to use for storing the child widgets. * @return Number of widget part elements used to fill the container. */ static int MakeWidgetTree(const NWidgetPart *parts, int count, NWidgetBase *parent) { /* Given parent must be either a #NWidgetContainer or a #NWidgetBackground object. */ NWidgetContainer *nwid_cont = dynamic_cast(parent); NWidgetBackground *nwid_parent = dynamic_cast(parent); assert((nwid_cont != NULL && nwid_parent == NULL) || (nwid_cont == NULL && nwid_parent != NULL)); int total_used = 0; while (true) { NWidgetBase *sub_widget = NULL; bool fill_sub = false; int num_used = MakeNWidget(parts, count - total_used, &sub_widget, &fill_sub); parts += num_used; total_used += num_used; /* Break out of loop when end reached */ if (sub_widget == NULL) break; /* Add sub_widget to parent container. */ if (nwid_cont) nwid_cont->Add(sub_widget); if (nwid_parent) nwid_parent->Add(sub_widget); /* If sub-widget is a container, recursively fill that container. */ WidgetType tp = sub_widget->type; if (fill_sub && (tp == NWID_HORIZONTAL || tp == NWID_HORIZONTAL_LTR || tp == NWID_VERTICAL || tp == WWT_PANEL || tp == WWT_FRAME || tp == WWT_INSET || tp == NWID_SELECTION || tp == NWID_LAYERED)) { int num_used = MakeWidgetTree(parts, count - total_used, sub_widget); parts += num_used; total_used += num_used; } } if (count == total_used) return total_used; // Reached the end of the array of parts? assert(total_used < count); assert(parts->type == WPT_ENDCONTAINER); return total_used + 1; // *parts is also 'used' } /** * Construct a nested widget tree from an array of parts. * @param parts Array with parts of the widgets. * @param count Length of the \a parts array. * @return Root of the nested widget tree, a vertical container containing the entire GUI. * @ingroup NestedWidgetParts */ NWidgetContainer *MakeNWidgets(const NWidgetPart *parts, int count) { NWidgetContainer *cont = new NWidgetVertical(); MakeWidgetTree(parts, count, cont); return cont; } /** * Construct a #Widget array from a nested widget parts array, taking care of all the steps and checks. * Also cache the result and use the cache if possible. * @param[in] parts Array with parts of the widgets. * @param parts_length Length of the \a parts array. * @param[in] orig_wid Pointer to original widget array. * @param wid_cache Pointer to the cache for storing the generated widget array (use \c NULL to prevent caching). * @return Cached value if available, otherwise the generated widget array. If \a wid_cache is \c NULL, the caller should free the returned array. * * @pre Before the first call, \c *wid_cache should be \c NULL. * @post The widget array stored in the \c *wid_cache should be free-ed by the caller. */ const Widget *InitializeWidgetArrayFromNestedWidgets(const NWidgetPart *parts, int parts_length, const Widget *orig_wid, Widget **wid_cache) { const bool rtl = false; // Direction of the language is left-to-right if (wid_cache != NULL && *wid_cache != NULL) return *wid_cache; assert(parts != NULL && parts_length > 0); NWidgetContainer *nwid = MakeNWidgets(parts, parts_length); Widget *gen_wid = InitializeNWidgets(nwid, rtl); if (!rtl && orig_wid) { /* There are two descriptions, compare them. * Comparing only makes sense when using a left-to-right language. */ bool ok = CompareWidgetArrays(orig_wid, gen_wid, false); if (ok) { DEBUG(misc, 1, "Nested widgets are equal, min-size(%u, %u)", nwid->smallest_x, nwid->smallest_y); } else { DEBUG(misc, 0, "Nested widgets give different results"); CompareWidgetArrays(orig_wid, gen_wid, true); } } delete nwid; if (wid_cache != NULL) *wid_cache = gen_wid; return gen_wid; }