diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 3063b045ad..94d4104c01 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -24,6 +24,10 @@ - Feature: [#14002] [Plugin] Use allowed_hosts when checking the binding IP for listening. - Feature: [#14059] [Plugin] Add optional filter to custom tools. - Feature: [#14142] [Plugin] Add option for taking transparent screenshots. +- Feature: [#14171] [Plugin] Add API for getting network traffic statistics. +- Feature: [#14171] [Plugin] Add API for creating custom widgets. +- Feature: [#14171] [Plugin] Add API for drawing graphics for custom widgets. +- Feature: [#14171] [Plugin] Add click event to spinners and allow them to be held down. - Change: [#13346] [Plugin] Renamed FootpathScenery to FootpathAddition, fix typos. - Change: [#13857] Change Rotation Control Toggle to track element number 256 - Fix: [#4605, #11912] Water palettes are not updated properly when selected in Object Selection. diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index fd3dad75de..d190ba67bd 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -70,6 +70,14 @@ declare global { y: number; } + /** + * Represents the width and height in pixels. + */ + interface ScreenSize { + width: number; + height: number; + } + /** * A coordinate within the game. * Each in-game tile is a size of 32x32. @@ -1397,6 +1405,7 @@ declare global { readonly players: Player[]; readonly currentPlayer: Player; defaultGroup: number; + readonly stats: NetworkStats; getServerInfo(): ServerInfo; addGroup(): void; @@ -1442,6 +1451,11 @@ declare global { readonly providerWebsite: string; } + interface NetworkStats { + bytesReceived: number[]; + bytesSent: number[]; + } + type PermissionType = "chat" | "terraform" | @@ -1996,14 +2010,15 @@ declare global { * Represents the type of a widget, e.g. button or label. */ type WidgetType = - "button" | "checkbox" | "colourpicker" | "dropdown" | "groupbox" | + "button" | "checkbox" | "colourpicker" | "custom" | "dropdown" | "groupbox" | "label" | "listview" | "spinner" | "textbox" | "viewport"; type Widget = - ButtonWidget | CheckboxWidget | ColourPickerWidget | DropdownWidget | GroupBoxWidget | + ButtonWidget | CheckboxWidget | ColourPickerWidget | CustomWidget | DropdownWidget | GroupBoxWidget | LabelWidget | ListView | SpinnerWidget | TextBoxWidget | ViewportWidget; interface WidgetBase { + readonly window?: Window; x: number; y: number; width: number; @@ -2040,6 +2055,11 @@ declare global { onChange?: (colour: number) => void; } + interface CustomWidget extends WidgetBase { + type: 'custom'; + onDraw?: (this: CustomWidget, g: GraphicsContext) => void; + } + interface DropdownWidget extends WidgetBase { type: 'dropdown'; items?: string[]; @@ -2105,8 +2125,10 @@ declare global { interface SpinnerWidget extends WidgetBase { type: 'spinner'; text?: string; + onDecrement?: () => void; onIncrement?: () => void; + onClick?: () => void; } interface TextBoxWidget extends WidgetBase { @@ -2191,6 +2213,44 @@ declare global { scrollTo(position: CoordsXY | CoordsXYZ): void; } + /** + * API for drawing graphics. + */ + interface GraphicsContext { + colour: number | undefined; + secondaryColour: number | undefined; + ternaryColour: number | undefined; + stroke: number; + fill: number; + paletteId: number | undefined; + readonly width: number; + readonly height: number; + + getImage(id: number): ImageInfo | undefined; + measureText(text: string): ScreenSize; + + clear(): void; + clip(x: number, y: number, width: number, height: number): void; + box(x: number, y: number, width: number, height: number): void; + image(id: number, x: number, y: number): void; + line(x1: number, y1: number, x2: number, y2: number): void; + rect(x: number, y: number, width: number, height: number): void; + text(text: string, x: number, y: number): void; + well(x: number, y: number, width: number, height: number): void; + } + + interface ImageInfo { + readonly id: number; + readonly offset: ScreenCoordsXY; + readonly width: number; + readonly height: number; + readonly isBMP: boolean; + readonly isRLE: boolean; + readonly isPalette: boolean; + readonly noZoom: boolean; + readonly nextZoomId: number | undefined; + } + /** * Listens for incoming connections. * Based on node.js net.Server, see https://nodejs.org/api/net.html for more information. diff --git a/src/openrct2-ui/interface/Widget.cpp b/src/openrct2-ui/interface/Widget.cpp index 5e04864630..1451f53756 100644 --- a/src/openrct2-ui/interface/Widget.cpp +++ b/src/openrct2-ui/interface/Widget.cpp @@ -49,9 +49,6 @@ void WidgetDraw(rct_drawpixelinfo* dpi, rct_window* w, rct_widgetindex widgetInd { switch (w->widgets[widgetIndex].type) { - case WindowWidgetType::Empty: - case WindowWidgetType::Last: - break; case WindowWidgetType::Frame: WidgetFrameDraw(dpi, w, widgetIndex); break; @@ -99,11 +96,11 @@ void WidgetDraw(rct_drawpixelinfo* dpi, rct_window* w, rct_widgetindex widgetInd case WindowWidgetType::Checkbox: WidgetCheckboxDraw(dpi, w, widgetIndex); break; - case WindowWidgetType::Placeholder: - break; case WindowWidgetType::TextBox: WidgetTextBoxDraw(dpi, w, widgetIndex); break; + default: + break; } } @@ -856,6 +853,11 @@ bool WidgetIsDisabled(rct_window* w, rct_widgetindex widgetIndex) return (w->disabled_widgets & (1LL << widgetIndex)) != 0; } +bool WidgetIsHoldable(rct_window* w, rct_widgetindex widgetIndex) +{ + return (w->hold_down_widgets & (1LL << widgetIndex)) != 0; +} + bool WidgetIsVisible(rct_window* w, rct_widgetindex widgetIndex) { return w->widgets[widgetIndex].IsVisible(); @@ -1050,6 +1052,18 @@ void WidgetSetDisabled(rct_window* w, rct_widgetindex widgetIndex, bool value) } } +void WidgetSetHoldable(rct_window* w, rct_widgetindex widgetIndex, bool value) +{ + if (value) + { + w->hold_down_widgets |= (1ULL << widgetIndex); + } + else + { + w->hold_down_widgets &= ~(1ULL << widgetIndex); + } +} + void WidgetSetVisible(rct_window* w, rct_widgetindex widgetIndex, bool value) { if (value) diff --git a/src/openrct2-ui/interface/Window.cpp b/src/openrct2-ui/interface/Window.cpp index c1f9d09b60..8942e803e2 100644 --- a/src/openrct2-ui/interface/Window.cpp +++ b/src/openrct2-ui/interface/Window.cpp @@ -632,8 +632,15 @@ void WindowDrawWidgets(rct_window* w, rct_drawpixelinfo* dpi) { // Check if widget is outside the draw region if (w->windowPos.x + widget->left < dpi->x + dpi->width && w->windowPos.x + widget->right >= dpi->x) + { if (w->windowPos.y + widget->top < dpi->y + dpi->height && w->windowPos.y + widget->bottom >= dpi->y) - WidgetDraw(dpi, w, widgetIndex); + { + if (w->IsLegacy()) + WidgetDraw(dpi, w, widgetIndex); + else + w->OnDrawWidget(widgetIndex, *dpi); + } + } } widgetIndex++; } @@ -681,6 +688,21 @@ void InvalidateAllWindowsAfterInput() }); } +bool Window::IsLegacy() +{ + return false; +} + +void Window::OnDraw(rct_drawpixelinfo& dpi) +{ + WindowDrawWidgets(this, &dpi); +} + +void Window::OnDrawWidget(rct_widgetindex widgetIndex, rct_drawpixelinfo& dpi) +{ + WidgetDraw(&dpi, this, widgetIndex); +} + void Window::InvalidateWidget(rct_widgetindex widgetIndex) { widget_invalidate(this, widgetIndex); diff --git a/src/openrct2-ui/interface/Window.h b/src/openrct2-ui/interface/Window.h index ee1de33ffa..19b0bacc0a 100644 --- a/src/openrct2-ui/interface/Window.h +++ b/src/openrct2-ui/interface/Window.h @@ -14,6 +14,10 @@ struct Window : rct_window { + virtual bool IsLegacy() override; + virtual void OnDraw(rct_drawpixelinfo& dpi) override; + virtual void OnDrawWidget(rct_widgetindex widgetIndex, rct_drawpixelinfo& dpi) override; + void InvalidateWidget(rct_widgetindex widgetIndex); bool IsWidgetDisabled(rct_widgetindex widgetIndex) const; bool IsWidgetPressed(rct_widgetindex widgetIndex) const; diff --git a/src/openrct2-ui/libopenrct2ui.vcxproj b/src/openrct2-ui/libopenrct2ui.vcxproj index d454e99e66..736db9ad95 100644 --- a/src/openrct2-ui/libopenrct2ui.vcxproj +++ b/src/openrct2-ui/libopenrct2ui.vcxproj @@ -54,6 +54,7 @@ + diff --git a/src/openrct2-ui/scripting/CustomWindow.cpp b/src/openrct2-ui/scripting/CustomWindow.cpp index cd0683ae7a..673c90a224 100644 --- a/src/openrct2-ui/scripting/CustomWindow.cpp +++ b/src/openrct2-ui/scripting/CustomWindow.cpp @@ -10,6 +10,8 @@ #ifdef ENABLE_SCRIPTING # include "../interface/Dropdown.h" +# include "../scripting/ScGraphicsContext.hpp" +# include "../scripting/ScWidget.hpp" # include "CustomListView.h" # include "ScUi.hpp" # include "ScWindow.hpp" @@ -18,6 +20,7 @@ # include # include # include +# include # include # include # include @@ -49,39 +52,6 @@ namespace OpenRCT2::Ui::Windows { WindowWidgetType::Resize, 1, 0, 0, 14, 0, 0xFFFFFFFF, STR_NONE }, // content panel }; - static void window_custom_close(rct_window* w); - static void window_custom_mouseup(rct_window* w, rct_widgetindex widgetIndex); - static void window_custom_mousedown(rct_window* w, rct_widgetindex widgetIndex, rct_widget* widget); - static void window_custom_resize(rct_window* w); - static void window_custom_dropdown(rct_window* w, rct_widgetindex widgetIndex, int32_t dropdownIndex); - static void window_custom_textinput(rct_window* w, rct_widgetindex widgetIndex, char* text); - static void window_custom_update(rct_window* w); - static void window_custom_scrollgetsize(rct_window* w, int32_t scrollIndex, int32_t* width, int32_t* height); - static void window_custom_scrollmousedrag(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords); - static void window_custom_scrollmousedown(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords); - static void window_custom_scrollmouseover(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords); - static void window_custom_invalidate(rct_window* w); - static void window_custom_paint(rct_window* w, rct_drawpixelinfo* dpi); - static void window_custom_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi, int32_t scrollIndex); - static void window_custom_update_viewport(rct_window* w); - - static rct_window_event_list window_custom_events([](auto& events) { - events.close = &window_custom_close; - events.mouse_up = &window_custom_mouseup; - events.resize = &window_custom_resize; - events.mouse_down = &window_custom_mousedown; - events.dropdown = &window_custom_dropdown; - events.text_input = &window_custom_textinput; - events.update = &window_custom_update; - events.get_scroll_size = &window_custom_scrollgetsize; - events.scroll_mousedown = &window_custom_scrollmousedown; - events.scroll_mousedrag = &window_custom_scrollmousedrag; - events.scroll_mouseover = &window_custom_scrollmouseover; - events.invalidate = &window_custom_invalidate; - events.paint = &window_custom_paint; - events.scroll_paint = &window_custom_scrollpaint; - }); - struct CustomWidgetDesc { // Properties @@ -115,6 +85,7 @@ namespace OpenRCT2::Ui::Windows // Event handlers DukValue OnClick; DukValue OnChange; + DukValue OnDraw; DukValue OnIncrement; DukValue OnDecrement; DukValue OnHighlight; @@ -163,6 +134,10 @@ namespace OpenRCT2::Ui::Windows } result.OnChange = desc["onChange"]; } + else if (result.Type == "custom") + { + result.OnDraw = desc["onDraw"]; + } else if (result.Type == "dropdown") { if (desc["items"].is_array()) @@ -208,6 +183,7 @@ namespace OpenRCT2::Ui::Windows result.Text = ProcessString(desc["text"]); result.OnIncrement = desc["onIncrement"]; result.OnDecrement = desc["onDecrement"]; + result.OnClick = desc["onClick"]; } else if (result.Type == "textbox") { @@ -402,474 +378,804 @@ namespace OpenRCT2::Ui::Windows } }; - static rct_windownumber _nextWindowNumber; - static CustomWindowInfo& GetInfo(rct_window* w); - static rct_windownumber GetNewWindowNumber(); - static void RefreshWidgets(rct_window* w); static void InvokeEventHandler(const std::shared_ptr& owner, const DukValue& dukHandler); static void InvokeEventHandler( const std::shared_ptr& owner, const DukValue& dukHandler, const std::vector& args); - rct_window* window_custom_open(std::shared_ptr owner, DukValue dukDesc) + class CustomWindow final : public Window { - auto desc = CustomWindowDesc::FromDukValue(dukDesc); + private: + static rct_windownumber _nextWindowNumber; - uint16_t windowFlags = WF_RESIZABLE | WF_TRANSPARENT; - - rct_window* window{}; - if (desc.X && desc.Y) + public: + void Initialise(std::shared_ptr owner, const CustomWindowDesc& desc) { - window = WindowCreate({ *desc.X, *desc.Y }, desc.Width, desc.Height, &window_custom_events, WC_CUSTOM, windowFlags); - } - else - { - window = WindowCreateAutoPos(desc.Width, desc.Height, &window_custom_events, WC_CUSTOM, windowFlags); - } + number = GetNewWindowNumber(); + custom_info = new CustomWindowInfo(owner, desc); + enabled_widgets = (1 << WIDX_CLOSE); - window->number = GetNewWindowNumber(); - window->custom_info = new CustomWindowInfo(owner, desc); - window->enabled_widgets = (1 << WIDX_CLOSE); + // Set window tab + page = desc.TabIndex.value_or(0); - // Set window tab - window->page = desc.TabIndex.value_or(0); - - // Set window colours - window->colours[0] = COLOUR_GREY; - window->colours[1] = COLOUR_GREY; - window->colours[2] = COLOUR_GREY; - auto numColours = std::min(std::size(window->colours), std::size(desc.Colours)); - for (size_t i = 0; i < numColours; i++) - { - window->colours[i] = desc.Colours[i]; - } - - if (desc.IsResizable()) - { - window->min_width = desc.MinWidth.value_or(0); - window->min_height = desc.MinHeight.value_or(0); - window->max_width = desc.MaxWidth.value_or(std::numeric_limits::max()); - window->max_height = desc.MaxHeight.value_or(std::numeric_limits::max()); - } - RefreshWidgets(window); - return window; - } - - static void window_custom_close(rct_window* w) - { - auto info = static_cast(w->custom_info); - if (info != nullptr) - { - InvokeEventHandler(info->Owner, info->Desc.OnClose); - delete info; - w->custom_info = nullptr; - } - } - - static void window_custom_change_tab(rct_window* w, size_t tabIndex) - { - const auto& info = GetInfo(w); - - w->page = static_cast(tabIndex); - w->frame_no = 0; - RefreshWidgets(w); - - w->Invalidate(); - window_event_resize_call(w); - window_event_invalidate_call(w); - WindowInitScrollWidgets(w); - w->Invalidate(); - - InvokeEventHandler(info.Owner, info.Desc.OnTabChange); - } - - static void window_custom_mouseup(rct_window* w, rct_widgetindex widgetIndex) - { - switch (widgetIndex) - { - case WIDX_CLOSE: - window_close(w); - break; - default: + // Set window colours + colours[0] = COLOUR_GREY; + colours[1] = COLOUR_GREY; + colours[2] = COLOUR_GREY; + auto numColours = std::min(std::size(colours), std::size(desc.Colours)); + for (size_t i = 0; i < numColours; i++) { - const auto& info = GetInfo(w); - if (widgetIndex >= WIDX_TAB_0 && widgetIndex < static_cast(WIDX_TAB_0 + info.Desc.Tabs.size())) + colours[i] = desc.Colours[i]; + } + + if (desc.IsResizable()) + { + min_width = desc.MinWidth.value_or(0); + min_height = desc.MinHeight.value_or(0); + max_width = desc.MaxWidth.value_or(std::numeric_limits::max()); + max_height = desc.MaxHeight.value_or(std::numeric_limits::max()); + } + RefreshWidgets(); + } + + void OnClose() override + { + auto info = static_cast(custom_info); + if (info != nullptr) + { + InvokeEventHandler(info->Owner, info->Desc.OnClose); + delete info; + custom_info = nullptr; + } + } + + void OnResize() override + { + if (width < min_width) + { + Invalidate(); + width = min_width; + } + if (height < min_height) + { + Invalidate(); + height = min_height; + } + UpdateViewport(); + } + + void OnUpdate() override + { + const auto& info = GetInfo(this); + if (info.Desc.Tabs.size() > static_cast(page)) + { + const auto& tab = info.Desc.Tabs[page]; + if (tab.imageFrameCount != 0) { - window_custom_change_tab(w, widgetIndex - WIDX_TAB_0); + frame_no++; + if (frame_no >= tab.imageFrameCount * tab.imageFrameDuration) + { + frame_no = 0; + } + widget_invalidate(this, WIDX_TAB_0 + this->page); + } + } + + InvokeEventHandler(info.Owner, info.Desc.OnUpdate); + + // Since the plugin may alter widget positions and sizes during an update event, + // we need to force an update for all list view scrollbars + rct_widgetindex widgetIndex = 0; + for (auto widget = widgets; widget->type != WindowWidgetType::Empty; widget++) + { + if (widget->type == WindowWidgetType::Scroll) + { + WidgetScrollUpdateThumbs(this, widgetIndex); + } + widgetIndex++; + } + } + + void OnPrepareDraw() override + { + widgets[WIDX_BACKGROUND].right = width - 1; + widgets[WIDX_BACKGROUND].bottom = height - 1; + widgets[WIDX_TITLE].right = width - 2; + widgets[WIDX_CLOSE].left = width - 13; + widgets[WIDX_CLOSE].right = width - 3; + widgets[WIDX_CONTENT_PANEL].right = width - 1; + widgets[WIDX_CONTENT_PANEL].bottom = height - 1; + widgets[WIDX_CLOSE].text = (colours[0] & COLOUR_FLAG_TRANSLUCENT) ? STR_CLOSE_X_WHITE : STR_CLOSE_X; + + // Having the content panel visible for transparent windows makes the borders darker than they should be + // For now just hide it if there are no tabs and the window is not resizable + auto& info = GetInfo(this); + auto canResize = (flags & WF_RESIZABLE) != 0 && (min_width != max_width || min_height != max_height); + auto numTabs = info.Desc.Tabs.size(); + if (canResize || numTabs != 0) + { + widgets[WIDX_CONTENT_PANEL].flags &= ~WIDGET_FLAGS::IS_HIDDEN; + } + else + { + widgets[WIDX_CONTENT_PANEL].flags |= WIDGET_FLAGS::IS_HIDDEN; + } + + SetPressedTab(); + + const auto& desc = info.Desc; + auto ft = Formatter::Common(); + ft.Add(desc.Title.c_str()); + + size_t scrollIndex = 0; + for (auto widget = widgets; widget->type != WindowWidgetType::Last; widget++) + { + if (widget->type == WindowWidgetType::Scroll) + { + auto& listView = info.ListViews[scrollIndex]; + auto wwidth = widget->width() + 1 - 2; + auto wheight = widget->height() + 1 - 2; + if (listView.GetScrollbars() == ScrollbarType::Horizontal + || listView.GetScrollbars() == ScrollbarType::Both) + { + wheight -= SCROLLBAR_WIDTH + 1; + } + if (listView.GetScrollbars() == ScrollbarType::Vertical || listView.GetScrollbars() == ScrollbarType::Both) + { + wwidth -= SCROLLBAR_WIDTH + 1; + } + listView.Resize({ wwidth, wheight }); + scrollIndex++; + } + } + } + + void OnDraw(rct_drawpixelinfo& dpi) override + { + WindowDrawWidgets(this, &dpi); + DrawTabImages(dpi); + if (viewport != nullptr) + { + auto widgetIndex = GetViewportWidgetIndex(); + if (WidgetIsVisible(this, widgetIndex.value_or(false))) + { + window_draw_viewport(&dpi, this); + } + } + } + + void OnDrawWidget(rct_widgetindex widgetIndex, rct_drawpixelinfo& dpi) override + { + const auto& widget = widgets[widgetIndex]; + const auto& info = GetInfo(this); + const auto widgetDesc = info.GetCustomWidgetDesc(this, widgetIndex); + if (widgetDesc != nullptr && widgetDesc->Type == "custom") + { + auto& onDraw = widgetDesc->OnDraw; + if (onDraw.is_function()) + { + rct_drawpixelinfo widgetDpi; + if (clip_drawpixelinfo( + &widgetDpi, &dpi, { windowPos.x + widget.left, windowPos.y + widget.top }, widget.width(), + widget.height())) + { + auto ctx = onDraw.context(); + auto dukWidget = ScWidget::ToDukValue(ctx, this, widgetIndex); + auto dukG = GetObjectAsDukValue(ctx, std::make_shared(ctx, widgetDpi)); + auto& scriptEngine = GetContext()->GetScriptEngine(); + scriptEngine.ExecutePluginCall(info.Owner, widgetDesc->OnDraw, dukWidget, { dukG }, false); + } + } + } + else + { + Window::OnDrawWidget(widgetIndex, dpi); + } + } + + void OnMouseUp(rct_widgetindex widgetIndex) override + { + switch (widgetIndex) + { + case WIDX_CLOSE: + window_close(this); + break; + default: + { + const auto& info = GetInfo(this); + if (widgetIndex >= WIDX_TAB_0 + && widgetIndex < static_cast(WIDX_TAB_0 + info.Desc.Tabs.size())) + { + ChangeTab(widgetIndex - WIDX_TAB_0); + break; + } + + const auto widgetDesc = info.GetCustomWidgetDesc(this, widgetIndex); + if (widgetDesc != nullptr) + { + if (widgetDesc->Type == "button") + { + InvokeEventHandler(info.Owner, widgetDesc->OnClick); + } + else if (widgetDesc->Type == "checkbox") + { + auto& widget = widgets[widgetIndex]; + widget.flags ^= WIDGET_FLAGS::IS_PRESSED; + bool isChecked = widget.flags & WIDGET_FLAGS::IS_PRESSED; + + WidgetSetCheckboxValue(this, widgetIndex, isChecked); + + std::vector args; + auto ctx = widgetDesc->OnChange.context(); + duk_push_boolean(ctx, isChecked); + args.push_back(DukValue::take_from_stack(ctx)); + InvokeEventHandler(info.Owner, widgetDesc->OnChange, args); + } + else if (widgetDesc->Type == "spinner") + { + auto& widget = widgets[widgetIndex]; + if (widget.text != STR_NUMERIC_DOWN && widget.text != STR_NUMERIC_UP) + { + InvokeEventHandler(info.Owner, widgetDesc->OnClick); + } + } + } break; } + } + } - const auto widgetDesc = info.GetCustomWidgetDesc(w, widgetIndex); - if (widgetDesc != nullptr) + void OnMouseDown(rct_widgetindex widgetIndex) override + { + auto* widget = &widgets[widgetIndex]; + const auto& info = GetInfo(this); + const auto widgetDesc = info.GetCustomWidgetDesc(this, widgetIndex); + if (widgetDesc != nullptr) + { + if (widgetDesc->Type == "colourpicker") { - if (widgetDesc->Type == "button") + WindowDropdownShowColour(this, widget, colours[widget->colour], widgetDesc->Colour); + } + else if (widgetDesc->Type == "dropdown") + { + widget--; + auto selectedIndex = widgetDesc->SelectedIndex; + const auto& items = widgetDesc->Items; + const auto numItems = std::min(items.size(), Dropdown::ItemsMaxSize); + for (size_t i = 0; i < numItems; i++) { - InvokeEventHandler(info.Owner, widgetDesc->OnClick); + gDropdownItemsFormat[i] = selectedIndex == static_cast(i) ? STR_OPTIONS_DROPDOWN_ITEM_SELECTED + : STR_OPTIONS_DROPDOWN_ITEM; + auto sz = items[i].c_str(); + std::memcpy(&gDropdownItemsArgs[i], &sz, sizeof(const char*)); } - else if (widgetDesc->Type == "checkbox") + WindowDropdownShowTextCustomWidth( + { windowPos.x + widget->left, windowPos.y + widget->top }, widget->height() + 1, + colours[widget->colour], 0, Dropdown::Flag::StayOpen, numItems, widget->width() - 3); + } + else if (widgetDesc->Type == "spinner") + { + if (widget->text == STR_NUMERIC_DOWN) { - auto& widget = w->widgets[widgetIndex]; - widget.flags ^= WIDGET_FLAGS::IS_PRESSED; - bool isChecked = widget.flags & WIDGET_FLAGS::IS_PRESSED; - - WidgetSetCheckboxValue(w, widgetIndex, isChecked); - - std::vector args; - auto ctx = widgetDesc->OnChange.context(); - duk_push_boolean(ctx, isChecked); - args.push_back(DukValue::take_from_stack(ctx)); - InvokeEventHandler(info.Owner, widgetDesc->OnChange, args); + InvokeEventHandler(info.Owner, widgetDesc->OnDecrement); + } + else if (widget->text == STR_NUMERIC_UP) + { + InvokeEventHandler(info.Owner, widgetDesc->OnIncrement); } } - break; - } - } - } - - static void window_custom_resize(rct_window* w) - { - if (w->width < w->min_width) - { - w->Invalidate(); - w->width = w->min_width; - } - if (w->height < w->min_height) - { - w->Invalidate(); - w->height = w->min_height; - } - window_custom_update_viewport(w); - } - - static void window_custom_mousedown(rct_window* w, rct_widgetindex widgetIndex, rct_widget* widget) - { - const auto& info = GetInfo(w); - const auto widgetDesc = info.GetCustomWidgetDesc(w, widgetIndex); - if (widgetDesc != nullptr) - { - if (widgetDesc->Type == "colourpicker") - { - WindowDropdownShowColour(w, widget, w->colours[widget->colour], widgetDesc->Colour); - } - else if (widgetDesc->Type == "dropdown") - { - widget--; - auto selectedIndex = widgetDesc->SelectedIndex; - const auto& items = widgetDesc->Items; - const auto numItems = std::min(items.size(), Dropdown::ItemsMaxSize); - for (size_t i = 0; i < numItems; i++) + else if (widgetDesc->Type == "textbox") { - gDropdownItemsFormat[i] = selectedIndex == static_cast(i) ? STR_OPTIONS_DROPDOWN_ITEM_SELECTED - : STR_OPTIONS_DROPDOWN_ITEM; - auto sz = items[i].c_str(); - std::memcpy(&gDropdownItemsArgs[i], &sz, sizeof(const char*)); - } - WindowDropdownShowTextCustomWidth( - { w->windowPos.x + widget->left, w->windowPos.y + widget->top }, widget->height() + 1, - w->colours[widget->colour], 0, Dropdown::Flag::StayOpen, numItems, widget->width() - 3); - } - else if (widgetDesc->Type == "spinner") - { - if (widget->text == STR_NUMERIC_DOWN) - { - InvokeEventHandler(info.Owner, widgetDesc->OnDecrement); - } - else if (widget->text == STR_NUMERIC_UP) - { - InvokeEventHandler(info.Owner, widgetDesc->OnIncrement); + auto* text = const_cast(widgetDesc->Text.c_str()); + window_start_textbox(this, widgetIndex, STR_STRING, text, widgetDesc->MaxLength + 1); } } - else if (widgetDesc->Type == "textbox") - { - auto* text = const_cast(widgetDesc->Text.c_str()); - window_start_textbox(w, widgetIndex, STR_STRING, text, widgetDesc->MaxLength + 1); - } } - } - static void window_custom_dropdown(rct_window* w, rct_widgetindex widgetIndex, int32_t dropdownIndex) - { - if (dropdownIndex == -1) - return; - - auto& info = GetInfo(w); - auto widgetDesc = info.GetCustomWidgetDesc(w, widgetIndex); - if (widgetDesc != nullptr) + void OnDropdown(rct_widgetindex widgetIndex, int32_t dropdownIndex) override { - if (widgetDesc->Type == "colourpicker") - { - UpdateWidgetColour(w, widgetIndex, dropdownIndex); - } - else if (widgetDesc->Type == "dropdown") - { - UpdateWidgetSelectedIndex(w, widgetIndex - 1, dropdownIndex); - } - } - } + if (dropdownIndex == -1) + return; - static void window_custom_textinput(rct_window* w, rct_widgetindex widgetIndex, char* text) - { - if (text == nullptr) - return; - - auto& info = GetInfo(w); - auto widgetDesc = info.GetCustomWidgetDesc(w, widgetIndex); - if (widgetDesc != nullptr) - { - if (widgetDesc->Type == "textbox") + auto& info = GetInfo(this); + auto widgetDesc = info.GetCustomWidgetDesc(this, widgetIndex); + if (widgetDesc != nullptr) { - UpdateWidgetText(w, widgetIndex, text); - - std::vector args; - auto ctx = widgetDesc->OnChange.context(); - duk_push_string(ctx, text); - args.push_back(DukValue::take_from_stack(ctx)); - InvokeEventHandler(info.Owner, widgetDesc->OnChange, args); - } - } - } - - static void window_custom_update(rct_window* w) - { - const auto& info = GetInfo(w); - if (info.Desc.Tabs.size() > static_cast(w->page)) - { - const auto& tab = info.Desc.Tabs[w->page]; - if (tab.imageFrameCount != 0) - { - w->frame_no++; - if (w->frame_no >= tab.imageFrameCount * tab.imageFrameDuration) + if (widgetDesc->Type == "colourpicker") { - w->frame_no = 0; + UpdateWidgetColour(this, widgetIndex, dropdownIndex); } - widget_invalidate(w, WIDX_TAB_0 + w->page); - } - } - - InvokeEventHandler(info.Owner, info.Desc.OnUpdate); - - // Since the plugin may alter widget positions and sizes during an update event, - // we need to force an update for all list view scrollbars - rct_widgetindex widgetIndex = 0; - for (auto widget = w->widgets; widget->type != WindowWidgetType::Empty; widget++) - { - if (widget->type == WindowWidgetType::Scroll) - { - WidgetScrollUpdateThumbs(w, widgetIndex); - } - widgetIndex++; - } - } - - static void window_custom_scrollgetsize(rct_window* w, int32_t scrollIndex, int32_t* width, int32_t* height) - { - auto& info = GetInfo(w); - if (scrollIndex < static_cast(info.ListViews.size())) - { - auto size = info.ListViews[scrollIndex].GetSize(); - *width = size.width; - *height = size.height; - } - } - - static void window_custom_scrollmousedown(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) - { - auto& info = GetInfo(w); - if (scrollIndex < static_cast(info.ListViews.size())) - { - info.ListViews[scrollIndex].MouseDown(screenCoords); - } - } - - static void window_custom_scrollmousedrag(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) - { - auto& info = GetInfo(w); - if (scrollIndex < static_cast(info.ListViews.size())) - { - info.ListViews[scrollIndex].MouseOver(screenCoords, true); - } - } - - static void window_custom_scrollmouseover(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) - { - auto& info = GetInfo(w); - if (scrollIndex < static_cast(info.ListViews.size())) - { - info.ListViews[scrollIndex].MouseOver(screenCoords, false); - } - } - - static void window_custom_set_pressed_tab(rct_window* w) - { - const auto& info = GetInfo(w); - auto numTabs = info.Desc.Tabs.size(); - if (numTabs != 0) - { - for (size_t i = 0; i < numTabs; i++) - { - w->pressed_widgets &= ~(1 << (WIDX_TAB_0 + i)); - } - w->pressed_widgets |= 1LL << (WIDX_TAB_0 + w->page); - } - } - - static void window_custom_invalidate(rct_window* w) - { - w->widgets[WIDX_BACKGROUND].right = w->width - 1; - w->widgets[WIDX_BACKGROUND].bottom = w->height - 1; - w->widgets[WIDX_TITLE].right = w->width - 2; - w->widgets[WIDX_CLOSE].left = w->width - 13; - w->widgets[WIDX_CLOSE].right = w->width - 3; - w->widgets[WIDX_CONTENT_PANEL].right = w->width - 1; - w->widgets[WIDX_CONTENT_PANEL].bottom = w->height - 1; - w->widgets[WIDX_CLOSE].text = (w->colours[0] & COLOUR_FLAG_TRANSLUCENT) ? STR_CLOSE_X_WHITE : STR_CLOSE_X; - - // Having the content panel visible for transparent windows makes the borders darker than they should be - // For now just hide it if there are no tabs and the window is not resizable - auto& info = GetInfo(w); - auto canResize = (w->flags & WF_RESIZABLE) != 0 && (w->min_width != w->max_width || w->min_height != w->max_height); - auto numTabs = info.Desc.Tabs.size(); - if (canResize || numTabs != 0) - { - w->widgets[WIDX_CONTENT_PANEL].flags &= ~WIDGET_FLAGS::IS_HIDDEN; - } - else - { - w->widgets[WIDX_CONTENT_PANEL].flags |= WIDGET_FLAGS::IS_HIDDEN; - } - - window_custom_set_pressed_tab(w); - - const auto& desc = info.Desc; - auto ft = Formatter::Common(); - ft.Add(desc.Title.c_str()); - - size_t scrollIndex = 0; - for (auto widget = w->widgets; widget->type != WindowWidgetType::Last; widget++) - { - if (widget->type == WindowWidgetType::Scroll) - { - auto& listView = info.ListViews[scrollIndex]; - auto width = widget->width() + 1 - 2; - auto height = widget->height() + 1 - 2; - if (listView.GetScrollbars() == ScrollbarType::Horizontal || listView.GetScrollbars() == ScrollbarType::Both) + else if (widgetDesc->Type == "dropdown") { - height -= SCROLLBAR_WIDTH + 1; + UpdateWidgetSelectedIndex(this, widgetIndex - 1, dropdownIndex); } - if (listView.GetScrollbars() == ScrollbarType::Vertical || listView.GetScrollbars() == ScrollbarType::Both) + } + } + + void OnTextInput(rct_widgetindex widgetIndex, std::string_view text) override + { + auto& info = GetInfo(this); + auto widgetDesc = info.GetCustomWidgetDesc(this, widgetIndex); + if (widgetDesc != nullptr) + { + if (widgetDesc->Type == "textbox") { - width -= SCROLLBAR_WIDTH + 1; + UpdateWidgetText(this, widgetIndex, text); + + std::vector args; + auto ctx = widgetDesc->OnChange.context(); + duk_push_lstring(ctx, text.data(), text.size()); + args.push_back(DukValue::take_from_stack(ctx)); + InvokeEventHandler(info.Owner, widgetDesc->OnChange, args); } - listView.Resize({ width, height }); - scrollIndex++; } } - } - static void window_custom_draw_tab_images(rct_window* w, rct_drawpixelinfo* dpi) - { - const auto& customInfo = GetInfo(w); - const auto& tabs = customInfo.Desc.Tabs; - size_t tabIndex = 0; - for (const auto& tab : tabs) + ScreenSize OnScrollGetSize(int32_t scrollIndex) override { - auto widgetIndex = static_cast(WIDX_TAB_0 + tabIndex); - auto widget = &w->widgets[widgetIndex]; - if (WidgetIsEnabled(w, widgetIndex)) + auto& info = GetInfo(this); + if (scrollIndex < static_cast(info.ListViews.size())) { - auto leftTop = w->windowPos + tab.offset + ScreenCoordsXY{ widget->left, widget->top }; - auto image = tab.imageFrameBase; - if (static_cast(w->page) == tabIndex && tab.imageFrameDuration != 0 && tab.imageFrameCount != 0) + auto size = info.ListViews[scrollIndex].GetSize(); + return { size.width, size.height }; + } + return {}; + } + + void OnScrollMouseDown(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override + { + auto& info = GetInfo(this); + if (scrollIndex < static_cast(info.ListViews.size())) + { + info.ListViews[scrollIndex].MouseDown(screenCoords); + } + } + + void OnScrollMouseDrag(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override + { + auto& info = GetInfo(this); + if (scrollIndex < static_cast(info.ListViews.size())) + { + info.ListViews[scrollIndex].MouseOver(screenCoords, true); + } + } + + void OnScrollMouseOver(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override + { + auto& info = GetInfo(this); + if (scrollIndex < static_cast(info.ListViews.size())) + { + info.ListViews[scrollIndex].MouseOver(screenCoords, false); + } + } + + void OnScrollDraw(int32_t scrollIndex, rct_drawpixelinfo& dpi) override + { + const auto& info = GetInfo(this); + if (scrollIndex < static_cast(info.ListViews.size())) + { + info.ListViews[scrollIndex].Paint(this, &dpi, &scrolls[scrollIndex]); + } + } + + private: + std::optional GetViewportWidgetIndex() + { + rct_widgetindex widgetIndex = 0; + for (auto widget = widgets; widget->type != WindowWidgetType::Last; widget++) + { + if (widget->type == WindowWidgetType::Viewport) { - auto frame = w->frame_no / tab.imageFrameDuration; - auto imageOffset = frame % tab.imageFrameCount; - image = image.WithIndex(image.GetIndex() + imageOffset); + return widgetIndex; } - gfx_draw_sprite(dpi, image.ToUInt32(), leftTop, image.GetTertiary()); + widgetIndex++; } - tabIndex++; + return std::nullopt; } - } - static std::optional GetViewportWidgetIndex(rct_window* w) - { - rct_widgetindex widgetIndex = 0; - for (auto widget = w->widgets; widget->type != WindowWidgetType::Last; widget++) + void UpdateViewport() { - if (widget->type == WindowWidgetType::Viewport) + auto viewportWidgetIndex = GetViewportWidgetIndex(); + if (viewportWidgetIndex) { - return widgetIndex; - } - widgetIndex++; - } - return std::nullopt; - } - - static void window_custom_paint(rct_window* w, rct_drawpixelinfo* dpi) - { - WindowDrawWidgets(w, dpi); - window_custom_draw_tab_images(w, dpi); - if (w->viewport != nullptr) - { - auto widgetIndex = GetViewportWidgetIndex(w); - if (WidgetIsVisible(w, widgetIndex.value_or(false))) - { - window_draw_viewport(dpi, w); - } - } - } - - static void window_custom_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi, int32_t scrollIndex) - { - const auto& info = GetInfo(w); - if (scrollIndex < static_cast(info.ListViews.size())) - { - info.ListViews[scrollIndex].Paint(w, dpi, &w->scrolls[scrollIndex]); - } - } - - static void window_custom_update_viewport(rct_window* w) - { - auto viewportWidgetIndex = GetViewportWidgetIndex(w); - if (viewportWidgetIndex) - { - auto viewportWidget = &w->widgets[*viewportWidgetIndex]; - auto& customInfo = GetInfo(w); - auto widgetInfo = customInfo.GetCustomWidgetDesc(w, *viewportWidgetIndex); - if (widgetInfo != nullptr) - { - auto left = w->windowPos.x + viewportWidget->left + 1; - auto top = w->windowPos.y + viewportWidget->top + 1; - auto width = viewportWidget->width() - 1; - auto height = viewportWidget->height() - 1; - auto viewport = w->viewport; - if (viewport == nullptr) + auto viewportWidget = &widgets[*viewportWidgetIndex]; + auto& customInfo = GetInfo(this); + auto widgetInfo = customInfo.GetCustomWidgetDesc(this, *viewportWidgetIndex); + if (widgetInfo != nullptr) { - auto mapX = 0; - auto mapY = 0; - auto mapZ = 0; - viewport_create( - w, { left, top }, width, height, 0, { mapX, mapY, mapZ }, VIEWPORT_FOCUS_TYPE_COORDINATE, - SPRITE_INDEX_NULL); - w->flags |= WF_NO_SCROLLING; - w->Invalidate(); + auto left = windowPos.x + viewportWidget->left + 1; + auto top = windowPos.y + viewportWidget->top + 1; + auto wwidth = viewportWidget->width() - 1; + auto wheight = viewportWidget->height() - 1; + if (viewport == nullptr) + { + auto mapX = 0; + auto mapY = 0; + auto mapZ = 0; + viewport_create( + this, { left, top }, wwidth, wheight, 0, { mapX, mapY, mapZ }, VIEWPORT_FOCUS_TYPE_COORDINATE, + SPRITE_INDEX_NULL); + flags |= WF_NO_SCROLLING; + Invalidate(); + } + else + { + if (viewport->pos.x != left || viewport->pos.y != top || viewport->width != wwidth + || viewport->height != wheight) + { + viewport->pos.x = left; + viewport->pos.y = top; + viewport->width = wwidth; + viewport->height = wheight; + viewport->view_width = wwidth * viewport->zoom; + viewport->view_height = wheight * viewport->zoom; + Invalidate(); + } + } + } + } + } + + void ChangeTab(size_t tabIndex) + { + const auto& info = GetInfo(this); + + page = static_cast(tabIndex); + frame_no = 0; + RefreshWidgets(); + + Invalidate(); + window_event_resize_call(this); + window_event_invalidate_call(this); + WindowInitScrollWidgets(this); + Invalidate(); + + InvokeEventHandler(info.Owner, info.Desc.OnTabChange); + } + + void SetPressedTab() + { + const auto& info = GetInfo(this); + auto numTabs = info.Desc.Tabs.size(); + if (numTabs != 0) + { + for (size_t i = 0; i < numTabs; i++) + { + pressed_widgets &= ~(1ULL << (WIDX_TAB_0 + i)); + } + pressed_widgets |= 1ULL << (WIDX_TAB_0 + page); + } + } + + void DrawTabImages(rct_drawpixelinfo& dpi) + { + const auto& customInfo = GetInfo(this); + const auto& tabs = customInfo.Desc.Tabs; + size_t tabIndex = 0; + for (const auto& tab : tabs) + { + auto widgetIndex = static_cast(WIDX_TAB_0 + tabIndex); + auto widget = &widgets[widgetIndex]; + if (WidgetIsEnabled(this, widgetIndex)) + { + auto leftTop = windowPos + tab.offset + ScreenCoordsXY{ widget->left, widget->top }; + auto image = tab.imageFrameBase; + if (static_cast(page) == tabIndex && tab.imageFrameDuration != 0 && tab.imageFrameCount != 0) + { + auto frame = frame_no / tab.imageFrameDuration; + auto imageOffset = frame % tab.imageFrameCount; + image = image.WithIndex(image.GetIndex() + imageOffset); + } + gfx_draw_sprite(&dpi, image.ToUInt32(), leftTop, image.GetTertiary()); + } + tabIndex++; + } + } + + void RefreshWidgets() + { + enabled_widgets = 0; + pressed_widgets = 0; + disabled_widgets = 0; + + auto& info = GetInfo(this); + auto& widgetList = info.Widgets; + + widgetList.clear(); + info.WidgetIndexMap.clear(); + info.ListViews.clear(); + + // Add default widgets (window shim) + widgetList.insert(widgetList.begin(), std::begin(CustomDefaultWidgets), std::end(CustomDefaultWidgets)); + for (size_t i = 0; i < widgetList.size(); i++) + { + info.WidgetIndexMap.push_back(std::numeric_limits::max()); + } + enabled_widgets = 1ULL << WIDX_CLOSE; + + // Add window tabs + if (info.Desc.Tabs.size() != 0) + { + widgetList[WIDX_CONTENT_PANEL].top = 43; + } + for (size_t tabDescIndex = 0; tabDescIndex < info.Desc.Tabs.size(); tabDescIndex++) + { + rct_widget widget{}; + widget.type = WindowWidgetType::Tab; + widget.colour = 1; + widget.left = static_cast(3 + (tabDescIndex * 31)); + widget.right = widget.left + 30; + widget.top = 17; + widget.bottom = 43; + widget.image = IMAGE_TYPE_REMAP | SPR_TAB; + widget.tooltip = STR_NONE; + widgetList.push_back(widget); + info.WidgetIndexMap.push_back(std::numeric_limits::max()); + enabled_widgets |= 1ULL << (widgetList.size() - 1); + } + + // Add custom widgets + auto firstCustomWidgetIndex = widgetList.size(); + auto totalWidgets = info.Desc.Widgets.size(); + auto tabWidgetsOffset = totalWidgets; + if (info.Desc.Tabs.size() != 0) + { + totalWidgets += info.Desc.Tabs[page].Widgets.size(); + } + for (size_t widgetDescIndex = 0; widgetDescIndex < totalWidgets; widgetDescIndex++) + { + const auto& widgetDesc = widgetDescIndex < info.Desc.Widgets.size() + ? info.Desc.Widgets[widgetDescIndex] + : info.Desc.Tabs[page].Widgets[widgetDescIndex - tabWidgetsOffset]; + auto preWidgetSize = widgetList.size(); + CreateWidget(widgetList, widgetDesc); + auto numWidetsAdded = widgetList.size() - preWidgetSize; + for (size_t i = 0; i < numWidetsAdded; i++) + { + info.WidgetIndexMap.push_back(widgetDescIndex); + } + + if (widgetDesc.Type == "listview") + { + CustomListView listView(this, info.ListViews.size()); + listView.SetScrollbars(widgetDesc.Scrollbars, true); + listView.SetColumns(widgetDesc.ListViewColumns, true); + listView.SetItems(widgetDesc.ListViewItems, true); + listView.SelectedCell = widgetDesc.SelectedCell; + listView.ShowColumnHeaders = widgetDesc.ShowColumnHeaders; + listView.IsStriped = widgetDesc.IsStriped; + listView.OnClick = widgetDesc.OnClick; + listView.OnHighlight = widgetDesc.OnHighlight; + listView.CanSelect = widgetDesc.CanSelect; + info.ListViews.push_back(std::move(listView)); + } + } + + for (size_t i = firstCustomWidgetIndex; i < widgetList.size(); i++) + { + auto mask = 1ULL << i; + auto widgetFlags = widgetList[i].flags; + if (widgetFlags & WIDGET_FLAGS::IS_ENABLED) + { + enabled_widgets |= mask; + } + if (widgetFlags & WIDGET_FLAGS::IS_PRESSED) + { + pressed_widgets |= mask; + } + if (widgetFlags & WIDGET_FLAGS::IS_DISABLED) + { + disabled_widgets |= mask; + } + if (widgetFlags & WIDGET_FLAGS::IS_HOLDABLE) + { + hold_down_widgets |= mask; + } + } + + widgetList.push_back({ WIDGETS_END }); + widgets = widgetList.data(); + + WindowInitScrollWidgets(this); + UpdateViewport(); + } + + static void CreateWidget(std::vector& widgetList, const CustomWidgetDesc& desc) + { + rct_widget widget{}; + widget.colour = 1; + widget.left = desc.X; + widget.top = desc.Y; + widget.right = desc.X + desc.Width - 1; + widget.bottom = desc.Y + desc.Height - 1; + widget.content = std::numeric_limits::max(); + widget.tooltip = STR_NONE; + if (!desc.Tooltip.empty()) + { + widget.sztooltip = const_cast(desc.Tooltip.c_str()); + widget.flags |= WIDGET_FLAGS::TOOLTIP_IS_STRING; + } + widget.flags |= WIDGET_FLAGS::IS_ENABLED; + if (desc.IsDisabled) + widget.flags |= WIDGET_FLAGS::IS_DISABLED; + if (!desc.IsVisible) + widget.flags |= WIDGET_FLAGS::IS_HIDDEN; + + if (desc.Type == "button") + { + if (desc.Image.HasValue()) + { + widget.type = desc.HasBorder ? WindowWidgetType::ImgBtn : WindowWidgetType::FlatBtn; + widget.image = desc.Image.ToUInt32(); } else { - if (viewport->pos.x != left || viewport->pos.y != top || viewport->width != width - || viewport->height != height) - { - viewport->pos.x = left; - viewport->pos.y = top; - viewport->width = width; - viewport->height = height; - viewport->view_width = width * viewport->zoom; - viewport->view_height = height * viewport->zoom; - w->Invalidate(); - } + widget.type = WindowWidgetType::Button; + widget.string = const_cast(desc.Text.c_str()); + widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; } + if (desc.IsPressed) + { + widget.flags |= WIDGET_FLAGS::IS_PRESSED; + } + widgetList.push_back(widget); + } + else if (desc.Type == "checkbox") + { + widget.type = WindowWidgetType::Checkbox; + widget.string = const_cast(desc.Text.c_str()); + widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; + if (desc.IsChecked) + { + widget.flags |= WIDGET_FLAGS::IS_PRESSED; + } + widgetList.push_back(widget); + } + else if (desc.Type == "colourpicker") + { + widget.type = WindowWidgetType::ColourBtn; + widget.image = GetColourButtonImage(desc.Colour); + widgetList.push_back(widget); + } + else if (desc.Type == "custom") + { + widget.type = WindowWidgetType::Custom; + widgetList.push_back(widget); + } + else if (desc.Type == "dropdown") + { + widget.type = WindowWidgetType::DropdownMenu; + if (desc.SelectedIndex >= 0 && static_cast(desc.SelectedIndex) < desc.Items.size()) + { + widget.string = const_cast(desc.Items[desc.SelectedIndex].c_str()); + } + else + { + widget.string = const_cast(""); + } + widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; + widgetList.push_back(widget); + + // Add the dropdown button + widget = {}; + widget.type = WindowWidgetType::Button; + widget.colour = 1; + widget.left = desc.X + desc.Width - 12; + widget.right = desc.X + desc.Width - 2; + widget.top = desc.Y + 1; + widget.bottom = desc.Y + desc.Height - 2; + widget.text = STR_DROPDOWN_GLYPH; + widget.tooltip = STR_NONE; + widget.flags |= WIDGET_FLAGS::IS_ENABLED; + if (desc.IsDisabled) + widget.flags |= WIDGET_FLAGS::IS_DISABLED; + widgetList.push_back(widget); + } + else if (desc.Type == "groupbox") + { + widget.type = WindowWidgetType::Groupbox; + widget.string = const_cast(desc.Text.c_str()); + widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; + widgetList.push_back(widget); + } + else if (desc.Type == "label") + { + widget.type = WindowWidgetType::Label; + widget.string = const_cast(desc.Text.c_str()); + widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; + if (desc.TextAlign == TextAlignment::CENTRE) + { + widget.type = WindowWidgetType::LabelCentred; + } + widgetList.push_back(widget); + } + else if (desc.Type == "listview") + { + widget.type = WindowWidgetType::Scroll; + widget.content = 0; + if (desc.Scrollbars == ScrollbarType::Horizontal) + widget.content = SCROLL_HORIZONTAL; + else if (desc.Scrollbars == ScrollbarType::Vertical) + widget.content = SCROLL_VERTICAL; + else if (desc.Scrollbars == ScrollbarType::Both) + widget.content = SCROLL_BOTH; + widgetList.push_back(widget); + } + else if (desc.Type == "spinner") + { + widget.type = WindowWidgetType::Spinner; + widget.string = const_cast(desc.Text.c_str()); + widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; + widgetList.push_back(widget); + + // Add the decrement button + widget = {}; + widget.type = WindowWidgetType::Button; + widget.colour = 1; + widget.left = desc.X + desc.Width - 26; + widget.right = widget.left + 12; + widget.top = desc.Y + 1; + widget.bottom = desc.Y + desc.Height - 2; + widget.text = STR_NUMERIC_DOWN; + widget.tooltip = STR_NONE; + widget.flags |= WIDGET_FLAGS::IS_ENABLED; + if (desc.IsDisabled) + widget.flags |= WIDGET_FLAGS::IS_DISABLED; + widget.flags |= WIDGET_FLAGS::IS_HOLDABLE; + widgetList.push_back(widget); + + // Add the increment button + widget.left = desc.X + desc.Width - 13; + widget.right = widget.left + 11; + widget.text = STR_NUMERIC_UP; + widgetList.push_back(widget); + } + else if (desc.Type == "textbox") + { + widget.type = WindowWidgetType::TextBox; + widget.string = const_cast(desc.Text.c_str()); + widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; + widgetList.push_back(widget); + } + else if (desc.Type == "viewport") + { + widget.type = WindowWidgetType::Viewport; + widget.text = STR_NONE; + widgetList.push_back(widget); } } + + static rct_windownumber GetNewWindowNumber() + { + auto result = _nextWindowNumber++; + while (window_find_by_number(WC_CUSTOM, result) != nullptr) + { + result++; + } + return result; + } + }; + + rct_windownumber CustomWindow::_nextWindowNumber; + + rct_window* window_custom_open(std::shared_ptr owner, DukValue dukDesc) + { + auto desc = CustomWindowDesc::FromDukValue(dukDesc); + uint16_t windowFlags = WF_RESIZABLE | WF_TRANSPARENT; + CustomWindow* window{}; + if (desc.X && desc.Y) + { + window = WindowCreate(WC_CUSTOM, { *desc.X, *desc.Y }, desc.Width, desc.Height, windowFlags); + } + else + { + window = WindowCreate(WC_CUSTOM, desc.Width, desc.Height, windowFlags); + } + if (window != nullptr) + { + window->Initialise(owner, desc); + } + return window; } static CustomWindowInfo& GetInfo(rct_window* w) @@ -877,279 +1183,6 @@ namespace OpenRCT2::Ui::Windows return *(static_cast(w->custom_info)); } - static rct_windownumber GetNewWindowNumber() - { - auto result = _nextWindowNumber++; - while (window_find_by_number(WC_CUSTOM, result) != nullptr) - { - result++; - } - return result; - } - - static void CreateWidget(std::vector& widgetList, const CustomWidgetDesc& desc) - { - rct_widget widget{}; - widget.colour = 1; - widget.left = desc.X; - widget.top = desc.Y; - widget.right = desc.X + desc.Width - 1; - widget.bottom = desc.Y + desc.Height - 1; - widget.content = std::numeric_limits::max(); - widget.tooltip = STR_NONE; - if (!desc.Tooltip.empty()) - { - widget.sztooltip = const_cast(desc.Tooltip.c_str()); - widget.flags |= WIDGET_FLAGS::TOOLTIP_IS_STRING; - } - widget.flags |= WIDGET_FLAGS::IS_ENABLED; - if (desc.IsDisabled) - widget.flags |= WIDGET_FLAGS::IS_DISABLED; - if (!desc.IsVisible) - widget.flags |= WIDGET_FLAGS::IS_HIDDEN; - - if (desc.Type == "button") - { - if (desc.Image.HasValue()) - { - widget.type = desc.HasBorder ? WindowWidgetType::ImgBtn : WindowWidgetType::FlatBtn; - widget.image = desc.Image.ToUInt32(); - } - else - { - widget.type = WindowWidgetType::Button; - widget.string = const_cast(desc.Text.c_str()); - widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; - } - if (desc.IsPressed) - { - widget.flags |= WIDGET_FLAGS::IS_PRESSED; - } - widgetList.push_back(widget); - } - else if (desc.Type == "checkbox") - { - widget.type = WindowWidgetType::Checkbox; - widget.string = const_cast(desc.Text.c_str()); - widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; - if (desc.IsChecked) - { - widget.flags |= WIDGET_FLAGS::IS_PRESSED; - } - widgetList.push_back(widget); - } - else if (desc.Type == "colourpicker") - { - widget.type = WindowWidgetType::ColourBtn; - widget.image = GetColourButtonImage(desc.Colour); - widgetList.push_back(widget); - } - else if (desc.Type == "dropdown") - { - widget.type = WindowWidgetType::DropdownMenu; - if (desc.SelectedIndex >= 0 && static_cast(desc.SelectedIndex) < desc.Items.size()) - { - widget.string = const_cast(desc.Items[desc.SelectedIndex].c_str()); - } - else - { - widget.string = const_cast(""); - } - widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; - widgetList.push_back(widget); - - // Add the dropdown button - widget = {}; - widget.type = WindowWidgetType::Button; - widget.colour = 1; - widget.left = desc.X + desc.Width - 12; - widget.right = desc.X + desc.Width - 2; - widget.top = desc.Y + 1; - widget.bottom = desc.Y + desc.Height - 2; - widget.text = STR_DROPDOWN_GLYPH; - widget.tooltip = STR_NONE; - widget.flags |= WIDGET_FLAGS::IS_ENABLED; - if (desc.IsDisabled) - widget.flags |= WIDGET_FLAGS::IS_DISABLED; - widgetList.push_back(widget); - } - else if (desc.Type == "groupbox") - { - widget.type = WindowWidgetType::Groupbox; - widget.string = const_cast(desc.Text.c_str()); - widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; - widgetList.push_back(widget); - } - else if (desc.Type == "label") - { - widget.type = WindowWidgetType::Label; - widget.string = const_cast(desc.Text.c_str()); - widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; - if (desc.TextAlign == TextAlignment::CENTRE) - { - widget.type = WindowWidgetType::LabelCentred; - } - widgetList.push_back(widget); - } - else if (desc.Type == "listview") - { - widget.type = WindowWidgetType::Scroll; - widget.content = 0; - if (desc.Scrollbars == ScrollbarType::Horizontal) - widget.content = SCROLL_HORIZONTAL; - else if (desc.Scrollbars == ScrollbarType::Vertical) - widget.content = SCROLL_VERTICAL; - else if (desc.Scrollbars == ScrollbarType::Both) - widget.content = SCROLL_BOTH; - widgetList.push_back(widget); - } - else if (desc.Type == "spinner") - { - widget.type = WindowWidgetType::Spinner; - widget.string = const_cast(desc.Text.c_str()); - widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; - widgetList.push_back(widget); - - // Add the decrement button - widget = {}; - widget.type = WindowWidgetType::Button; - widget.colour = 1; - widget.left = desc.X + desc.Width - 26; - widget.right = widget.left + 12; - widget.top = desc.Y + 1; - widget.bottom = desc.Y + desc.Height - 2; - widget.text = STR_NUMERIC_DOWN; - widget.tooltip = STR_NONE; - widget.flags |= WIDGET_FLAGS::IS_ENABLED; - if (desc.IsDisabled) - widget.flags |= WIDGET_FLAGS::IS_DISABLED; - widgetList.push_back(widget); - - // Add the increment button - widget.left = desc.X + desc.Width - 13; - widget.right = widget.left + 11; - widget.text = STR_NUMERIC_UP; - widgetList.push_back(widget); - } - else if (desc.Type == "textbox") - { - widget.type = WindowWidgetType::TextBox; - widget.string = const_cast(desc.Text.c_str()); - widget.flags |= WIDGET_FLAGS::TEXT_IS_STRING; - widgetList.push_back(widget); - } - else if (desc.Type == "viewport") - { - widget.type = WindowWidgetType::Viewport; - widget.text = STR_NONE; - widgetList.push_back(widget); - } - } - - static void RefreshWidgets(rct_window* w) - { - w->enabled_widgets = 0; - w->pressed_widgets = 0; - w->disabled_widgets = 0; - - auto& info = GetInfo(w); - auto& widgets = info.Widgets; - - widgets.clear(); - info.WidgetIndexMap.clear(); - info.ListViews.clear(); - - // Add default widgets (window shim) - widgets.insert(widgets.begin(), std::begin(CustomDefaultWidgets), std::end(CustomDefaultWidgets)); - for (size_t i = 0; i < widgets.size(); i++) - { - info.WidgetIndexMap.push_back(std::numeric_limits::max()); - } - w->enabled_widgets = 1ULL << WIDX_CLOSE; - - // Add window tabs - if (info.Desc.Tabs.size() != 0) - { - widgets[WIDX_CONTENT_PANEL].top = 43; - } - for (size_t tabDescIndex = 0; tabDescIndex < info.Desc.Tabs.size(); tabDescIndex++) - { - rct_widget widget{}; - widget.type = WindowWidgetType::Tab; - widget.colour = 1; - widget.left = static_cast(3 + (tabDescIndex * 31)); - widget.right = widget.left + 30; - widget.top = 17; - widget.bottom = 43; - widget.image = IMAGE_TYPE_REMAP | SPR_TAB; - widget.tooltip = STR_NONE; - widgets.push_back(widget); - info.WidgetIndexMap.push_back(std::numeric_limits::max()); - w->enabled_widgets |= 1ULL << (widgets.size() - 1); - } - - // Add custom widgets - auto firstCustomWidgetIndex = widgets.size(); - auto totalWidgets = info.Desc.Widgets.size(); - auto tabWidgetsOffset = totalWidgets; - if (info.Desc.Tabs.size() != 0) - { - totalWidgets += info.Desc.Tabs[w->page].Widgets.size(); - } - for (size_t widgetDescIndex = 0; widgetDescIndex < totalWidgets; widgetDescIndex++) - { - const auto& widgetDesc = widgetDescIndex < info.Desc.Widgets.size() - ? info.Desc.Widgets[widgetDescIndex] - : info.Desc.Tabs[w->page].Widgets[widgetDescIndex - tabWidgetsOffset]; - auto preWidgetSize = widgets.size(); - CreateWidget(widgets, widgetDesc); - auto numWidetsAdded = widgets.size() - preWidgetSize; - for (size_t i = 0; i < numWidetsAdded; i++) - { - info.WidgetIndexMap.push_back(widgetDescIndex); - } - - if (widgetDesc.Type == "listview") - { - CustomListView listView(w, info.ListViews.size()); - listView.SetScrollbars(widgetDesc.Scrollbars, true); - listView.SetColumns(widgetDesc.ListViewColumns, true); - listView.SetItems(widgetDesc.ListViewItems, true); - listView.SelectedCell = widgetDesc.SelectedCell; - listView.ShowColumnHeaders = widgetDesc.ShowColumnHeaders; - listView.IsStriped = widgetDesc.IsStriped; - listView.OnClick = widgetDesc.OnClick; - listView.OnHighlight = widgetDesc.OnHighlight; - listView.CanSelect = widgetDesc.CanSelect; - info.ListViews.push_back(std::move(listView)); - } - } - - for (size_t i = firstCustomWidgetIndex; i < widgets.size(); i++) - { - auto mask = 1ULL << i; - auto flags = widgets[i].flags; - if (flags & WIDGET_FLAGS::IS_ENABLED) - { - w->enabled_widgets |= mask; - } - if (flags & WIDGET_FLAGS::IS_PRESSED) - { - w->pressed_widgets |= mask; - } - if (flags & WIDGET_FLAGS::IS_DISABLED) - { - w->disabled_widgets |= mask; - } - } - - widgets.push_back({ WIDGETS_END }); - w->widgets = widgets.data(); - - WindowInitScrollWidgets(w); - window_custom_update_viewport(w); - } - static void InvokeEventHandler(const std::shared_ptr& owner, const DukValue& dukHandler) { std::vector args; diff --git a/src/openrct2-ui/scripting/ScGraphicsContext.hpp b/src/openrct2-ui/scripting/ScGraphicsContext.hpp new file mode 100644 index 0000000000..a8470b1822 --- /dev/null +++ b/src/openrct2-ui/scripting/ScGraphicsContext.hpp @@ -0,0 +1,267 @@ +/***************************************************************************** + * Copyright (c) 2014-2020 OpenRCT2 developers + * + * For a complete list of all authors, please refer to contributors.md + * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 is licensed under the GNU General Public License version 3. + *****************************************************************************/ + +#pragma once + +#ifdef ENABLE_SCRIPTING + +# include +# include + +namespace OpenRCT2::Scripting +{ + class ScGraphicsContext + { + private: + duk_context* _ctx{}; + rct_drawpixelinfo _dpi{}; + + std::optional _colour{}; + std::optional _secondaryColour{}; + std::optional _ternaryColour{}; + std::optional _paletteId{}; + uint8_t _stroke{}; + uint8_t _fill{}; + + public: + ScGraphicsContext(duk_context* ctx, const rct_drawpixelinfo& dpi) + : _ctx(ctx) + , _dpi(dpi) + { + } + + static void Register(duk_context* ctx) + { + dukglue_register_property(ctx, &ScGraphicsContext::colour_get, &ScGraphicsContext::colour_set, "colour"); + dukglue_register_property( + ctx, &ScGraphicsContext::secondaryColour_get, &ScGraphicsContext::secondaryColour_set, "secondaryColour"); + dukglue_register_property( + ctx, &ScGraphicsContext::ternaryColour_get, &ScGraphicsContext::ternaryColour_set, "ternaryColour"); + dukglue_register_property(ctx, &ScGraphicsContext::paletteId_get, &ScGraphicsContext::paletteId_set, "paletteId"); + dukglue_register_property(ctx, &ScGraphicsContext::fill_get, &ScGraphicsContext::fill_set, "fill"); + dukglue_register_property(ctx, &ScGraphicsContext::stroke_get, &ScGraphicsContext::stroke_set, "stroke"); + dukglue_register_property(ctx, &ScGraphicsContext::width_get, nullptr, "width"); + dukglue_register_property(ctx, &ScGraphicsContext::height_get, nullptr, "height"); + + dukglue_register_method(ctx, &ScGraphicsContext::getImage, "getImage"); + dukglue_register_method(ctx, &ScGraphicsContext::measureText, "measureText"); + + dukglue_register_method(ctx, &ScGraphicsContext::box, "box"); + dukglue_register_method(ctx, &ScGraphicsContext::clear, "clear"); + dukglue_register_method(ctx, &ScGraphicsContext::clip, "clip"); + dukglue_register_method(ctx, &ScGraphicsContext::image, "image"); + dukglue_register_method(ctx, &ScGraphicsContext::line, "line"); + dukglue_register_method(ctx, &ScGraphicsContext::rect, "rect"); + dukglue_register_method(ctx, &ScGraphicsContext::text, "text"); + dukglue_register_method(ctx, &ScGraphicsContext::well, "well"); + } + + private: + DukValue colour_get() const + { + return ToDuk(_ctx, _colour); + } + + void colour_set(DukValue value) + { + if (value.type() == DukValue::NUMBER) + _colour = static_cast(value.as_int()); + else + _colour = {}; + } + + DukValue secondaryColour_get() const + { + return ToDuk(_ctx, _secondaryColour); + } + + void secondaryColour_set(DukValue value) + { + if (value.type() == DukValue::NUMBER) + _secondaryColour = static_cast(value.as_int()); + else + _secondaryColour = {}; + } + + DukValue ternaryColour_get() const + { + return ToDuk(_ctx, _ternaryColour); + } + + void ternaryColour_set(DukValue value) + { + if (value.type() == DukValue::NUMBER) + _ternaryColour = static_cast(value.as_int()); + else + _ternaryColour = {}; + } + + DukValue paletteId_get() const + { + return ToDuk(_ctx, _paletteId); + } + + void paletteId_set(DukValue value) + { + if (value.type() == DukValue::NUMBER) + _paletteId = static_cast(value.as_int()); + else + _paletteId = {}; + } + + uint8_t fill_get() const + { + return _fill; + } + + void fill_set(uint8_t value) + { + _fill = value; + } + + uint8_t stroke_get() const + { + return _stroke; + } + + void stroke_set(uint8_t value) + { + _stroke = value; + } + + int32_t width_get() const + { + return _dpi.width; + } + + int32_t height_get() const + { + return _dpi.height; + } + + DukValue getImage(uint32_t id) + { + auto* g1 = gfx_get_g1_element(id); + if (g1 == nullptr) + { + return ToDuk(_ctx, undefined); + } + else + { + DukObject obj(_ctx); + obj.Set("id", id); + obj.Set("offset", ToDuk(_ctx, { g1->x_offset, g1->y_offset })); + obj.Set("width", g1->width); + obj.Set("height", g1->height); + + obj.Set("isBMP", (g1->flags & G1_FLAG_BMP) != 0); + obj.Set("isRLE", (g1->flags & G1_FLAG_RLE_COMPRESSION) != 0); + obj.Set("isPalette", (g1->flags & G1_FLAG_PALETTE) != 0); + obj.Set("noZoom", (g1->flags & G1_FLAG_NO_ZOOM_DRAW) != 0); + + if (g1->flags & G1_FLAG_HAS_ZOOM_SPRITE) + { + obj.Set("nextZoomId", id - g1->zoomed_offset); + } + else + { + obj.Set("nextZoomId", undefined); + } + return obj.Take(); + } + } + + DukValue measureText(const std::string& text) + { + gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM; + auto width = gfx_get_string_width(text); + auto height = string_get_height_raw(text.c_str()); + return ToDuk(_ctx, { width, height }); + } + + void box(int32_t x, int32_t y, int32_t width, int32_t height) + { + gfx_fill_rect_inset(&_dpi, { x, y, x + width - 1, y + height - 1 }, _colour.value_or(0), 0); + } + + void well(int32_t x, int32_t y, int32_t width, int32_t height) + { + gfx_fill_rect_inset( + &_dpi, { x, y, x + width - 1, y + height - 1 }, _colour.value_or(0), + INSET_RECT_FLAG_BORDER_INSET | INSET_RECT_FLAG_FILL_DONT_LIGHTEN); + } + + void clear() + { + gfx_clear(&_dpi, _fill); + } + + void clip(int32_t x, int32_t y, int32_t width, int32_t height) + { + rct_drawpixelinfo newDpi; + clip_drawpixelinfo(&newDpi, &_dpi, { x, y }, width, height); + _dpi = newDpi; + } + + void image(uint32_t id, int32_t x, int32_t y) + { + ImageId img; + img = img.WithIndex(id); + if (_paletteId) + { + img = img.WithRemap(*_paletteId); + } + else + { + if (_colour) + { + img = img.WithPrimary(*_colour); + } + if (_secondaryColour) + { + img = img.WithSecondary(*_secondaryColour); + } + } + gfx_draw_sprite(&_dpi, static_cast(img.ToUInt32()), { x, y }, _ternaryColour.value_or(0)); + } + + void line(int32_t x1, int32_t y1, int32_t x2, int32_t y2) + { + gfx_draw_line(&_dpi, { { x1, y1 }, { x2, y2 } }, _stroke); + } + + void rect(int32_t x, int32_t y, int32_t width, int32_t height) + { + if (_stroke != 0) + { + line(x, y, x + width, y); + line(x + width - 1, y + 1, x + width - 1, y + height - 1); + line(x, y + height - 1, x + width, y + height - 1); + line(x, y + 1, x, y + height - 1); + + x++; + y++; + width -= 2; + height -= 2; + } + if (_fill != 0) + { + gfx_fill_rect(&_dpi, { x, y, x + width - 1, y + height - 1 }, _fill); + } + } + + void text(const std::string& text, int32_t x, int32_t y) + { + gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM; + gfx_draw_string(&_dpi, text.c_str(), _colour.value_or(0), { x, y }); + } + }; +} // namespace OpenRCT2::Scripting + +#endif diff --git a/src/openrct2-ui/scripting/ScWidget.hpp b/src/openrct2-ui/scripting/ScWidget.hpp index 2973d7c7b2..cc719a8312 100644 --- a/src/openrct2-ui/scripting/ScWidget.hpp +++ b/src/openrct2-ui/scripting/ScWidget.hpp @@ -25,6 +25,7 @@ namespace OpenRCT2::Scripting { + class ScWindow; class ScWidget { protected: @@ -43,6 +44,8 @@ namespace OpenRCT2::Scripting static DukValue ToDukValue(duk_context* ctx, rct_window* w, rct_widgetindex widgetIndex); private: + std::shared_ptr window_get() const; + std::string name_get() const { auto w = GetWindow(); @@ -108,6 +111,8 @@ namespace OpenRCT2::Scripting return "empty"; case WindowWidgetType::Placeholder: return "placeholder"; + case WindowWidgetType::Custom: + return "custom"; case WindowWidgetType::Last: return "last"; } @@ -374,18 +379,7 @@ namespace OpenRCT2::Scripting } public: - static void Register(duk_context* ctx) - { - // Common - dukglue_register_property(ctx, &ScWidget::name_get, &ScWidget::name_set, "name"); - dukglue_register_property(ctx, &ScWidget::type_get, nullptr, "type"); - dukglue_register_property(ctx, &ScWidget::x_get, &ScWidget::x_set, "x"); - dukglue_register_property(ctx, &ScWidget::y_get, &ScWidget::y_set, "y"); - dukglue_register_property(ctx, &ScWidget::width_get, &ScWidget::width_set, "width"); - dukglue_register_property(ctx, &ScWidget::height_get, &ScWidget::height_set, "height"); - dukglue_register_property(ctx, &ScWidget::isDisabled_get, &ScWidget::isDisabled_set, "isDisabled"); - dukglue_register_property(ctx, &ScWidget::isVisible_get, &ScWidget::isVisible_set, "isVisible"); - } + static void Register(duk_context* ctx); protected: rct_window* GetWindow() const diff --git a/src/openrct2-ui/scripting/UiExtensions.cpp b/src/openrct2-ui/scripting/UiExtensions.cpp index 316b9af0b2..6757a7c3a4 100644 --- a/src/openrct2-ui/scripting/UiExtensions.cpp +++ b/src/openrct2-ui/scripting/UiExtensions.cpp @@ -12,6 +12,7 @@ # include "UiExtensions.h" # include "CustomMenu.h" +# include "ScGraphicsContext.hpp" # include "ScTileSelection.hpp" # include "ScTitleSequence.hpp" # include "ScUi.hpp" @@ -29,6 +30,7 @@ void UiScriptExtensions::Extend(ScriptEngine& scriptEngine) dukglue_register_global(ctx, std::make_shared(), "titleSequenceManager"); dukglue_register_global(ctx, std::make_shared(scriptEngine), "ui"); + ScGraphicsContext::Register(ctx); ScTileSelection::Register(ctx); ScTool::Register(ctx); ScUi::Register(ctx); @@ -54,4 +56,22 @@ void UiScriptExtensions::Extend(ScriptEngine& scriptEngine) InitialiseCustomMenuItems(scriptEngine); } +std::shared_ptr ScWidget::window_get() const +{ + return std::make_shared(_class, _number); +} + +void ScWidget::Register(duk_context* ctx) +{ + dukglue_register_property(ctx, &ScWidget::window_get, nullptr, "window"); + dukglue_register_property(ctx, &ScWidget::name_get, &ScWidget::name_set, "name"); + dukglue_register_property(ctx, &ScWidget::type_get, nullptr, "type"); + dukglue_register_property(ctx, &ScWidget::x_get, &ScWidget::x_set, "x"); + dukglue_register_property(ctx, &ScWidget::y_get, &ScWidget::y_set, "y"); + dukglue_register_property(ctx, &ScWidget::width_get, &ScWidget::width_set, "width"); + dukglue_register_property(ctx, &ScWidget::height_get, &ScWidget::height_set, "height"); + dukglue_register_property(ctx, &ScWidget::isDisabled_get, &ScWidget::isDisabled_set, "isDisabled"); + dukglue_register_property(ctx, &ScWidget::isVisible_get, &ScWidget::isVisible_set, "isVisible"); +} + #endif diff --git a/src/openrct2/drawing/Drawing.String.cpp b/src/openrct2/drawing/Drawing.String.cpp index 84f9eea202..a793c19dce 100644 --- a/src/openrct2/drawing/Drawing.String.cpp +++ b/src/openrct2/drawing/Drawing.String.cpp @@ -356,7 +356,7 @@ void draw_string_centred_raw(rct_drawpixelinfo* dpi, const ScreenCoordsXY& coord } } -int32_t string_get_height_raw(char* buffer) +int32_t string_get_height_raw(std::string_view text) { uint16_t fontBase = gCurrentFontSpriteBase; @@ -366,7 +366,7 @@ int32_t string_get_height_raw(char* buffer) else if (fontBase == FONT_SPRITE_BASE_TINY) height += 6; - FmtString fmt(buffer); + FmtString fmt(text); for (const auto& token : fmt) { switch (token.kind) diff --git a/src/openrct2/drawing/Drawing.h b/src/openrct2/drawing/Drawing.h index 8143c7f65d..221eaa5e1a 100644 --- a/src/openrct2/drawing/Drawing.h +++ b/src/openrct2/drawing/Drawing.h @@ -459,6 +459,13 @@ public: return result; } + constexpr ImageId WithRemap(uint8_t paletteId) + { + ImageId result = *this; + result._value = (_value & ~MASK_REMAP) | ((paletteId << SHIFT_REMAP) & MASK_REMAP) | FLAG_PRIMARY; + return result; + } + constexpr ImageId WithPrimary(colour_t colour) { ImageId result = *this; @@ -757,7 +764,7 @@ int32_t gfx_wrap_string(char* buffer, int32_t width, int32_t* num_lines, int32_t int32_t gfx_get_string_width(std::string_view text); int32_t gfx_get_string_width_new_lined(std::string_view text); int32_t gfx_get_string_width_no_formatting(std::string_view text); -int32_t string_get_height_raw(char* buffer); +int32_t string_get_height_raw(std::string_view text); int32_t gfx_clip_string(char* buffer, int32_t width); void shorten_path(utf8* buffer, size_t bufferSize, const utf8* path, int32_t availableWidth); void ttf_draw_string( diff --git a/src/openrct2/interface/Widget.h b/src/openrct2/interface/Widget.h index 565c203242..3f8735f90b 100644 --- a/src/openrct2/interface/Widget.h +++ b/src/openrct2/interface/Widget.h @@ -36,6 +36,7 @@ enum class WindowWidgetType : uint8_t Scroll = 22, Checkbox = 23, Placeholder = 25, + Custom = 28, TextBox = 27, Last = 26, }; @@ -137,6 +138,7 @@ void WidgetDraw(rct_drawpixelinfo* dpi, rct_window* w, rct_widgetindex widgetInd bool WidgetIsEnabled(rct_window* w, rct_widgetindex widgetIndex); bool WidgetIsDisabled(rct_window* w, rct_widgetindex widgetIndex); +bool WidgetIsHoldable(rct_window* w, rct_widgetindex widgetIndex); bool WidgetIsVisible(rct_window* w, rct_widgetindex widgetIndex); bool WidgetIsPressed(rct_window* w, rct_widgetindex widgetIndex); bool WidgetIsHighlighted(rct_window* w, rct_widgetindex widgetIndex); @@ -147,6 +149,7 @@ void WidgetScrollGetPart( void WidgetSetEnabled(rct_window* w, rct_widgetindex widgetIndex, bool enabled); void WidgetSetDisabled(rct_window* w, rct_widgetindex widgetIndex, bool value); +void WidgetSetHoldable(rct_window* w, rct_widgetindex widgetIndex, bool value); void WidgetSetVisible(rct_window* w, rct_widgetindex widgetIndex, bool value); void WidgetSetCheckboxValue(rct_window* w, rct_widgetindex widgetIndex, int32_t value); diff --git a/src/openrct2/interface/Window.cpp b/src/openrct2/interface/Window.cpp index dd9a29a1b0..da2abfa055 100644 --- a/src/openrct2/interface/Window.cpp +++ b/src/openrct2/interface/Window.cpp @@ -1530,9 +1530,10 @@ void window_event_scroll_mousedown_call(rct_window* w, int32_t scrollIndex, cons void window_event_scroll_mousedrag_call(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) { - if (w->event_handlers != nullptr) - if (w->event_handlers->scroll_mousedrag != nullptr) - w->event_handlers->scroll_mousedrag(w, scrollIndex, screenCoords); + if (w->event_handlers == nullptr) + w->OnScrollMouseDrag(scrollIndex, screenCoords); + else if (w->event_handlers->scroll_mousedrag != nullptr) + w->event_handlers->scroll_mousedrag(w, scrollIndex, screenCoords); } void window_event_scroll_mouseover_call(rct_window* w, int32_t scrollIndex, const ScreenCoordsXY& screenCoords) diff --git a/src/openrct2/interface/Window.h b/src/openrct2/interface/Window.h index 0aa1795afa..a83aadc053 100644 --- a/src/openrct2/interface/Window.h +++ b/src/openrct2/interface/Window.h @@ -77,6 +77,7 @@ namespace WIDGET_FLAGS const WidgetFlags IS_DISABLED = 1 << 3; const WidgetFlags TOOLTIP_IS_STRING = 1 << 4; const WidgetFlags IS_HIDDEN = 1 << 5; + const WidgetFlags IS_HOLDABLE = 1 << 6; } // namespace WIDGET_FLAGS enum class WindowWidgetType : uint8_t; diff --git a/src/openrct2/interface/Window_internal.h b/src/openrct2/interface/Window_internal.h index 60a4a01ffe..8272f45d95 100644 --- a/src/openrct2/interface/Window_internal.h +++ b/src/openrct2/interface/Window_internal.h @@ -111,6 +111,11 @@ struct rct_window rct_window() = default; virtual ~rct_window() = default; + virtual bool IsLegacy() + { + return true; + } + // Events virtual void OnOpen() { @@ -133,6 +138,9 @@ struct rct_window virtual void OnDraw(rct_drawpixelinfo& dpi) { } + virtual void OnDrawWidget(rct_widgetindex widgetIndex, rct_drawpixelinfo& dpi) + { + } virtual OpenRCT2String OnTooltip(rct_widgetindex widgetIndex, rct_string_id fallback) { return { fallback, {} }; @@ -153,6 +161,9 @@ struct rct_window { return {}; } + virtual void OnScrollMouseDrag(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) + { + } virtual void OnScrollMouseOver(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) { } diff --git a/src/openrct2/scripting/Duktape.hpp b/src/openrct2/scripting/Duktape.hpp index ced2950f18..73cddc4e52 100644 --- a/src/openrct2/scripting/Duktape.hpp +++ b/src/openrct2/scripting/Duktape.hpp @@ -50,6 +50,11 @@ namespace OpenRCT2::Scripting return value.type() == DukValue::BOOLEAN ? value.as_bool() : defaultValue; } + enum class DukUndefined + { + }; + constexpr DukUndefined undefined{}; + /** * Allows creation of an object on the duktape stack and setting properties on it before * retrieving the DukValue instance of it. @@ -88,6 +93,13 @@ namespace OpenRCT2::Scripting duk_put_prop_string(_ctx, _idx, name); } + void Set(const char* name, DukUndefined) + { + EnsureObjectPushed(); + duk_push_undefined(_ctx); + duk_put_prop_string(_ctx, _idx, name); + } + void Set(const char* name, bool value) { EnsureObjectPushed(); @@ -277,12 +289,24 @@ namespace OpenRCT2::Scripting return DukValue::take_from_stack(ctx); } + template<> inline DukValue ToDuk(duk_context* ctx, const DukUndefined&) + { + duk_push_undefined(ctx); + return DukValue::take_from_stack(ctx); + } + template<> inline DukValue ToDuk(duk_context* ctx, const bool& value) { duk_push_boolean(ctx, value); return DukValue::take_from_stack(ctx); } + template<> inline DukValue ToDuk(duk_context* ctx, const uint8_t& value) + { + duk_push_int(ctx, value); + return DukValue::take_from_stack(ctx); + } + template<> inline DukValue ToDuk(duk_context* ctx, const int32_t& value) { duk_push_int(ctx, value); @@ -401,6 +425,14 @@ namespace OpenRCT2::Scripting return result; } + template<> inline DukValue ToDuk(duk_context* ctx, const ScreenSize& value) + { + DukObject dukCoords(ctx); + dukCoords.Set("width", value.width); + dukCoords.Set("height", value.height); + return dukCoords.Take(); + } + } // namespace OpenRCT2::Scripting #endif diff --git a/src/openrct2/scripting/ScNetwork.hpp b/src/openrct2/scripting/ScNetwork.hpp index 9497eaf600..ae8ddec4b1 100644 --- a/src/openrct2/scripting/ScNetwork.hpp +++ b/src/openrct2/scripting/ScNetwork.hpp @@ -369,6 +369,39 @@ namespace OpenRCT2::Scripting return nullptr; } + DukValue stats_get() const + { +# ifndef DISABLE_NETWORK + auto obj = OpenRCT2::Scripting::DukObject(_context); + auto networkStats = network_get_stats(); + { + duk_push_array(_context); + duk_uarridx_t index = 0; + for (auto v : networkStats.bytesReceived) + { + duk_push_number(_context, v); + duk_put_prop_index(_context, -2, index); + index++; + } + obj.Set("bytesReceived", DukValue::take_from_stack(_context)); + } + { + duk_push_array(_context); + duk_uarridx_t index = 0; + for (auto v : networkStats.bytesSent) + { + duk_push_number(_context, v); + duk_put_prop_index(_context, -2, index); + index++; + } + obj.Set("bytesSent", DukValue::take_from_stack(_context)); + } + return obj.Take(); +# else + return ToDuk(_context, nullptr); +# endif + } + std::shared_ptr getGroup(int32_t index) const { # ifndef DISABLE_NETWORK @@ -490,6 +523,7 @@ namespace OpenRCT2::Scripting dukglue_register_property(ctx, &ScNetwork::players_get, nullptr, "players"); dukglue_register_property(ctx, &ScNetwork::currentPlayer_get, nullptr, "currentPlayer"); dukglue_register_property(ctx, &ScNetwork::defaultGroup_get, &ScNetwork::defaultGroup_set, "defaultGroup"); + dukglue_register_property(ctx, &ScNetwork::stats_get, nullptr, "stats"); dukglue_register_method(ctx, &ScNetwork::addGroup, "addGroup"); dukglue_register_method(ctx, &ScNetwork::getGroup, "getGroup"); dukglue_register_method(ctx, &ScNetwork::removeGroup, "removeGroup"); diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index b7c3911337..1892c0d48b 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -44,7 +44,7 @@ using namespace OpenRCT2; using namespace OpenRCT2::Scripting; -static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 22; +static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 23; struct ExpressionStringifier final { @@ -73,8 +73,14 @@ private: _ss << "\n" << std::string(_indent, ' '); } - void Stringify(const DukValue& val, bool canStartWithNewLine) + void Stringify(const DukValue& val, bool canStartWithNewLine, int32_t nestLevel) { + if (nestLevel >= 8) + { + _ss << "[...]"; + return; + } + switch (val.type()) { case DukValue::Type::UNDEFINED: @@ -99,11 +105,11 @@ private: } else if (val.is_array()) { - StringifyArray(val, canStartWithNewLine); + StringifyArray(val, canStartWithNewLine, nestLevel); } else { - StringifyObject(val, canStartWithNewLine); + StringifyObject(val, canStartWithNewLine, nestLevel); } break; case DukValue::Type::BUFFER: @@ -118,7 +124,7 @@ private: } } - void StringifyArray(const DukValue& val, bool canStartWithNewLine) + void StringifyArray(const DukValue& val, bool canStartWithNewLine, int32_t nestLevel) { constexpr auto maxItemsToShow = 4; @@ -139,7 +145,7 @@ private: { _ss << ", "; } - Stringify(DukValue::take_from_stack(_context), false); + Stringify(DukValue::take_from_stack(_context), false, nestLevel + 1); } } _ss << " ]"; @@ -177,7 +183,7 @@ private: { if (duk_get_prop_index(_context, -1, i)) { - Stringify(DukValue::take_from_stack(_context), false); + Stringify(DukValue::take_from_stack(_context), false, nestLevel + 1); } } } @@ -191,7 +197,7 @@ private: duk_pop(_context); } - void StringifyObject(const DukValue& val, bool canStartWithNewLine) + void StringifyObject(const DukValue& val, bool canStartWithNewLine, int32_t nestLevel) { auto numEnumerables = GetNumEnumerablesOnObject(val); if (numEnumerables == 0) @@ -222,7 +228,7 @@ private: // For some reason the key was not a string _ss << "?: "; } - Stringify(value, true); + Stringify(value, true, nestLevel + 1); index++; } duk_pop_2(_context); @@ -261,7 +267,7 @@ private: // For some reason the key was not a string _ss << "?: "; } - Stringify(value, true); + Stringify(value, true, nestLevel + 1); index++; } duk_pop_2(_context); @@ -343,7 +349,7 @@ public: static std::string StringifyExpression(const DukValue& val) { ExpressionStringifier instance(val.context()); - instance.Stringify(val, false); + instance.Stringify(val, false, 0); return instance._ss.str(); } }; @@ -691,17 +697,27 @@ std::future ScriptEngine::Eval(const std::string& s) DukValue ScriptEngine::ExecutePluginCall( const std::shared_ptr& plugin, const DukValue& func, const std::vector& args, bool isGameStateMutable) +{ + duk_push_undefined(_context); + auto dukUndefined = DukValue::take_from_stack(_context); + return ExecutePluginCall(plugin, func, dukUndefined, args, isGameStateMutable); +} + +DukValue ScriptEngine::ExecutePluginCall( + const std::shared_ptr& plugin, const DukValue& func, const DukValue& thisValue, const std::vector& args, + bool isGameStateMutable) { DukStackFrame frame(_context); if (func.is_function()) { ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, isGameStateMutable); func.push(); + thisValue.push(); for (const auto& arg : args) { arg.push(); } - auto result = duk_pcall(_context, static_cast(args.size())); + auto result = duk_pcall_method(_context, static_cast(args.size())); if (result == DUK_EXEC_SUCCESS) { return DukValue::take_from_stack(_context); diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 9a4c034b90..0c5ba8df20 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -202,6 +202,9 @@ namespace OpenRCT2::Scripting DukValue ExecutePluginCall( const std::shared_ptr& plugin, const DukValue& func, const std::vector& args, bool isGameStateMutable); + DukValue ExecutePluginCall( + const std::shared_ptr& plugin, const DukValue& func, const DukValue& thisValue, + const std::vector& args, bool isGameStateMutable); void LogPluginInfo(const std::shared_ptr& plugin, std::string_view message);