This commit is contained in:
Aria 2025-03-21 22:23:30 +11:00
commit 9c94d113d3
Signed by untrusted user who does not match committer: aria
GPG key ID: 19AB7AA462B8AB3B
10260 changed files with 1237388 additions and 0 deletions

View file

@ -0,0 +1,76 @@
INCLUDE_DIRECTORIES (
${STAR_EXTERN_INCLUDES}
${STAR_CORE_INCLUDES}
${STAR_BASE_INCLUDES}
${STAR_GAME_INCLUDES}
${STAR_PLATFORM_INCLUDES}
${STAR_APPLICATION_INCLUDES}
${STAR_RENDERING_INCLUDES}
${STAR_WINDOWING_INCLUDES}
)
SET (star_windowing_HEADERS
StarButtonGroup.hpp
StarButtonWidget.hpp
StarCanvasWidget.hpp
StarFlowLayout.hpp
StarFuelWidget.hpp
StarGuiContext.hpp
StarGuiReader.hpp
StarGuiTypes.hpp
StarImageWidget.hpp
StarImageStretchWidget.hpp
StarItemGridWidget.hpp
StarItemSlotWidget.hpp
StarKeyBindings.hpp
StarLabelWidget.hpp
StarLargeCharPlateWidget.hpp
StarLayout.hpp
StarListWidget.hpp
StarPane.hpp
StarPaneManager.hpp
StarPortraitWidget.hpp
StarProgressWidget.hpp
StarScrollArea.hpp
StarSliderBar.hpp
StarStackWidget.hpp
StarTabSet.hpp
StarTextBoxWidget.hpp
StarVerticalLayout.hpp
StarWidget.hpp
StarWidgetParsing.hpp
)
SET (star_windowing_SOURCES
StarButtonGroup.cpp
StarButtonWidget.cpp
StarCanvasWidget.cpp
StarFlowLayout.cpp
StarFuelWidget.cpp
StarGuiContext.cpp
StarGuiReader.cpp
StarGuiTypes.cpp
StarImageWidget.cpp
StarImageStretchWidget.cpp
StarItemGridWidget.cpp
StarItemSlotWidget.cpp
StarKeyBindings.cpp
StarLayout.cpp
StarLabelWidget.cpp
StarLargeCharPlateWidget.cpp
StarListWidget.cpp
StarPane.cpp
StarPaneManager.cpp
StarPortraitWidget.cpp
StarProgressWidget.cpp
StarScrollArea.cpp
StarSliderBar.cpp
StarStackWidget.cpp
StarTabSet.cpp
StarTextBoxWidget.cpp
StarVerticalLayout.cpp
StarWidget.cpp
StarWidgetParsing.cpp
)
ADD_LIBRARY (star_windowing OBJECT ${star_windowing_SOURCES} ${star_windowing_HEADERS})

View file

@ -0,0 +1,90 @@
#include "StarButtonGroup.hpp"
#include "StarButtonWidget.hpp"
namespace Star {
void ButtonGroup::setCallback(WidgetCallbackFunc callback) {
m_callback = callback;
}
ButtonWidget* ButtonGroup::button(int id) const {
return m_buttons.value(id);
}
List<ButtonWidget*> ButtonGroup::buttons() const {
return m_buttons.values();
}
size_t ButtonGroup::buttonCount() const {
return m_buttons.size();
}
int ButtonGroup::addButton(ButtonWidget* button, int id) {
if (!button)
return NoButton;
else if (m_buttonIds.contains(button))
return m_buttonIds.get(button);
// If we are auto-generating an id, start at the last id and work forward.
if (id == NoButton && !m_buttons.empty())
id = (prev(m_buttons.end()))->first;
while (m_buttons.contains(id))
++id;
m_buttons[id] = button;
m_buttonIds[button] = id;
return id;
}
void ButtonGroup::removeButton(ButtonWidget* button) {
if (m_buttonIds.contains(button)) {
m_buttons.remove(m_buttonIds.get(button));
m_buttonIds.remove(button);
}
}
int ButtonGroup::id(ButtonWidget* button) const {
if (m_buttonIds.contains(button))
return m_buttonIds.get(button);
else
return NoButton;
}
ButtonWidget* ButtonGroup::checkedButton() const {
for (auto const& pair : m_buttons) {
if (pair.second->isChecked())
return pair.second;
}
return {};
}
int ButtonGroup::checkedId() const {
return id(checkedButton());
}
void ButtonGroup::select(int id) {
auto b = button(id);
if (!b->isChecked())
b->check();
}
void ButtonGroup::wasChecked(ButtonWidget* self) {
for (auto const& pair : m_buttons) {
if (pair.second != self)
pair.second->setChecked(false);
}
if (m_callback)
m_callback(self);
}
bool ButtonGroup::toggle() const {
return m_toggle;
}
void ButtonGroup::setToggle(bool toggleMode) {
m_toggle = toggleMode;
}
}

View file

@ -0,0 +1,59 @@
#ifndef STAR_BUTTON_GROUP_HPP
#define STAR_BUTTON_GROUP_HPP
#include "StarWidget.hpp"
namespace Star {
STAR_CLASS(ButtonWidget);
STAR_CLASS(ButtonGroup);
STAR_CLASS(ButtonGroupWidget);
// Manages group of buttons in which *at most* a single button can be checked
// at any time.
class ButtonGroup {
public:
friend class ButtonWidget;
static int const NoButton = -1;
// Callback is called when any child buttons checked state is changed, and
// its parameter is the button being checked.
void setCallback(WidgetCallbackFunc callback);
ButtonWidget* button(int id) const;
List<ButtonWidget*> buttons() const;
size_t buttonCount() const;
int addButton(ButtonWidget* button, int id = NoButton);
void removeButton(ButtonWidget* button);
int id(ButtonWidget* button) const;
void select(int id);
// Will return null if no button is checked.
ButtonWidget* checkedButton() const;
// Will return NoButton if no button is checked.
int checkedId() const;
// when true it is not required for one of the buttons to be selected
bool toggle() const;
void setToggle(bool toggleMode);
protected:
// Should be called by child button widgets when they are changed from
// unchecked to checked.
void wasChecked(ButtonWidget* self);
private:
WidgetCallbackFunc m_callback;
Map<int, ButtonWidget*> m_buttons;
Map<ButtonWidget*, int> m_buttonIds;
bool m_toggle;
};
class ButtonGroupWidget : public ButtonGroup, public Widget {};
}
#endif

View file

@ -0,0 +1,380 @@
#include "StarButtonWidget.hpp"
#include "StarRoot.hpp"
#include "StarJsonExtra.hpp"
#include "StarRandom.hpp"
#include "StarAssets.hpp"
namespace Star {
ButtonWidget::ButtonWidget() {
m_hovered = false;
m_pressed = false;
m_checkable = false;
m_checked = false;
m_disabled = false;
m_highlighted = false;
m_hasCheckedImages = false;
m_sustain = false;
m_invisible = false;
m_hTextAnchor = HorizontalAnchor::HMidAnchor;
m_fontColor = Color::White;
m_fontColorDisabled = Color::Gray;
auto assets = Root::singleton().assets();
auto interfaceConfig = assets->json("/interface.config");
m_pressedOffset = jsonToVec2I(interfaceConfig.get("buttonPressedOffset"));
m_fontSize = interfaceConfig.query("font.buttonSize").toInt();
}
ButtonWidget::ButtonWidget(WidgetCallbackFunc callback,
String const& baseImage,
String const& hoverImage,
String const& pressedImage,
String const& disabledImage)
: ButtonWidget() {
setCallback(callback);
setImages(baseImage, hoverImage, pressedImage, disabledImage);
}
ButtonWidget::~ButtonWidget() {
if (m_buttonGroup)
m_buttonGroup->removeButton(this);
}
void ButtonWidget::renderImpl() {
if (isPressed() && sustainCallbackOnDownHold())
if (m_callback)
m_callback(this);
Vec2F position = Vec2F(screenPosition());
Vec2F textPosition = position + Vec2F(m_textOffset);
if (m_hTextAnchor == HorizontalAnchor::HMidAnchor)
textPosition += Vec2F(size()) / 2;
else if (m_hTextAnchor == HorizontalAnchor::RightAnchor)
textPosition += Vec2F(size()[0], size()[1] / 2);
else if (m_hTextAnchor == HorizontalAnchor::LeftAnchor)
textPosition += Vec2F(0, size()[1] / 2);
// We need to show the down button offset if we're pressing the button or
// don't have checked images and thus need some way to show that the button
// is checked (there's probably some better default behavior in that case)
if (m_pressed || (m_checked && !m_hasCheckedImages)) {
position += Vec2F(m_pressedOffset);
textPosition += Vec2F(m_pressedOffset);
}
if (m_hasCheckedImages && m_checked) {
if (m_disabled)
drawButtonPart(m_disabledImageChecked, position);
else if ((m_pressed || m_highlighted) && !m_pressedImageChecked.empty())
drawButtonPart(m_pressedImageChecked, position);
else if ((m_pressed || m_hovered || m_highlighted) && !m_hoverImageChecked.empty())
drawButtonPart(m_hoverImageChecked, position);
else
drawButtonPart(m_baseImageChecked, position);
} else {
if (m_disabled)
drawButtonPart(m_disabledImage, position);
else if ((m_pressed || m_highlighted) && !m_pressedImage.empty())
drawButtonPart(m_pressedImage, position);
else if ((m_pressed || m_hovered || m_highlighted) && !m_hoverImage.empty())
drawButtonPart(m_hoverImage, position);
else if (!m_invisible)
drawButtonPart(m_baseImage, position);
}
if (!m_overlayImage.empty())
drawButtonPart(m_overlayImage, position);
if (!m_text.empty()) {
auto& guiContext = GuiContext::singleton();
guiContext.setFontSize(m_fontSize);
if (m_disabled)
guiContext.setFontColor(m_fontColorDisabled.toRgba());
else if (m_fontColorChecked && m_checked)
guiContext.setFontColor(m_fontColorChecked.value().toRgba());
else
guiContext.setFontColor(m_fontColor.toRgba());
guiContext.setFontMode(FontMode::Shadow);
guiContext.renderInterfaceText(m_text, {textPosition, m_hTextAnchor, VerticalAnchor::VMidAnchor});
guiContext.setFontMode(FontMode::Normal);
}
}
bool ButtonWidget::sendEvent(InputEvent const& event) {
if (m_visible && !m_disabled) {
if (event.is<MouseButtonDownEvent>() && event.get<MouseButtonDownEvent>().mouseButton == MouseButton::Left) {
if (inMember(*context()->mousePosition(event))) {
if (!isPressed()) {
auto assets = Root::singleton().assets();
auto sound = Random::randValueFrom(assets->json("/interface.config:buttonClickSound").toArray(), "").toString();
if (!sound.empty())
context()->playAudio(sound);
}
setPressed(true);
if (m_callback) {
focus();
return true;
}
} else {
blur();
return false;
}
} else if (event.is<MouseButtonUpEvent>()) {
setPressed(false);
return false;
}
}
return false;
}
void ButtonWidget::mouseOver() {
Widget::mouseOver();
if (!m_disabled) {
if (!m_hovered) {
auto assets = Root::singleton().assets();
auto sound = Random::randValueFrom(assets->json("/interface.config:buttonHoverSound").toArray(), "").toString();
if (!sound.empty())
context()->playAudio(sound);
}
m_hovered = true;
}
}
void ButtonWidget::mouseOut() {
Widget::mouseOut();
m_hovered = false;
m_pressed = false;
}
void ButtonWidget::mouseReturnStillDown() {
Widget::mouseReturnStillDown();
m_hovered = true;
m_pressed = true;
}
void ButtonWidget::hide() {
Widget::hide();
m_pressed = false;
m_hovered = false;
}
void ButtonWidget::setCallback(WidgetCallbackFunc callback) {
m_callback = callback;
}
ButtonGroupPtr ButtonWidget::buttonGroup() const {
return m_buttonGroup;
}
void ButtonWidget::setButtonGroup(ButtonGroupPtr newGroup, int id) {
if (m_buttonGroup != newGroup) {
if (m_buttonGroup)
m_buttonGroup->removeButton(this);
m_buttonGroup = move(newGroup);
if (m_buttonGroup) {
setCheckable(true);
m_buttonGroup->addButton(this, id);
}
}
}
int ButtonWidget::buttonGroupId() {
if (m_buttonGroup)
return m_buttonGroup->id(this);
else
return ButtonGroup::NoButton;
}
bool ButtonWidget::isHovered() const {
return m_hovered;
}
bool ButtonWidget::isPressed() const {
return m_pressed;
}
void ButtonWidget::setPressed(bool pressed) {
if (m_pressed != pressed) {
// Button action is triggered when the button is released after being pressed
if (m_pressed) {
check();
if (m_callback) {
m_callback(this);
}
}
m_pressed = pressed;
}
}
bool ButtonWidget::isCheckable() const {
return m_checkable;
}
void ButtonWidget::setCheckable(bool checkable) {
m_checkable = checkable;
}
bool ButtonWidget::isHighlighted() const {
return m_highlighted;
}
void ButtonWidget::setHighlighted(bool highlighted) {
m_highlighted = highlighted;
}
bool ButtonWidget::isChecked() const {
return m_checked;
}
void ButtonWidget::setChecked(bool checked) {
// might cause button groups to have multiple selected against its rules, be careful with direct poking, use check()
// instead.
m_checked = checked;
}
void ButtonWidget::check() {
if (m_checkable) {
// If we are part of an exclusive button group, then don't uncheck if
// we are already checked and pressed again.
if (m_buttonGroup) {
if (m_buttonGroup->toggle() || !isChecked()) {
setChecked(!m_buttonGroup->toggle() || !isChecked());
m_buttonGroup->wasChecked(this);
}
} else {
setChecked(!isChecked());
}
}
}
bool ButtonWidget::sustainCallbackOnDownHold() {
return m_sustain;
}
void ButtonWidget::setSustainCallbackOnDownHold(bool sustain) {
m_sustain = sustain;
}
void ButtonWidget::setImages(String const& baseImage, String const& hoverImage, String const& pressedImage, String const& disabledImage) {
m_baseImage = baseImage;
m_hoverImage = hoverImage;
m_pressedImage = pressedImage;
m_disabledImage = disabledImage;
if (m_disabledImage.empty() && !m_baseImage.empty())
m_disabledImage = m_baseImage + Root::singleton().assets()->json("/interface.config:disabledButton").toString();
updateSize();
}
void ButtonWidget::setCheckedImages(String const& baseImage, String const& hoverImage, String const& pressedImage, String const& disabledImage) {
m_hasCheckedImages = !baseImage.empty();
m_baseImageChecked = baseImage;
m_hoverImageChecked = hoverImage;
m_pressedImageChecked = pressedImage;
m_disabledImageChecked = disabledImage;
if (m_hasCheckedImages && m_disabledImageChecked.empty())
m_disabledImageChecked = m_baseImageChecked + Root::singleton().assets()->json("/interface.config:disabledButton").toString();
updateSize();
}
void ButtonWidget::setOverlayImage(String const& overlayImage) {
m_overlayImage = overlayImage;
}
Vec2I const& ButtonWidget::pressedOffset() const {
return m_pressedOffset;
}
void ButtonWidget::setPressedOffset(Vec2I const& offset) {
m_pressedOffset = offset;
}
void ButtonWidget::setText(String const& text) {
m_text = text;
}
void ButtonWidget::setFontSize(int size) {
m_fontSize = size;
}
void ButtonWidget::setTextOffset(Vec2I textOffset) {
m_textOffset = textOffset;
}
void ButtonWidget::setTextAlign(HorizontalAnchor hAnchor) {
m_hTextAnchor = hAnchor;
}
void ButtonWidget::setFontColor(Color color) {
m_fontColor = color;
}
void ButtonWidget::setFontColorDisabled(Color color) {
m_fontColorDisabled = color;
}
void ButtonWidget::setFontColorChecked(Color color) {
m_fontColorChecked = color;
}
// Although ButtonWidget wraps other widgets from time to time. These should never be "accessible"
WidgetPtr ButtonWidget::getChildAt(Vec2I const&) {
return {};
}
void ButtonWidget::disable() {
m_disabled = true;
m_pressed = false;
}
void ButtonWidget::enable() {
m_disabled = false;
}
void ButtonWidget::setEnabled(bool enabled) {
m_disabled = !enabled;
m_pressed &= enabled; // and off the pressed flag if the button is no longer enabled.
}
void ButtonWidget::setInvisible(bool invisible) {
m_invisible = invisible;
}
RectI ButtonWidget::getScissorRect() const {
if (m_pressed) {
return RectI::withSize(screenPosition(), size() + m_pressedOffset);
}
return RectI::withSize(screenPosition(), size());
}
void ButtonWidget::drawButtonPart(String const& image, Vec2F const& position) {
auto& guiContext = GuiContext::singleton();
auto imageSize = guiContext.textureSize(image);
guiContext.drawInterfaceQuad(image, position + Vec2F(m_buttonBoundSize - imageSize) / 2);
}
void ButtonWidget::updateSize() {
if (m_invisible || m_baseImage.empty())
return;
auto& guiContext = GuiContext::singleton();
m_buttonBoundSize = guiContext.textureSize(m_baseImage);
if (!m_hoverImage.empty())
m_buttonBoundSize = m_buttonBoundSize.piecewiseMax(guiContext.textureSize(m_hoverImage));
if (!m_pressedImage.empty())
m_buttonBoundSize = m_buttonBoundSize.piecewiseMax(guiContext.textureSize(m_pressedImage));
if (!m_baseImageChecked.empty())
m_buttonBoundSize = m_buttonBoundSize.piecewiseMax(guiContext.textureSize(m_baseImageChecked));
if (!m_hoverImageChecked.empty())
m_buttonBoundSize = m_buttonBoundSize.piecewiseMax(guiContext.textureSize(m_hoverImageChecked));
if (!m_pressedImageChecked.empty())
m_buttonBoundSize = m_buttonBoundSize.piecewiseMax(guiContext.textureSize(m_pressedImageChecked));
if (!m_disabledImageChecked.empty())
m_buttonBoundSize = m_buttonBoundSize.piecewiseMax(guiContext.textureSize(m_disabledImageChecked));
setSize(Vec2I(m_buttonBoundSize));
}
}

View file

@ -0,0 +1,138 @@
#ifndef STAR_BUTTON_WIDGET_HPP
#define STAR_BUTTON_WIDGET_HPP
#include "StarButtonGroup.hpp"
namespace Star {
STAR_CLASS(ButtonWidget);
class ButtonWidget : public Widget {
public:
ButtonWidget();
ButtonWidget(WidgetCallbackFunc callback,
String const& baseImage,
String const& hoverImage = "",
String const& pressedImage = "",
String const& disabledImage = "");
virtual ~ButtonWidget();
virtual bool sendEvent(InputEvent const& event) override;
virtual void mouseOver() override;
virtual void mouseOut() override;
virtual void mouseReturnStillDown() override;
virtual void hide() override;
// Callback is called when the checked / pressed state is changed.
void setCallback(WidgetCallbackFunc callback);
ButtonGroupPtr buttonGroup() const;
// Sets the button group for this widget, and adds it to the button group if
// it is not already added. Additionally, sets the button as checkable.
void setButtonGroup(ButtonGroupPtr buttonGroup, int id = ButtonGroup::NoButton);
// If a button group is set, returns this button's id in the button group.
int buttonGroupId();
bool isHovered() const;
bool isPressed() const;
void setPressed(bool pressed);
bool isCheckable() const;
void setCheckable(bool checkable);
bool isHighlighted() const;
void setHighlighted(bool highlighted);
bool isChecked() const;
void setChecked(bool checked);
// Either checks a button, or toggles the state, depending on whether the
// button is part of an exclusive group or not.
void check();
bool sustainCallbackOnDownHold();
void setSustainCallbackOnDownHold(bool sustain);
void setImages(String const& baseImage,
String const& hoverImage = "",
String const& pressedImage = "",
String const& disabledImage = "");
void setCheckedImages(String const& baseImage,
String const& hoverImage = "",
String const& pressedImage = "",
String const& disabledImage = "");
void setOverlayImage(String const& overlayImage = "");
// Used to offset drawing when the button is being pressed / checked
Vec2I const& pressedOffset() const;
void setPressedOffset(Vec2I const& offset);
virtual void setText(String const& text);
virtual void setFontSize(int size);
virtual void setTextOffset(Vec2I textOffset);
void setTextAlign(HorizontalAnchor hAnchor);
void setFontColor(Color color);
void setFontColorDisabled(Color color);
void setFontColorChecked(Color color);
virtual WidgetPtr getChildAt(Vec2I const& pos) override;
void disable();
void enable();
void setEnabled(bool enabled);
void setInvisible(bool invisible);
protected:
virtual RectI getScissorRect() const override;
virtual void renderImpl() override;
void drawButtonPart(String const& image, Vec2F const& position);
void updateSize();
WidgetCallbackFunc m_callback;
ButtonGroupPtr m_buttonGroup;
bool m_hovered;
bool m_pressed;
bool m_checkable;
bool m_checked;
bool m_disabled;
bool m_highlighted;
String m_baseImage;
String m_hoverImage;
String m_pressedImage;
String m_disabledImage;
bool m_hasCheckedImages;
String m_baseImageChecked;
String m_hoverImageChecked;
String m_pressedImageChecked;
String m_disabledImageChecked;
String m_overlayImage;
bool m_invisible;
Vec2I m_pressedOffset;
Vec2U m_buttonBoundSize;
int m_fontSize;
String m_text;
Vec2I m_textOffset;
bool m_sustain;
private:
HorizontalAnchor m_hTextAnchor;
Color m_fontColor;
Color m_fontColorDisabled;
Maybe<Color> m_fontColorChecked;
};
}
#endif

View file

@ -0,0 +1,238 @@
#include "StarCanvasWidget.hpp"
namespace Star {
CanvasWidget::CanvasWidget() {
m_captureKeyboard = false;
m_captureMouse = false;
}
void CanvasWidget::setCaptureMouseEvents(bool captureMouse) {
m_captureMouse = captureMouse;
}
void CanvasWidget::setCaptureKeyboardEvents(bool captureKeyboard) {
m_captureKeyboard = captureKeyboard;
}
void CanvasWidget::clear() {
m_renderOps.clear();
}
void CanvasWidget::drawImage(String texName, Vec2F const& position, float scale, Vec4B const& color) {
m_renderOps.append(make_tuple(move(texName), position, scale, color, false));
}
void CanvasWidget::drawImageCentered(String texName, Vec2F const& position, float scale, Vec4B const& color) {
m_renderOps.append(make_tuple(move(texName), position, scale, color, true));
}
void CanvasWidget::drawImageRect(String texName, RectF const& texCoords, RectF const& screenCoords, Vec4B const& color) {
m_renderOps.append(make_tuple(move(texName), texCoords, screenCoords, color));
}
void CanvasWidget::drawDrawable(Drawable drawable, Vec2F const& screenPos) {
m_renderOps.append(make_tuple(move(drawable), screenPos));
}
void CanvasWidget::drawDrawables(List<Drawable> drawables, Vec2F const& screenPos) {
for (auto& drawable : drawables)
drawDrawable(move(drawable), screenPos);
}
void CanvasWidget::drawTiledImage(String texName, float textureScale, Vec2D const& offset, RectF const& screenCoords, Vec4B const& color) {
m_renderOps.append(make_tuple(move(texName), textureScale, offset, screenCoords, color));
}
void CanvasWidget::drawLine(Vec2F const& begin, Vec2F const end, Vec4B const& color, float lineWidth) {
m_renderOps.append(make_tuple(begin, end, color, lineWidth));
}
void CanvasWidget::drawRect(RectF const& coords, Vec4B const& color) {
m_renderOps.append(make_tuple(coords, color));
}
void CanvasWidget::drawPoly(PolyF const& poly, Vec4B const& color, float lineWidth) {
m_renderOps.append(make_tuple(poly, color, lineWidth));
}
void CanvasWidget::drawTriangles(List<tuple<Vec2F, Vec2F, Vec2F>> const& triangles, Vec4B const& color) {
m_renderOps.append(make_tuple(triangles, color));
}
void CanvasWidget::drawText(String s, TextPositioning position, unsigned fontSize, Vec4B const& color, FontMode mode, float lineSpacing) {
m_renderOps.append(make_tuple(move(s), move(position), fontSize, color, mode, lineSpacing));
}
Vec2I CanvasWidget::mousePosition() const {
return m_mousePosition;
}
List<CanvasWidget::ClickEvent> CanvasWidget::pullClickEvents() {
return take(m_clickEvents);
}
List<CanvasWidget::KeyEvent> CanvasWidget::pullKeyEvents() {
return take(m_keyEvents);
}
bool CanvasWidget::sendEvent(InputEvent const& event) {
if (!m_visible)
return false;
auto& context = GuiContext::singleton();
if (auto mouseButtonDown = event.ptr<MouseButtonDownEvent>()) {
if (inMember(*context.mousePosition(event)) && m_captureMouse) {
m_clickEvents.append({*context.mousePosition(event) - screenPosition(), mouseButtonDown->mouseButton, true});
m_clickEvents.limitSizeBack(MaximumEventBuffer);
return true;
}
} else if (auto mouseButtonUp = event.ptr<MouseButtonUpEvent>()) {
if (m_captureMouse) {
m_clickEvents.append({*context.mousePosition(event) - screenPosition(), mouseButtonUp->mouseButton, false});
m_clickEvents.limitSizeBack(MaximumEventBuffer);
return true;
}
} else if (event.is<MouseMoveEvent>()) {
m_mousePosition = *context.mousePosition(event) - screenPosition();
return false;
} else if (auto keyDown = event.ptr<KeyDownEvent>()) {
if (m_captureKeyboard) {
m_keyEvents.append({keyDown->key, true});
return true;
}
} else if (auto keyUp = event.ptr<KeyUpEvent>()) {
if (m_captureKeyboard) {
m_keyEvents.append({keyUp->key, false});
m_keyEvents.limitSizeBack(MaximumEventBuffer);
return true;
}
}
return Widget::sendEvent(event);
}
KeyboardCaptureMode CanvasWidget::keyboardCaptured() const {
return m_captureKeyboard ? KeyboardCaptureMode::KeyEvents : KeyboardCaptureMode::None;
}
void CanvasWidget::renderImpl() {
auto renderingOffset = Vec2F(screenPosition());
for (auto const& op : m_renderOps) {
if (auto args = op.ptr<ImageOp>())
tupleUnpackFunction(bind(&CanvasWidget::renderImage, this, renderingOffset, _1, _2, _3, _4, _5), *args);
if (auto args = op.ptr<ImageRectOp>())
tupleUnpackFunction(bind(&CanvasWidget::renderImageRect, this, renderingOffset, _1, _2, _3, _4), *args);
if (auto args = op.ptr<DrawableOp>())
tupleUnpackFunction(bind(&CanvasWidget::renderDrawable, this, renderingOffset, _1, _2), *args);
if (auto args = op.ptr<TiledImageOp>())
tupleUnpackFunction(bind(&CanvasWidget::renderTiledImage, this, renderingOffset, _1, _2, _3, _4, _5), *args);
if (auto args = op.ptr<LineOp>())
tupleUnpackFunction(bind(&CanvasWidget::renderLine, this, renderingOffset, _1, _2, _3, _4), *args);
if (auto args = op.ptr<RectOp>())
tupleUnpackFunction(bind(&CanvasWidget::renderRect, this, renderingOffset, _1, _2), *args);
if (auto args = op.ptr<PolyOp>())
tupleUnpackFunction(bind(&CanvasWidget::renderPoly, this, renderingOffset, _1, _2, _3), *args);
if (auto args = op.ptr<TrianglesOp>())
tupleUnpackFunction(bind(&CanvasWidget::renderTriangles, this, renderingOffset, _1, _2), *args);
if (auto args = op.ptr<TextOp>())
tupleUnpackFunction(bind(&CanvasWidget::renderText, this, renderingOffset, _1, _2, _3, _4, _5, _6), *args);
}
}
void CanvasWidget::renderImage(Vec2F const& renderingOffset, String const& texName, Vec2F const& position, float scale, Vec4B const& color, bool centered) {
auto& context = GuiContext::singleton();
auto texSize = Vec2F(context.textureSize(texName));
if (centered) {
auto screenCoords = RectF::withSize(renderingOffset * context.interfaceScale() + (position - scale * texSize / 2.0f) * context.interfaceScale(), texSize * scale * context.interfaceScale());
context.drawQuad(texName, screenCoords, color);
} else {
auto screenCoords = RectF::withSize(renderingOffset * context.interfaceScale() + position * context.interfaceScale(), texSize * scale * context.interfaceScale());
context.drawQuad(texName, screenCoords, color);
}
}
void CanvasWidget::renderImageRect(Vec2F const& renderingOffset, String const& texName, RectF const& texCoords, RectF const& screenCoords, Vec4B const& color) {
auto& context = GuiContext::singleton();
context.drawQuad(texName, texCoords, screenCoords.scaled(context.interfaceScale()).translated(renderingOffset * context.interfaceScale()), color);
}
void CanvasWidget::renderDrawable(Vec2F const& renderingOffset, Drawable drawable, Vec2F const& screenPos) {
auto& context = GuiContext::singleton();
drawable.scale(context.interfaceScale());
context.drawDrawable(move(drawable), renderingOffset * context.interfaceScale() + screenPos * context.interfaceScale(), 1);
}
void CanvasWidget::renderTiledImage(Vec2F const& renderingOffset, String const& texName, float textureScale, Vec2D const& offset, RectF const& screenCoords, Vec4B const& color) {
auto& context = GuiContext::singleton();
Vec2F texSize = Vec2F(context.textureSize(texName));
Vec2F texScaledSize = texSize * textureScale;
Vec2I textureCount = Vec2I::ceil(screenCoords.size().piecewiseDivide(texScaledSize)) + Vec2I(2, 2);
Vec2F screenLowerLeft = screenCoords.min() - Vec2F(pfmod<double>(texScaledSize[0] - offset[0], texScaledSize[0]), pfmod<double>(texScaledSize[1] - offset[1], texScaledSize[1]));
for (int x = 0; x < textureCount[0]; ++x) {
for (int y = 0; y < textureCount[1]; ++y) {
Vec2F screenPos = screenLowerLeft + texScaledSize.piecewiseMultiply({(float)x, (float)y});
RectF screenRect = RectF::withSize(screenPos, texScaledSize);
RectF limitedScreenRect;
limitedScreenRect.setXMin(max(screenRect.xMin(), screenCoords.xMin()));
limitedScreenRect.setYMin(max(screenRect.yMin(), screenCoords.yMin()));
limitedScreenRect.setXMax(min(screenRect.xMax(), screenCoords.xMax()));
limitedScreenRect.setYMax(min(screenRect.yMax(), screenCoords.yMax()));
RectF limitedTexRect = limitedScreenRect.translated(-screenPos).scaled(1 / textureScale);
if (limitedScreenRect.isEmpty())
continue;
context.drawQuad(texName, limitedTexRect, limitedScreenRect.translated(renderingOffset).scaled(context.interfaceScale()), color);
}
}
}
void CanvasWidget::renderLine(Vec2F const& renderingOffset, Vec2F const& begin, Vec2F const end, Vec4B const& color, float lineWidth) {
auto& context = GuiContext::singleton();
context.drawLine(
renderingOffset * context.interfaceScale() + begin * context.interfaceScale(),
renderingOffset * context.interfaceScale() + end * context.interfaceScale(),
color, lineWidth);
}
void CanvasWidget::renderRect(Vec2F const& renderingOffset, RectF const& coords, Vec4B const& color) {
auto& context = GuiContext::singleton();
context.drawQuad(coords.scaled(context.interfaceScale()).translated(renderingOffset * context.interfaceScale()), color);
}
void CanvasWidget::renderPoly(Vec2F const& renderingOffset, PolyF poly, Vec4B const& color, float lineWidth) {
auto& context = GuiContext::singleton();
poly.translate(renderingOffset);
context.drawInterfacePolyLines(poly, color, lineWidth);
}
void CanvasWidget::renderTriangles(Vec2F const& renderingOffset, List<tuple<Vec2F, Vec2F, Vec2F>> const& triangles, Vec4B const& color) {
auto& context = GuiContext::singleton();
auto translated = triangles.transformed([&renderingOffset](tuple<Vec2F, Vec2F, Vec2F> const& poly) {
return tuple<Vec2F, Vec2F, Vec2F>(get<0>(poly) + renderingOffset,
get<1>(poly) + renderingOffset,
get<2>(poly) + renderingOffset);
});
context.drawInterfaceTriangles(translated, color);
}
void CanvasWidget::renderText(Vec2F const& renderingOffset, String const& s, TextPositioning const& position, unsigned fontSize, Vec4B const& color, FontMode mode, float lineSpacing) {
auto& context = GuiContext::singleton();
context.setFontSize(fontSize);
context.setFontColor(color);
context.setFontMode(mode);
context.setLineSpacing(lineSpacing);
TextPositioning translatedPosition = position;
translatedPosition.pos += renderingOffset;
context.renderInterfaceText(s, translatedPosition);
context.setDefaultLineSpacing();
}
}

View file

@ -0,0 +1,106 @@
#ifndef STAR_CANVAS_WIDGET_HPP
#define STAR_CANVAS_WIDGET_HPP
#include "StarWidget.hpp"
namespace Star {
STAR_CLASS(CanvasWidget);
// Very simple Widget that allows easy drawing to its surface, to easily tie a
// simplified rendering / input context into the regular widget / GuiReader
// system.
class CanvasWidget : public Widget {
public:
struct ClickEvent {
Vec2I position;
MouseButton button;
// True when button down, false when key up
bool buttonDown;
};
struct KeyEvent {
Key key;
// True when key down, false when key up
bool keyDown;
};
unsigned MaximumEventBuffer = 16;
CanvasWidget();
void setCaptureMouseEvents(bool captureMouse);
void setCaptureKeyboardEvents(bool captureKeyboard);
// Returns mouse position relative to the lower left of the drawing region.
Vec2I mousePosition() const;
// Pulls recent click events relative to the lower left of the drawing
// region, if configured to capture mouse events
List<ClickEvent> pullClickEvents();
// Pulls recent key events captured by this Canvas, if configured to capture
// key events.
List<KeyEvent> pullKeyEvents();
bool sendEvent(InputEvent const& event) override;
KeyboardCaptureMode keyboardCaptured() const override;
// Call before drawing to clear old draw data.
void clear();
void drawImage(String texName, Vec2F const& position, float scale = 1.0f, Vec4B const& color = Vec4B(255, 255, 255, 255));
void drawImageRect(String texName, RectF const& texCoords, RectF const& screenCoords, Vec4B const& color);
void drawImageCentered(String texName, Vec2F const& position, float scale = 1.0f, Vec4B const& color = Vec4B(255, 255, 255, 255));
void drawDrawable(Drawable drawable, Vec2F const& screenPos);
void drawDrawables(List<Drawable> drawables, Vec2F const& screenPos);
// Draw an image whose texture is applied over the entire screen rect in a
// tiled manner, so that it wraps in X and Y.
void drawTiledImage(String texName, float textureScale, Vec2D const& offset, RectF const& screenCoords, Vec4B const& color = Vec4B::filled(255));
void drawLine(Vec2F const& begin, Vec2F const end, Vec4B const& color = Vec4B(255, 255, 255, 255), float lineWidth = 1.0f);
void drawRect(RectF const& coords, Vec4B const& color = Vec4B(255, 255, 255, 255));
void drawPoly(PolyF const& poly, Vec4B const& color = Vec4B(255, 255, 255, 255), float lineWidth = 1.0f);
void drawTriangles(List<tuple<Vec2F, Vec2F, Vec2F>> const& poly, Vec4B const& color = Vec4B(255, 255, 255, 255));
void drawText(String s, TextPositioning position, unsigned fontSize, Vec4B const& color = Vec4B(255, 255, 255, 255), FontMode mode = FontMode::Normal, float lineSpacing = Star::DefaultLineSpacing);
protected:
void renderImpl() override;
void renderImage(Vec2F const& renderingOffset, String const& texName, Vec2F const& position, float scale, Vec4B const& color, bool centered);
void renderImageRect(Vec2F const& renderingOffset, String const& texName, RectF const& texCoords, RectF const& screenCoords, Vec4B const& color);
void renderDrawable(Vec2F const& renderingOffset, Drawable drawable, Vec2F const& screenPos);
void renderTiledImage(Vec2F const& renderingOffset, String const& texName, float textureScale, Vec2D const& offset, RectF const& screenCoords, Vec4B const& color);
void renderLine(Vec2F const& renderingOffset, Vec2F const& begin, Vec2F const end, Vec4B const& color, float lineWidth);
void renderRect(Vec2F const& renderingOffset, RectF const& coords, Vec4B const& color);
void renderPoly(Vec2F const& renderingOffset, PolyF poly, Vec4B const& color, float lineWidth);
void renderTriangles(Vec2F const& renderingOffset, List<tuple<Vec2F, Vec2F, Vec2F>> const& triangles, Vec4B const& color);
void renderText(Vec2F const& renderingOffset, String const& s, TextPositioning const& position, unsigned fontSize, Vec4B const& color, FontMode mode, float lineSpacing);
private:
bool m_captureKeyboard;
bool m_captureMouse;
Vec2I m_mousePosition;
List<ClickEvent> m_clickEvents;
List<KeyEvent> m_keyEvents;
typedef tuple<RectF, Vec4B> RectOp;
typedef tuple<String, Vec2F, float, Vec4B, bool> ImageOp;
typedef tuple<String, RectF, RectF, Vec4B> ImageRectOp;
typedef tuple<Drawable, Vec2F> DrawableOp;
typedef tuple<String, float, Vec2D, RectF, Vec4B> TiledImageOp;
typedef tuple<Vec2F, Vec2F, Vec4B, float> LineOp;
typedef tuple<PolyF, Vec4B, float> PolyOp;
typedef tuple<List<tuple<Vec2F, Vec2F, Vec2F>>, Vec4B> TrianglesOp;
typedef tuple<String, TextPositioning, unsigned, Vec4B, FontMode, float> TextOp;
typedef MVariant<RectOp, ImageOp, ImageRectOp, DrawableOp, TiledImageOp, LineOp, PolyOp, TrianglesOp, TextOp> RenderOp;
List<RenderOp> m_renderOps;
};
}
#endif

View file

@ -0,0 +1,36 @@
#include "StarFlowLayout.hpp"
namespace Star {
FlowLayout::FlowLayout() : m_wrap(true) {}
void FlowLayout::update() {
Layout::update();
int consumedWidth = 0;
int rowHeight = 0;
Vec2I currentOffset = {0, size()[1]};
for (auto child : m_members) {
if (m_wrap && consumedWidth + child->size()[0] > size()[0] && consumedWidth != 0) { // wrapping
currentOffset[0] = 0;
consumedWidth = 0;
currentOffset[1] -= rowHeight + m_spacing[1];
}
if (rowHeight < child->size()[1]) {
rowHeight = child->size()[1];
}
child->setPosition(Vec2I{currentOffset[0], currentOffset[1] - child->size()[1]});
consumedWidth += child->size()[0] + m_spacing[0];
currentOffset[0] = consumedWidth;
}
}
void FlowLayout::setSpacing(Vec2I const& spacing) {
m_spacing = spacing;
}
void FlowLayout::setWrapping(bool wrap) {
m_wrap = wrap;
}
}

View file

@ -0,0 +1,25 @@
#ifndef STAR_FLOW_LAYOUT_HPP
#define STAR_FLOW_LAYOUT_HPP
#include "StarLayout.hpp"
namespace Star {
// Super simple, only supports left to right, top to bottom flow layouts
// currently
STAR_CLASS(FlowLayout);
class FlowLayout : public Layout {
public:
FlowLayout();
virtual void update() override;
void setSpacing(Vec2I const& spacing);
void setWrapping(bool wrap);
private:
Vec2I m_spacing;
bool m_wrap;
};
}
#endif

View file

@ -0,0 +1,111 @@
#include "StarFuelWidget.hpp"
#include "StarInterpolation.hpp"
#include "StarGameTypes.hpp"
#include "StarRoot.hpp"
#include "StarAssets.hpp"
namespace Star {
FuelWidget::FuelWidget() {
auto assets = Root::singleton().assets();
m_fontSize = assets->json("/interface.config:font.buttonSize").toInt();
m_fuelLevel = 0;
m_maxLevel = 0;
m_potential = 0;
m_requested = 0;
m_pingTimeout = 0;
disableScissoring();
}
void FuelWidget::update() {
m_pingTimeout -= WorldTimestep;
if (m_pingTimeout < 0)
m_pingTimeout = 0;
}
void FuelWidget::renderImpl() {
context()->resetInterfaceScissorRect();
Vec2F textureSize = Vec2F(context()->textureSize("/interface/fuel/fuelgauge.png"));
RectF entireTex = RectF::withSize({}, textureSize);
RectF entirePosition = RectF::withSize(Vec2F(screenPosition()), textureSize);
Vec2F textPosition = entirePosition.center();
float fuel = 1;
float fuelPotential = 1;
float fuelRequested = 0;
if (m_maxLevel > 0) {
fuel = std::min(1.0f, m_fuelLevel / m_maxLevel);
fuelPotential = std::min(1.0f, (m_fuelLevel + m_potential) / m_maxLevel);
fuelRequested = std::min(1.0f, m_requested / m_maxLevel);
fuel -= fuelRequested;
if (fuel < 0)
fuel = 0;
}
auto shift = [](float begin, float end, RectF templ) {
RectF result = templ;
result.min()[0] = lerp(begin, templ.min()[0], templ.max()[0]);
result.max()[0] = lerp(end, templ.min()[0], templ.max()[0]);
return result;
};
if (std::fmod(m_pingTimeout, 0.2f) > 0.1f)
context()->drawInterfaceQuad("/interface/fuel/fuelgaugebackgroundflash.png", shift(0, 1, entireTex), shift(0, 1, entirePosition));
else
context()->drawInterfaceQuad("/interface/fuel/fuelgaugebackground.png", shift(0, 1, entireTex), shift(0, 1, entirePosition));
context()->drawInterfaceQuad("/interface/fuel/fuelgaugegreen.png",
shift(fuel, fuelPotential, entireTex),
shift(fuel, fuelPotential, entirePosition));
context()->drawInterfaceQuad("/interface/fuel/fuelgaugered.png",
shift(fuel, fuelRequested, entireTex),
shift(fuel, fuelRequested, entirePosition));
context()->drawInterfaceQuad("/interface/fuel/fuelgauge.png", shift(0, fuel, entireTex), shift(0, fuel, entirePosition));
context()->drawInterfaceQuad("/interface/fuel/fuelgaugemarkings.png", shift(0, 1, entireTex), shift(0, 1, entirePosition));
auto& guiContext = GuiContext::singleton();
guiContext.setFontSize(m_fontSize);
if (m_potential != 0) {
guiContext.setFontColor(Color::White.toRgba());
} else if (m_fuelLevel == 0) {
if ((m_requested != 0) && (m_requested == m_fuelLevel))
guiContext.setFontColor(Color::Orange.toRgba());
else
guiContext.setFontColor(Color::Red.toRgba());
} else {
guiContext.setFontColor(Color::White.toRgba());
}
guiContext.renderInterfaceText(strf("Fuel %s/%s", std::min(m_fuelLevel + m_potential, m_maxLevel), (int)m_maxLevel),
{textPosition, HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor});
}
void FuelWidget::setCurrentFuelLevel(float amount) {
m_fuelLevel = amount;
}
void FuelWidget::setMaxFuelLevel(float amount) {
m_maxLevel = amount;
}
void FuelWidget::setPotentialFuelAmount(float amount) {
m_potential = amount;
}
void FuelWidget::setRequestedFuelAmount(float amount) {
m_requested = amount;
}
void FuelWidget::ping() {
m_pingTimeout = 1.0f;
}
}

View file

@ -0,0 +1,41 @@
#ifndef STAR_FUEL_WIDGET_HPP
#define STAR_FUEL_WIDGET_HPP
#include "StarWidget.hpp"
namespace Star {
STAR_CLASS(FuelWidget);
class FuelWidget : public Widget {
public:
FuelWidget();
virtual ~FuelWidget() {}
virtual void update();
void setCurrentFuelLevel(float amount);
void setMaxFuelLevel(float amount);
void setPotentialFuelAmount(float amount);
void setRequestedFuelAmount(float amount);
void ping();
protected:
virtual void renderImpl();
float m_fuelLevel;
float m_maxLevel;
float m_potential;
float m_requested;
float m_pingTimeout;
unsigned m_fontSize;
private:
};
}
#endif

View file

@ -0,0 +1,427 @@
#include "StarGuiContext.hpp"
#include "StarRoot.hpp"
#include "StarConfiguration.hpp"
#include "StarMixer.hpp"
#include "StarAssets.hpp"
#include "StarImageMetadataDatabase.hpp"
namespace Star {
GuiContext* GuiContext::s_singleton;
GuiContext* GuiContext::singletonPtr() {
return s_singleton;
}
GuiContext& GuiContext::singleton() {
if (!s_singleton)
throw GuiContextException("GuiContext::singleton() called with no GuiContext instance available");
else
return *s_singleton;
}
GuiContext::GuiContext(MixerPtr mixer, ApplicationControllerPtr appController) {
if (s_singleton)
throw GuiContextException("Singleton GuiContext has been constructed twice");
s_singleton = this;
m_mixer = move(mixer);
m_applicationController = move(appController);
m_interfaceScale = 1;
m_shiftHeld = false;
refreshKeybindings();
}
GuiContext::~GuiContext() {
s_singleton = nullptr;
}
void GuiContext::renderInit(RendererPtr renderer) {
m_renderer = move(renderer);
auto textureGroup = m_renderer->createTextureGroup();
m_textureCollection = make_shared<AssetTextureGroup>(textureGroup);
m_drawablePainter = make_shared<DrawablePainter>(m_renderer, m_textureCollection);
m_textPainter = make_shared<TextPainter>(Root::singleton().assets()->font("/hobo.ttf")->clone(), m_renderer, textureGroup);
}
MixerPtr const& GuiContext::mixer() const {
return m_mixer;
}
ApplicationControllerPtr const& GuiContext::applicationController() const {
return m_applicationController;
}
RendererPtr const& GuiContext::renderer() const {
if (!m_renderer)
throw GuiContextException("GuiContext::renderer() called before renderInit");
return m_renderer;
}
AssetTextureGroupPtr const& GuiContext::assetTextureGroup() const {
if (!m_textureCollection)
throw GuiContextException("GuiContext::assetTextureGroup() called before renderInit");
return m_textureCollection;
}
TextPainterPtr const& GuiContext::textPainter() const {
if (!m_textPainter)
throw GuiContextException("GuiContext::textPainter() called before renderInit");
return m_textPainter;
}
unsigned GuiContext::windowWidth() const {
return renderer()->screenSize()[0];
}
unsigned GuiContext::windowHeight() const {
return renderer()->screenSize()[1];
}
Vec2U GuiContext::windowSize() const {
return renderer()->screenSize();
}
Vec2U GuiContext::windowInterfaceSize() const {
return Vec2U::ceil(Vec2F(windowSize()) / interfaceScale());
}
int GuiContext::interfaceScale() const {
return m_interfaceScale;
}
void GuiContext::setInterfaceScale(int interfaceScale) {
m_interfaceScale = interfaceScale;
}
Maybe<Vec2I> GuiContext::mousePosition(InputEvent const& event) const {
auto getInterfacePosition = [this](Vec2I pos) {
return Vec2I(pos) / interfaceScale();
};
if (auto mouseMoveEvent = event.ptr<MouseMoveEvent>())
return getInterfacePosition(mouseMoveEvent->mousePosition);
else if (auto mouseDownEvent = event.ptr<MouseButtonDownEvent>())
return getInterfacePosition(mouseDownEvent->mousePosition);
else if (auto mouseUpEvent = event.ptr<MouseButtonUpEvent>())
return getInterfacePosition(mouseUpEvent->mousePosition);
else if (auto mouseWheelEvent = event.ptr<MouseWheelEvent>())
return getInterfacePosition(mouseWheelEvent->mousePosition);
else
return {};
}
Set<InterfaceAction> GuiContext::actions(InputEvent const& event) const {
return m_keyBindings.actions(event);
}
Set<InterfaceAction> GuiContext::actionsForKey(Key key) const {
return m_keyBindings.actionsForKey(key);
}
void GuiContext::refreshKeybindings() {
m_keyBindings = KeyBindings(Root::singleton().configuration()->get("bindings"));
}
void GuiContext::setInterfaceScissorRect(RectI const& scissor) {
renderer()->setScissorRect(RectI(scissor).scaled(interfaceScale()));
}
void GuiContext::resetInterfaceScissorRect() {
renderer()->setScissorRect({});
}
Vec2U GuiContext::textureSize(String const& texName) {
return assetTextureGroup()->loadTexture(texName)->size();
}
void GuiContext::drawQuad(RectF const& screenCoords, Vec4B const& color) {
renderer()->render(renderFlatRect(screenCoords, color, 0.0f));
}
void GuiContext::drawQuad(String const& texName, RectF const& screenCoords, Vec4B const& color) {
renderer()->render(renderTexturedRect(assetTextureGroup()->loadTexture(texName), screenCoords, color, 0.0f));
}
void GuiContext::drawQuad(String const& texName, Vec2F const& screenPos, int pixelRatio, Vec4B const& color) {
renderer()->render(renderTexturedRect(assetTextureGroup()->loadTexture(texName), screenPos, pixelRatio, color, 0.0f));
}
void GuiContext::drawQuad(String const& texName, RectF const& texCoords, RectF const& screenCoords, Vec4B const& color) {
renderer()->render(RenderQuad{assetTextureGroup()->loadTexture(texName),
RenderVertex{Vec2F(screenCoords.xMin(), screenCoords.yMin()), Vec2F(texCoords.xMin(), texCoords.yMin()), color, 0.0f},
RenderVertex{Vec2F(screenCoords.xMax(), screenCoords.yMin()), Vec2F(texCoords.xMax(), texCoords.yMin()), color, 0.0f},
RenderVertex{Vec2F(screenCoords.xMax(), screenCoords.yMax()), Vec2F(texCoords.xMax(), texCoords.yMax()), color, 0.0f},
RenderVertex{Vec2F(screenCoords.xMin(), screenCoords.yMax()), Vec2F(texCoords.xMin(), texCoords.yMax()), color, 0.0f}});
}
void GuiContext::drawDrawable(Drawable drawable, Vec2F const& screenPos, int pixelRatio, Vec4B const& color) {
if (drawable.isLine())
drawable.linePart().width *= pixelRatio;
drawable.scale(pixelRatio);
drawable.translate(screenPos);
drawable.color *= Color::rgba(color);
m_drawablePainter->drawDrawable(drawable);
}
void GuiContext::drawLine(Vec2F const& begin, Vec2F const end, Vec4B const& color, float lineWidth) {
Vec2F left = vnorm(Vec2F(end) - Vec2F(begin)).rot90() * lineWidth / 2.0f;
renderer()->render(RenderQuad{{},
RenderVertex{Vec2F(begin) + left, Vec2F(), color, 0.0f},
RenderVertex{Vec2F(begin) - left, Vec2F(), color, 0.0f},
RenderVertex{Vec2F(end) - left, Vec2F(), color, 0.0f},
RenderVertex{Vec2F(end) + left, Vec2F(), color, 0.0f}});
}
void GuiContext::drawPolyLines(PolyF const& poly, Vec4B const& color, float lineWidth) {
for (size_t i = 0; i < poly.sides(); ++i)
drawLine(poly.vertex(i), poly.vertex(i + 1), color, lineWidth);
}
void GuiContext::drawTriangles(List<tuple<Vec2F, Vec2F, Vec2F>> const& triangles, Vec4B const& color) {
for (auto poly : triangles) {
renderer()->render(RenderTriangle{{},
RenderVertex{get<0>(poly), Vec2F(), color, 0.0f},
RenderVertex{get<1>(poly), Vec2F(), color, 0.0f},
RenderVertex{get<2>(poly), Vec2F(), color, 0.0f}});
}
}
void GuiContext::drawInterfaceDrawable(Drawable drawable, Vec2F const& screenPos, Vec4B const& color) {
drawDrawable(move(drawable), screenPos * interfaceScale(), (float)interfaceScale(), color);
}
void GuiContext::drawInterfaceLine(Vec2F const& begin, Vec2F const end, Vec4B const& color, float lineWidth) {
drawLine(begin * interfaceScale(), end * interfaceScale(), color, lineWidth * interfaceScale());
}
void GuiContext::drawInterfacePolyLines(PolyF poly, Vec4B const& color, float lineWidth) {
poly.scale(interfaceScale());
drawPolyLines(poly, color, lineWidth * interfaceScale());
}
void GuiContext::drawInterfaceQuad(RectF const& screenCoords, Vec4B const& color) {
drawQuad(screenCoords.scaled(interfaceScale()), color);
}
void GuiContext::drawInterfaceQuad(String const& texName, Vec2F const& screenCoords, Vec4B const& color) {
drawQuad(texName, screenCoords * interfaceScale(), interfaceScale(), color);
}
void GuiContext::drawInterfaceQuad(String const& texName, Vec2F const& screenCoords, float scale, Vec4B const& color) {
drawQuad(texName, screenCoords * interfaceScale(), interfaceScale() * scale, color);
}
void GuiContext::drawInterfaceQuad(String const& texName, RectF const& texCoords, RectF const& screenCoords, Vec4B const& color) {
drawQuad(texName, texCoords, screenCoords.scaled(interfaceScale()), color);
}
void GuiContext::drawInterfaceTriangles(List<tuple<Vec2F, Vec2F, Vec2F>> const& triangles, Vec4B const& color) {
drawTriangles(triangles.transformed([this](tuple<Vec2F, Vec2F, Vec2F> const& poly) {
return tuple<Vec2F, Vec2F, Vec2F>(get<0>(poly) * interfaceScale(), get<1>(poly) * interfaceScale(), get<2>(poly) * interfaceScale());
}), color);
}
void GuiContext::drawImageStretchSet(ImageStretchSet const& imageSet, RectF const& screenPos, GuiDirection direction, Vec4B const& color) {
int innerSize;
int innerOffset = 0;
RectF begin, end, inner;
if (direction == GuiDirection::Horizontal) {
innerSize = screenPos.width();
if (imageSet.begin.size())
innerOffset = textureSize(imageSet.begin)[0];
if (imageSet.end.size())
innerSize = std::max(0, innerSize - innerOffset - Vec2I(textureSize(imageSet.end))[0]);
else
innerSize = std::max(0, innerSize - innerOffset);
if (imageSet.begin.size())
begin = RectF::withSize(screenPos.min(), Vec2F(textureSize(imageSet.begin)[0], screenPos.height()));
else
begin = RectF::withSize(screenPos.min(), Vec2F(0, screenPos.height()));
inner = RectF::withSize(screenPos.min() + Vec2F(innerOffset, 0), Vec2F(innerSize, screenPos.height()));
if (imageSet.end.size())
end = RectF::withSize(screenPos.min() + Vec2F(innerOffset + innerSize, 0), Vec2F(textureSize(imageSet.end)[0], screenPos.height()));
else
end = RectF::withSize(screenPos.min(), Vec2F(0, screenPos.height()));
} else {
innerSize = screenPos.height();
if (imageSet.begin.size())
innerOffset = textureSize(imageSet.begin)[1];
if (imageSet.end.size())
innerSize = std::max(0, innerSize - innerOffset - Vec2I(textureSize(imageSet.end))[1]);
else
innerSize = std::max(0, innerSize - innerOffset);
if (imageSet.begin.size())
begin = RectF::withSize(screenPos.min(), Vec2F(screenPos.width(), textureSize(imageSet.begin)[1]));
else
begin = RectF::withSize(screenPos.min(), Vec2F(screenPos.width(), 0));
inner = RectF::withSize(screenPos.min() + Vec2F(0, innerOffset), Vec2F(screenPos.width(), innerSize));
if (imageSet.end.size()) {
end = RectF::withSize(screenPos.min() + Vec2F(0, innerOffset + innerSize), Vec2F(screenPos.width(), textureSize(imageSet.end)[1]));
} else {
end = RectF::withSize(screenPos.min(), Vec2F(screenPos.width(), 0));
}
}
if (imageSet.begin.size())
drawInterfaceQuad(imageSet.begin, RectF(Vec2F(), Vec2F(textureSize(imageSet.begin))), begin, color);
if (imageSet.type == ImageStretchSet::ImageStretchType::Stretch) {
drawInterfaceQuad(imageSet.inner, RectF(Vec2F(), Vec2F(textureSize(imageSet.inner))), inner, color);
} else {
int position = 0;
auto texSize = Vec2F(textureSize(imageSet.inner));
if (direction == GuiDirection::Horizontal) {
starAssert(texSize[0] > 0);
while (position < inner.width()) {
RectF partialImage = RectF::withSize(Vec2F(), Vec2F(std::min(inner.width() - position, texSize[0]), texSize[1]));
drawInterfaceQuad(imageSet.inner, partialImage, RectF::withSize(inner.min() + Vec2F(position, 0), partialImage.size()), color);
position += partialImage.size()[0];
}
} else {
starAssert(texSize[1] > 0);
while (position < inner.height()) {
RectF partialImage = RectF::withSize(
Vec2F(0, max(0.0f, texSize[1] - (inner.height() - position))),
Vec2F(texSize[0], std::min(inner.height() - position, texSize[1])));
drawInterfaceQuad(imageSet.inner, partialImage, RectF::withSize(inner.min() + Vec2F(0, position), partialImage.size()), color);
position += partialImage.size()[1];
}
}
}
if (imageSet.end.size())
drawInterfaceQuad(imageSet.end, RectF(Vec2F(), Vec2F(textureSize(imageSet.end))), end, color);
}
RectF GuiContext::renderText(String const& s, TextPositioning const& position) {
return textPainter()->renderText(s, position);
}
RectF GuiContext::renderInterfaceText(String const& s, TextPositioning const& position) {
auto res = renderText(s, {
position.pos * interfaceScale(),
position.hAnchor,
position.vAnchor,
position.wrapWidth.apply(bind(std::multiplies<int>(), _1, interfaceScale())),
position.charLimit
});
return RectF(res).scaled(1.0f / interfaceScale());
}
RectF GuiContext::determineTextSize(String const& s, TextPositioning const& positioning) {
return textPainter()->determineTextSize(s, positioning);
}
RectF GuiContext::determineInterfaceTextSize(String const& s, TextPositioning const& positioning) {
auto res = determineTextSize(s, {
positioning.pos * interfaceScale(),
positioning.hAnchor,
positioning.vAnchor,
positioning.wrapWidth.apply(bind(std::multiplies<int>(), _1, interfaceScale()))
});
return RectF(res).scaled(1.0f / interfaceScale());
}
void GuiContext::setFontSize(unsigned size) {
setFontSize(size, interfaceScale());
}
void GuiContext::setFontSize(unsigned size, int pixelRatio) {
textPainter()->setFontSize(size * pixelRatio);
}
void GuiContext::setFontColor(Vec4B const& color) {
textPainter()->setFontColor(color);
}
void GuiContext::setFontMode(FontMode mode) {
textPainter()->setMode(mode);
}
void GuiContext::setFontProcessingDirectives(String const& directives) {
textPainter()->setProcessingDirectives(directives);
}
void GuiContext::setLineSpacing(float lineSpacing) {
textPainter()->setLineSpacing(lineSpacing);
}
void GuiContext::setDefaultLineSpacing() {
textPainter()->setLineSpacing(DefaultLineSpacing);
}
int GuiContext::stringWidth(String const& s) {
return textPainter()->stringWidth(s);
}
int GuiContext::stringInterfaceWidth(String const& s) {
if (interfaceScale()) {
// font size is already adjusted UP by interfaceScale, so we have to adjust
// it back down
return stringWidth(s) / interfaceScale();
}
return 0;
}
StringList GuiContext::wrapText(String const& s, Maybe<unsigned> wrapWidth) {
return textPainter()->wrapText(s, wrapWidth);
}
StringList GuiContext::wrapInterfaceText(String const& s, Maybe<unsigned> wrapWidth) {
if (wrapWidth)
*wrapWidth *= interfaceScale();
return wrapText(s, wrapWidth);
}
bool GuiContext::shiftHeld() const {
return m_shiftHeld;
}
void GuiContext::setShiftHeld(bool held) {
m_shiftHeld = held;
}
void GuiContext::playAudio(AudioInstancePtr audioInstance) {
m_mixer->play(audioInstance);
}
void GuiContext::playAudio(String const& audioAsset, int loops, float volume) {
auto assets = Root::singleton().assets();
auto config = Root::singleton().configuration();
auto audioInstance = make_shared<AudioInstance>(*assets->audio(audioAsset));
audioInstance->setVolume(volume);
audioInstance->setLoops(loops);
m_mixer->play(move(audioInstance));
}
String GuiContext::getClipboard() const {
return m_applicationController->getClipboard().value();
}
void GuiContext::setClipboard(String text) {
m_applicationController->setClipboard(move(text));
}
void GuiContext::cleanup() {
int64_t textureTimeout = Root::singleton().assets()->json("/rendering.config:textureTimeout").toInt();
if (m_textureCollection)
m_textureCollection->cleanup(textureTimeout);
if (m_textPainter)
m_textPainter->cleanup(textureTimeout);
}
}

View file

@ -0,0 +1,145 @@
#ifndef STAR_GUI_CONTEXT_HPP
#define STAR_GUI_CONTEXT_HPP
#include "StarApplicationController.hpp"
#include "StarTextPainter.hpp"
#include "StarDrawablePainter.hpp"
#include "StarAssetTextureGroup.hpp"
#include "StarInputEvent.hpp"
#include "StarDrawable.hpp"
#include "StarThread.hpp"
#include "StarGuiTypes.hpp"
#include "StarRenderer.hpp"
#include "StarKeyBindings.hpp"
#include "StarMixer.hpp"
namespace Star {
STAR_EXCEPTION(GuiContextException, StarException);
class GuiContext {
public:
// Get pointer to the singleton root instance, if it exists. Otherwise,
// returns nullptr.
static GuiContext* singletonPtr();
// Gets reference to GuiContext singleton, throws GuiContextException if root
// is not initialized.
static GuiContext& singleton();
GuiContext(MixerPtr mixer, ApplicationControllerPtr appController);
~GuiContext();
GuiContext(GuiContext const&) = delete;
GuiContext& operator=(GuiContext const&) = delete;
void renderInit(RendererPtr renderer);
MixerPtr const& mixer() const;
ApplicationControllerPtr const& applicationController() const;
RendererPtr const& renderer() const;
AssetTextureGroupPtr const& assetTextureGroup() const;
TextPainterPtr const& textPainter() const;
unsigned windowWidth() const;
unsigned windowHeight() const;
Vec2U windowSize() const;
Vec2U windowInterfaceSize() const;
int interfaceScale() const;
void setInterfaceScale(int interfaceScale);
Maybe<Vec2I> mousePosition(InputEvent const& event) const;
Set<InterfaceAction> actions(InputEvent const& event) const;
// used to cancel chorded inputs on KeyUp
Set<InterfaceAction> actionsForKey(Key key) const;
void refreshKeybindings();
// Drawing wrappers to internal renderers. Automatically loads textures before drawing.
void setInterfaceScissorRect(RectI const& scissor);
void resetInterfaceScissorRect();
Vec2U textureSize(String const& texName);
void drawQuad(RectF const& screenCoords, Vec4B const& color = Vec4B::filled(255));
void drawQuad(String const& texName, RectF const& screenCoords, Vec4B const& color = Vec4B::filled(255));
void drawQuad(String const& texName, Vec2F const& screenPos, int pixelRatio, Vec4B const& color = Vec4B::filled(255));
void drawQuad(String const& texName, RectF const& texCoords, RectF const& screenCoords, Vec4B const& color = Vec4B::filled(255));
void drawDrawable(Drawable drawable, Vec2F const& screenPos, int pixelRatio, Vec4B const& color = Vec4B::filled(255));
void drawLine(Vec2F const& begin, Vec2F const end, Vec4B const& color, float lineWidth = 1);
void drawPolyLines(PolyF const& poly, Vec4B const& color, float lineWidth = 1);
void drawTriangles(List<tuple<Vec2F, Vec2F, Vec2F>> const& triangles, Vec4B const& color);
void drawInterfaceDrawable(Drawable drawable, Vec2F const& screenPos, Vec4B const& color = Vec4B::filled(255));
void drawInterfaceLine(Vec2F const& begin, Vec2F const end, Vec4B const& color, float lineWidth = 1);
void drawInterfacePolyLines(PolyF poly, Vec4B const& color, float lineWidth = 1);
void drawInterfaceTriangles(List<tuple<Vec2F, Vec2F, Vec2F>> const& triangles, Vec4B const& color);
void drawInterfaceQuad(RectF const& screenCoords, Vec4B const& color = Vec4B::filled(255));
void drawInterfaceQuad(String const& texName, Vec2F const& screenPos, Vec4B const& color = Vec4B::filled(255));
void drawInterfaceQuad(String const& texName, Vec2F const& screenPos, float scale, Vec4B const& color = Vec4B::filled(255));
void drawInterfaceQuad(String const& texName, RectF const& texCoords, RectF const& screenCoords, Vec4B const& color = Vec4B::filled(255));
void drawImageStretchSet(ImageStretchSet const& imageSet, RectF const& screenPos, GuiDirection direction = GuiDirection::Horizontal, Vec4B const& color = Vec4B::filled(255));
RectF renderText(String const& s, TextPositioning const& positioning);
RectF renderInterfaceText(String const& s, TextPositioning const& positioning);
RectF determineTextSize(String const& s, TextPositioning const& positioning);
RectF determineInterfaceTextSize(String const& s, TextPositioning const& positioning);
void setFontSize(unsigned size);
void setFontSize(unsigned size, int pixelRatio);
void setFontColor(Vec4B const& color);
void setFontMode(FontMode mode);
void setFontProcessingDirectives(String const& directives);
void setLineSpacing(float lineSpacing);
void setDefaultLineSpacing();
int stringWidth(String const& s);
int stringInterfaceWidth(String const& s);
StringList wrapText(String const& s, Maybe<unsigned> wrapWidth);
StringList wrapInterfaceText(String const& s, Maybe<unsigned> wrapWidth);
void playAudio(AudioInstancePtr audioInstance);
void playAudio(String const& audioAsset, int loops = 0, float volume = 1);
bool shiftHeld() const;
void setShiftHeld(bool held);
String getClipboard() const;
void setClipboard(String text);
void cleanup();
private:
static GuiContext* s_singleton;
MixerPtr m_mixer;
ApplicationControllerPtr m_applicationController;
RendererPtr m_renderer;
AssetTextureGroupPtr m_textureCollection;
TextPainterPtr m_textPainter;
DrawablePainterPtr m_drawablePainter;
KeyBindings m_keyBindings;
int m_interfaceScale;
bool m_shiftHeld;
};
}
#endif

View file

@ -0,0 +1,88 @@
#include "StarGuiReader.hpp"
#include "StarPane.hpp"
#include "StarJsonExtra.hpp"
namespace Star {
GuiReader::GuiReader() {
m_constructors["background"] = [=](
String const& name, Json const& config) { return backgroundHandler(name, config); };
m_constructors["button"] = [=](String const& name, Json const& config) { return buttonHandler(name, config); };
m_constructors["itemslot"] = [=](String const& name, Json const& config) { return itemSlotHandler(name, config); };
m_constructors["itemgrid"] = [=](String const& name, Json const& config) { return itemGridHandler(name, config); };
m_constructors["list"] = [=](String const& name, Json const& config) { return listHandler(name, config); };
m_constructors["panefeature"] = [=](
String const& name, Json const& config) { return paneFeatureHandler(name, config); };
m_constructors["radioGroup"] = [=](
String const& name, Json const& config) { return radioGroupHandler(name, config); };
m_constructors["spinner"] = [=](String const& name, Json const& config) { return spinnerHandler(name, config); };
m_constructors["slider"] = [=](String const& name, Json const& config) { return sliderHandler(name, config); };
m_constructors["textbox"] = [=](String const& name, Json const& config) { return textboxHandler(name, config); };
m_constructors["title"] = [=](String const& name, Json const& config) { return titleHandler(name, config); };
m_constructors["stack"] = [=](String const& name, Json const& config) { return stackHandler(name, config); };
m_constructors["tabSet"] = [=](String const& name, Json const& config) { return tabSetHandler(name, config); };
m_constructors["scrollArea"] = [=](
String const& name, Json const& config) { return scrollAreaHandler(name, config); };
}
WidgetConstructResult GuiReader::titleHandler(String const&, Json const& config) {
if (m_pane) {
String title = config.getString("title", "");
String subtitle = config.getString("subtitle", "");
Json iconConfig = config.get("icon", Json());
if (iconConfig.isNull()) {
m_pane->setTitleString(title, subtitle);
} else {
try {
String type = iconConfig.getString("type");
auto icon = m_constructors.get(type)("icon", iconConfig);
if (!icon.obj)
throw WidgetParserException(strf("Title specified incompatible icon type: %s", type));
m_pane->setTitle(icon.obj, title, subtitle);
} catch (JsonException const& e) {
throw WidgetParserException(strf("Malformed icon configuration data in title. %s", outputException(e, false)));
}
}
} else {
throw StarException("Only Pane controls support the 'title' command");
}
return {};
}
WidgetConstructResult GuiReader::paneFeatureHandler(String const&, Json const& config) {
if (m_pane) {
m_pane->setAnchor(PaneAnchorNames.getLeft(config.getString("anchor", "None")));
if (config.contains("offset"))
m_pane->setAnchorOffset(jsonToVec2I(config.get("offset")));
if (config.getBool("positionLocked", false))
m_pane->lockPosition();
} else {
throw StarException("Only Pane controls support the 'panefeature' command");
}
return {};
}
WidgetConstructResult GuiReader::backgroundHandler(String const&, Json const& config) {
if (m_pane) {
String header, body, footer;
try {
header = config.getString("fileHeader", "");
body = config.getString("fileBody", "");
footer = config.getString("fileFooter", "");
} catch (MapException const& e) {
throw WidgetParserException(
strf("Malformed gui json, missing a required value in the map. %s", outputException(e, false)));
}
m_pane->setBG(header, body, footer);
} else {
throw StarException("Only Pane controls support the 'background' command");
}
return {};
}
}

View file

@ -0,0 +1,22 @@
#ifndef STAR_GUI_READER_HPP
#define STAR_GUI_READER_HPP
#include "StarWidgetParsing.hpp"
namespace Star {
STAR_EXCEPTION(GUIBuilderException, StarException);
class GuiReader : public WidgetParser {
public:
GuiReader();
protected:
WidgetConstructResult titleHandler(String const& _unused, Json const& config);
WidgetConstructResult paneFeatureHandler(String const& _unused, Json const& config);
WidgetConstructResult backgroundHandler(String const& _unused, Json const& config);
};
}
#endif

View file

@ -0,0 +1,29 @@
#include "StarGuiTypes.hpp"
namespace Star {
bool ImageStretchSet::fullyPopulated() const {
return inner.size() && begin.size() && end.size();
}
GuiDirection otherDirection(GuiDirection direction) {
switch (direction) {
case GuiDirection::Horizontal:
return GuiDirection::Vertical;
case GuiDirection::Vertical:
return GuiDirection::Horizontal;
default:
starAssert(false);
return (GuiDirection)-1;
}
}
EnumMap<GuiDirection> const GuiDirectionNames{
{GuiDirection::Horizontal, "horizontal"}, {GuiDirection::Vertical, "vertical"},
};
String rarityBorder(Rarity rarity) {
return strf("/interface/inventory/itemborder%s.png", RarityNames.getRight(rarity).toLower());
}
}

View file

@ -0,0 +1,51 @@
#ifndef STAR_WIDGET_UTILITIES_HPP
#define STAR_WIDGET_UTILITIES_HPP
#include "StarString.hpp"
#include "StarVector.hpp"
#include "StarBiMap.hpp"
#include "StarGameTypes.hpp"
namespace Star {
struct ImageStretchSet {
// how does inner fill up space?
enum class ImageStretchType {
Stretch,
Repeat
};
String begin;
String inner;
String end;
ImageStretchType type;
bool fullyPopulated() const;
};
enum class GuiDirection {
Horizontal,
Vertical
};
extern EnumMap<GuiDirection> const GuiDirectionNames;
GuiDirection otherDirection(GuiDirection direction);
template <typename T>
T directionalValueFromVector(GuiDirection direction, Vector<T, 2> const& vec);
String rarityBorder(Rarity rarity);
template <typename T>
T& directionalValueFromVector(GuiDirection direction, Vector<T, 2> const& vec) {
switch (direction) {
case GuiDirection::Horizontal:
return vec[0];
case GuiDirection::Vertical:
return vec[1];
}
}
}
#endif

View file

@ -0,0 +1,14 @@
#include "StarImageStretchWidget.hpp"
namespace Star {
ImageStretchWidget::ImageStretchWidget(ImageStretchSet const& imageStretchSet, GuiDirection direction)
: m_imageStretchSet(imageStretchSet), m_direction(direction) {
}
void ImageStretchWidget::renderImpl() {
context()->drawImageStretchSet(m_imageStretchSet, RectF(screenBoundRect()), m_direction);
}
}

View file

@ -0,0 +1,25 @@
#ifndef STAR_IMAGE_STRETCH_WIDGET_HPP
#define STAR_IMAGE_STRETCH_WIDGET_HPP
#include "StarWidget.hpp"
namespace Star {
STAR_CLASS(ImageStretchWidget);
class ImageStretchWidget : public Widget {
public:
ImageStretchWidget(ImageStretchSet const& imageStretchSet, GuiDirection direction);
virtual ~ImageStretchWidget() {}
protected:
virtual void renderImpl();
private:
ImageStretchSet m_imageStretchSet;
GuiDirection m_direction;
};
}
#endif

View file

@ -0,0 +1,130 @@
#include "StarImageWidget.hpp"
namespace Star {
ImageWidget::ImageWidget(String const& image) {
m_centered = false;
m_trim = false;
m_scale = 1;
m_rotation = 0;
m_maxSize = Vec2I{4096, 4096};
m_minSize = Vec2I{0, 0};
m_offset = Vec2I{0, 0};
setImage(image);
}
void ImageWidget::renderImpl() {
auto screenPos = screenPosition();
for (auto const& drawable : m_drawables)
context()->drawDrawable(drawable, Vec2F(screenPos) * context()->interfaceScale() + Vec2F(m_offset), context()->interfaceScale(), Vec4B::filled(255));
}
bool ImageWidget::interactive() const {
return false;
}
void ImageWidget::setImage(String const& image) {
if (image.empty())
setDrawables({});
else
setDrawables({Drawable::makeImage(image, 1.0f, false, Vec2F())});
}
void ImageWidget::setScale(float scale) {
m_scale = scale;
transformDrawables();
}
void ImageWidget::setRotation(float rotation) {
m_rotation = rotation;
transformDrawables();
}
String ImageWidget::image() const {
if (!m_drawables.size())
return "";
return m_drawables[0].imagePart().image;
}
void ImageWidget::setDrawables(List<Drawable> drawables) {
m_baseDrawables = move(drawables);
transformDrawables();
}
Vec2I ImageWidget::offset() {
return m_offset;
}
void ImageWidget::setOffset(Vec2I const& offset) {
m_offset = offset;
}
bool ImageWidget::centered() {
return m_centered;
}
void ImageWidget::setCentered(bool centered) {
m_centered = centered;
transformDrawables();
}
bool ImageWidget::trim() {
return m_trim;
}
void ImageWidget::setTrim(bool trim) {
m_trim = trim;
transformDrawables();
}
void ImageWidget::setMaxSize(Vec2I const& size) {
m_maxSize = size;
transformDrawables();
}
void ImageWidget::setMinSize(Vec2I const& size) {
m_minSize = size;
transformDrawables();
}
RectI ImageWidget::screenBoundRect() const {
RectI base = RectI::withSize(screenPosition(), size());
if (m_centered) {
base.translate(-Vec2I::floor(size() / 2));
}
return base;
}
void ImageWidget::transformDrawables() {
m_drawables.clear();
for (auto drawable : m_baseDrawables)
m_drawables.append(Drawable(drawable));
if (m_rotation != 0)
Drawable::rotateAll(m_drawables, m_rotation);
// When 'centered' is true, the drawables provided are pre-centered
// around 0,0. Tooltips use this, as well as quest dialog portraits.
if (m_centered) {
auto boundBox = Drawable::boundBoxAll(m_drawables, m_trim);
Drawable::translateAll(m_drawables, -boundBox.center());
}
auto boundBox = Drawable::boundBoxAll(m_drawables, m_trim);
auto size = boundBox.size().piecewiseMax({0, 0});
if (size[0] && size[1]) {
if ((size[0] > m_maxSize[0]) || (size[1] > m_maxSize[1])) {
m_scale = std::min(m_maxSize[0] / size[0], m_maxSize[1] / size[1]);
} else if ((size[0] < m_minSize[0]) || (size[1] < m_minSize[1])) {
m_scale = std::min(m_minSize[0] / size[0], m_minSize[1] / size[1]);
}
}
Drawable::scaleAll(m_drawables, m_scale);
setSize(Vec2I((size * m_scale).ceil()));
}
}

View file

@ -0,0 +1,51 @@
#ifndef STAR_IMAGE_WIDGET_HPP
#define STAR_IMAGE_WIDGET_HPP
#include "StarWidget.hpp"
namespace Star {
STAR_CLASS(ImageWidget);
class ImageWidget : public Widget {
public:
ImageWidget(String const& image = {});
bool interactive() const override;
void setImage(String const& image);
void setScale(float scale);
void setRotation(float rotation);
String image() const;
void setDrawables(List<Drawable> drawables);
Vec2I offset();
void setOffset(Vec2I const& offset);
bool centered();
void setCentered(bool centered);
bool trim();
void setTrim(bool trim);
void setMaxSize(Vec2I const& size);
void setMinSize(Vec2I const& size);
RectI screenBoundRect() const override;
protected:
void renderImpl() override;
private:
void transformDrawables();
List<Drawable> m_baseDrawables;
List<Drawable> m_drawables;
bool m_centered;
bool m_trim;
float m_scale;
float m_rotation;
Vec2I m_offset;
Vec2I m_maxSize;
Vec2I m_minSize;
};
}
#endif

View file

@ -0,0 +1,289 @@
#include "StarItemGridWidget.hpp"
#include "StarRoot.hpp"
#include "StarJsonExtra.hpp"
#include "StarAssets.hpp"
namespace Star {
ItemGridWidget::ItemGridWidget(ItemBagConstPtr bag, Vec2I const& dimensions, Vec2I const& spacing, String const& backingImage, unsigned bagOffset)
: ItemGridWidget(bag, dimensions, {spacing[0], 0}, {0, spacing[1]}, backingImage, bagOffset) {}
ItemGridWidget::ItemGridWidget(ItemBagConstPtr bag, Vec2I const& dimensions, Vec2I const& rowSpacing, Vec2I const& columnSpacing, String const& backingImage, unsigned bagOffset)
: m_bagOffset(bagOffset), m_dimensions(dimensions), m_rowSpacing(rowSpacing), m_columnSpacing(columnSpacing), m_backingImage(backingImage) {
m_selectedIndex = 0;
m_progress = 1;
m_drawBackingImageWhenFull = false;
m_drawBackingImageWhenEmpty = true;
m_showDurability = false;
m_highlightEmpty = false;
setItemBag(bag);
auto assets = Root::singleton().assets();
m_itemDraggableArea = jsonToRectI(assets->json("/interface.config:itemDraggableArea"));
Vec2I calculatedSize = {
m_dimensions[0] * m_rowSpacing[0] + m_dimensions[1] * m_columnSpacing[0],
m_dimensions[0] * m_rowSpacing[1] + m_dimensions[1] * m_columnSpacing[1]
};
setSize(calculatedSize.piecewiseMax(m_itemDraggableArea.size()));
disableScissoring();
markAsContainer();
}
ItemBagConstPtr ItemGridWidget::bag() const {
return m_bag;
}
ItemPtr ItemGridWidget::itemAt(Vec2I const& position) const {
auto pos = bagLocationAt(position);
if (pos != NPos && m_bag)
return m_bag->at(pos);
return {};
}
ItemPtr ItemGridWidget::itemAt(size_t index) const {
if (index < m_bag->size())
return m_bag->at(index);
return {};
}
ItemPtr ItemGridWidget::selectedItem() const {
return itemAt(selectedIndex());
}
ItemSlotWidgetPtr ItemGridWidget::itemWidgetAt(Vec2I const& position) const {
auto pos = bagLocationAt(position);
if (pos != NPos)
return m_slots[pos];
return {};
}
ItemSlotWidgetPtr ItemGridWidget::itemWidgetAt(size_t index) const {
if (index < m_slots.size())
return m_slots[index];
return {};
}
Vec2I ItemGridWidget::dimensions() const {
return m_dimensions;
}
size_t ItemGridWidget::itemSlots() const {
return m_dimensions.x() * m_dimensions.y();
}
size_t ItemGridWidget::bagSize() const {
if (!m_bag)
return 0;
return m_bag->size();
}
size_t ItemGridWidget::effectiveSize() const {
return min(itemSlots(), bagSize());
}
size_t ItemGridWidget::bagLocationAt(Vec2I const& position) const {
if (m_bag) {
for (size_t i = 0; i < (m_bag->size() - m_bagOffset) && i < unsigned(m_dimensions[0] * m_dimensions[1]); ++i) {
Vec2I loc = locOfItemSlot(i);
RectI bagItemArea = RectI(m_itemDraggableArea).translated(screenPosition() + loc);
if (bagItemArea.contains(position))
return i + m_bagOffset;
}
}
return NPos;
}
Vec2I ItemGridWidget::positionOfSlot(size_t slotNumber) {
return m_slots.at(slotNumber)->position() + position();
}
bool ItemGridWidget::sendEvent(InputEvent const& event) {
if (m_visible) {
if (auto mouseButton = event.ptr<MouseButtonDownEvent>()) {
if (mouseButton->mouseButton == MouseButton::Left || (m_rightClickCallback && mouseButton->mouseButton == MouseButton::Right)) {
Vec2I mousePos = *context()->mousePosition(event);
for (size_t i = 0; i < (m_bag->size() - m_bagOffset) && i < unsigned(m_dimensions[0] * m_dimensions[1]); ++i) {
Vec2I loc = locOfItemSlot(i);
RectI bagItemArea = RectI(m_itemDraggableArea).translated(screenPosition() + loc);
if (bagItemArea.contains(mousePos)) {
m_selectedIndex = i;
if (mouseButton->mouseButton == MouseButton::Right)
m_rightClickCallback(this);
else
m_callback(this);
return true;
}
}
}
}
}
return false;
}
Vec2I ItemGridWidget::locOfItemSlot(unsigned slot) const {
Vec2I loc = {
(int)slot % (int)m_dimensions[0] * m_rowSpacing[0] + // x contribution from row
(int)slot / (int)m_dimensions[0] * m_columnSpacing[0], // x contribution from column
(m_dimensions[0] - 1) * m_rowSpacing[1] - (int)slot % (int)m_dimensions[0] * m_rowSpacing[1] + // y contribution from row
(m_dimensions[1] - 1) * m_columnSpacing[1] - (int)slot / (int)m_dimensions[0] * m_columnSpacing[1] // y contribution from column
};
return loc;
}
void ItemGridWidget::setCallback(WidgetCallbackFunc callback) {
m_callback = callback;
}
void ItemGridWidget::setRightClickCallback(WidgetCallbackFunc callback) {
m_rightClickCallback = callback;
}
void ItemGridWidget::setItemBag(ItemBagConstPtr bag) {
m_bag = bag;
if (!m_bag)
return;
removeAllChildren();
m_slots.clear();
for (size_t i = 0; i < m_bag->size() - m_bagOffset && i < (unsigned)m_dimensions[0] * m_dimensions[1]; ++i) {
auto itemSlot = make_shared<ItemSlotWidget>(m_bag->at(i), m_backingImage);
addChild(strf("%d", i), itemSlot);
m_slots.append(itemSlot);
itemSlot->setBackingImageAffinity(m_drawBackingImageWhenFull, m_drawBackingImageWhenEmpty);
itemSlot->setProgress(m_progress);
itemSlot->setPosition(locOfItemSlot(i));
itemSlot->showDurability(m_showDurability);
}
m_itemNames = slotItemNames();
}
void ItemGridWidget::setProgress(float progress) {
m_progress = progress;
if (!m_bag)
return;
for (size_t i = 0; i < m_bag->size() - m_bagOffset && i < (unsigned)m_dimensions[0] * m_dimensions[1]; ++i) {
auto itemSlot = m_slots.at(i);
itemSlot->setProgress(m_progress);
}
}
size_t ItemGridWidget::selectedIndex() const {
return m_selectedIndex + m_bagOffset;
}
void ItemGridWidget::updateAllItemSlots() {
if (!m_bag)
return;
for (size_t i = 0; i < m_bag->size() - m_bagOffset && i < (unsigned)m_dimensions[0] * m_dimensions[1]; ++i) {
auto item = m_bag->at(i + m_bagOffset);
auto slot = m_slots.at(i);
slot->setItem(item);
slot->setHighlightEnabled(!item && m_highlightEmpty);
}
}
void ItemGridWidget::updateItemState() {
updateAllItemSlots();
auto newState = slotItemNames();
for (size_t i = 0; i < newState.size(); ++i) {
if (newState[i].empty()) {
m_changedSlots.remove(i);
continue;
}
if (newState[i].compare(m_itemNames[i]) != 0)
m_changedSlots.insert(i);
}
m_itemNames = newState;
}
void ItemGridWidget::indicateChangedSlots() {
for (auto i : m_changedSlots)
m_slots[i]->indicateNew();
}
void ItemGridWidget::setHighlightEmpty(bool highlight) {
m_highlightEmpty = highlight;
}
void ItemGridWidget::clearChangedSlots() {
m_changedSlots.clear();
}
bool ItemGridWidget::slotsChanged() {
return m_changedSlots.size() > 0;
}
HashSet<ItemDescriptor> ItemGridWidget::uniqueItemState() {
HashSet<ItemDescriptor> state;
if (!m_bag)
return state;
for (auto item : m_bag->items()) {
if (item)
state.add(item->descriptor().singular());
}
return state;
}
List<String> ItemGridWidget::slotItemNames() {
List<String> itemNames;
for (auto slot : m_slots) {
if (slot->item())
itemNames.append(slot->item()->name());
else
itemNames.append(String());
}
return itemNames;
}
void ItemGridWidget::setBackingImageAffinity(bool full, bool empty) {
m_drawBackingImageWhenFull = full;
m_drawBackingImageWhenEmpty = empty;
if (!m_bag)
return;
for (size_t i = 0; i < m_bag->size() - m_bagOffset && i < (unsigned)m_dimensions[0] * m_dimensions[1]; ++i) {
auto itemSlot = m_slots.at(i);
itemSlot->setBackingImageAffinity(m_drawBackingImageWhenFull, m_drawBackingImageWhenEmpty);
}
}
void ItemGridWidget::showDurability(bool show) {
m_showDurability = show;
if (!m_bag)
return;
for (size_t i = 0; i < m_bag->size() - m_bagOffset && i < (unsigned)m_dimensions[0] * m_dimensions[1]; ++i) {
auto itemSlot = m_slots.at(i);
itemSlot->showDurability(m_showDurability);
}
}
RectI ItemGridWidget::getScissorRect() const {
auto assets = Root::singleton().assets();
auto durabilityOffset = jsonToVec2I(assets->json("/interface.config:itemIconDurabilityOffset"));
auto itemCountRightAnchor = jsonToVec2I(assets->json("/interface.config:itemCountRightAnchor"));
Vec2I extra = Vec2I(durabilityOffset * -1).piecewiseMax(itemCountRightAnchor * -1).piecewiseMax(Vec2I());
return RectI::withSize(screenPosition() - extra, size() + extra);
}
void ItemGridWidget::renderImpl() {
updateAllItemSlots();
}
}

View file

@ -0,0 +1,101 @@
#ifndef STAR_ITEMGRID_WIDGET_HPP
#define STAR_ITEMGRID_WIDGET_HPP
#include "StarItemBag.hpp"
#include "StarWidget.hpp"
#include "StarItemSlotWidget.hpp"
#include "StarItem.hpp"
namespace Star {
STAR_CLASS(ItemGridWidget);
class ItemGridWidget : public Widget {
public:
ItemGridWidget(ItemBagConstPtr bag, Vec2I const& dimensions, Vec2I const& spacing, String const& backingImage, unsigned bagOffset);
ItemGridWidget(ItemBagConstPtr bag, Vec2I const& dimensions, Vec2I const& rowSpacing, Vec2I const& columnSpacing, String const& backingImage, unsigned bagOffset);
ItemBagConstPtr bag() const;
ItemPtr itemAt(Vec2I const& position) const;
ItemPtr itemAt(size_t index) const;
ItemPtr selectedItem() const;
ItemSlotWidgetPtr itemWidgetAt(Vec2I const& position) const;
ItemSlotWidgetPtr itemWidgetAt(size_t index) const;
// Returns the dimensions of the item grid
Vec2I dimensions() const;
// Returns the number of item slots in the grid (dimensions.x() * dimensions.y())
size_t itemSlots() const;
// Returns the size of the underlying bag.
size_t bagSize() const;
// Returns the min of bagSize() and itemSlots()
size_t effectiveSize() const;
size_t bagLocationAt(Vec2I const& position) const;
Vec2I positionOfSlot(size_t slotNumber);
bool sendEvent(InputEvent const& event) override;
void setCallback(WidgetCallbackFunc callback);
void setRightClickCallback(WidgetCallbackFunc callback);
void setItemBag(ItemBagConstPtr bag);
void setProgress(float progress);
size_t selectedIndex() const;
void updateAllItemSlots();
// Item states, keeping track of new items
void updateItemState();
void clearChangedSlots();
bool slotsChanged();
void indicateChangedSlots();
void setHighlightEmpty(bool highlight);
void setBackingImageAffinity(bool full, bool empty);
void showDurability(bool show);
virtual RectI getScissorRect() const override;
protected:
void renderImpl() override;
HashSet<ItemDescriptor> uniqueItemState();
List<String> slotItemNames();
private:
Vec2I locOfItemSlot(unsigned slot) const;
ItemBagConstPtr m_bag;
List<ItemSlotWidgetPtr> m_slots;
unsigned m_bagOffset;
Vec2I m_dimensions;
Vec2I m_rowSpacing;
Vec2I m_columnSpacing;
List<String> m_itemNames;
Set<size_t> m_changedSlots;
RectI m_itemDraggableArea;
String m_backingImage;
bool m_drawBackingImageWhenFull;
bool m_drawBackingImageWhenEmpty;
bool m_showDurability;
float m_progress;
bool m_highlightEmpty;
unsigned m_selectedIndex;
WidgetCallbackFunc m_callback;
WidgetCallbackFunc m_rightClickCallback;
};
}
#endif

View file

@ -0,0 +1,205 @@
#include "StarItemSlotWidget.hpp"
#include "StarRoot.hpp"
#include "StarJsonExtra.hpp"
#include "StarWidgetParsing.hpp"
#include "StarImageMetadataDatabase.hpp"
#include "StarItem.hpp"
#include "StarDurabilityItem.hpp"
#include "StarAssets.hpp"
#include "StarGameTypes.hpp"
namespace Star {
ItemSlotWidget::ItemSlotWidget(ItemPtr const& item, String const& backingImage)
: m_item(item), m_backingImage(backingImage) {
m_drawBackingImageWhenFull = false;
m_drawBackingImageWhenEmpty = true;
m_fontSize = 0;
m_progress = 1;
auto assets = Root::singleton().assets();
auto interfaceConfig = assets->json("/interface.config");
m_countPosition = TextPositioning(jsonToVec2F(interfaceConfig.get("itemCountRightAnchor")), HorizontalAnchor::RightAnchor);
m_countFontMode = FontMode::Normal;
m_fontSize = interfaceConfig.query("font.itemSize").toInt();
m_fontColor = Color::rgb(jsonToVec3B(interfaceConfig.query("font.defaultColor")));
m_itemDraggableArea = jsonToRectI(interfaceConfig.get("itemDraggableArea"));
m_durabilityOffset = jsonToVec2I(interfaceConfig.get("itemIconDurabilityOffset"));
auto newItemIndicatorConfig = interfaceConfig.get("newItemAnimation");
m_newItemIndicator = Animation(newItemIndicatorConfig);
// End animation before it begins, only display when triggered
m_newItemIndicator.update(newItemIndicatorConfig.getDouble("animationCycle") * newItemIndicatorConfig.getDouble("loops", 1.0f));
Json highlightAnimationConfig = interfaceConfig.get("highlightAnimation");
m_highlightAnimation = Animation(highlightAnimationConfig);
m_highlightEnabled = false;
Vec2I backingImageSize;
if (m_backingImage.size()) {
auto imgMetadata = Root::singleton().imageMetadataDatabase();
backingImageSize = Vec2I(imgMetadata->imageSize(m_backingImage));
}
setSize(m_itemDraggableArea.max().piecewiseMax(backingImageSize));
WidgetParser parser;
parser.construct(assets->json("/interface/itemSlot.config").get("config"), this);
m_durabilityBar = fetchChild<ProgressWidget>("durabilityBar");
m_durabilityBar->hide();
m_showDurability = false;
m_showCount = true;
m_showRarity = true;
m_showLinkIndicator = false;
disableScissoring();
}
void ItemSlotWidget::update() {
Widget::update();
}
bool ItemSlotWidget::sendEvent(InputEvent const& event) {
if (m_visible) {
if (auto mouseButton = event.ptr<MouseButtonDownEvent>()) {
if (mouseButton->mouseButton == MouseButton::Left || (m_rightClickCallback && mouseButton->mouseButton == MouseButton::Right)) {
Vec2I mousePos = *context()->mousePosition(event);
RectI itemArea = m_itemDraggableArea.translated(screenPosition());
if (itemArea.contains(mousePos)) {
if (mouseButton->mouseButton == MouseButton::Right)
m_rightClickCallback(this);
else
m_callback(this);
return true;
}
}
}
}
return false;
}
void ItemSlotWidget::setCallback(WidgetCallbackFunc callback) {
m_callback = callback;
}
void ItemSlotWidget::setRightClickCallback(WidgetCallbackFunc callback) {
m_rightClickCallback = callback;
}
void ItemSlotWidget::setItem(ItemPtr const& item) {
m_item = item;
}
ItemPtr ItemSlotWidget::item() const {
return m_item;
}
void ItemSlotWidget::setProgress(float progress) {
m_progress = progress;
}
void ItemSlotWidget::setBackingImageAffinity(bool full, bool empty) {
m_drawBackingImageWhenFull = full;
m_drawBackingImageWhenEmpty = empty;
}
void ItemSlotWidget::setCountPosition(TextPositioning textPositioning) {
m_countPosition = textPositioning;
}
void ItemSlotWidget::setCountFontMode(FontMode fontMode) {
m_countFontMode = fontMode;
}
void ItemSlotWidget::showDurability(bool show) {
m_showDurability = show;
}
void ItemSlotWidget::showCount(bool show) {
m_showCount = show;
}
void ItemSlotWidget::showRarity(bool showRarity) {
m_showRarity = showRarity;
}
void ItemSlotWidget::showLinkIndicator(bool showLinkIndicator) {
m_showLinkIndicator = showLinkIndicator;
}
void ItemSlotWidget::indicateNew() {
m_newItemIndicator.reset();
}
void ItemSlotWidget::setHighlightEnabled(bool highlight) {
if (!m_highlightEnabled && highlight)
m_highlightAnimation.reset();
m_highlightEnabled = highlight;
}
void ItemSlotWidget::renderImpl() {
if (m_item) {
if (m_drawBackingImageWhenFull && m_backingImage != "")
context()->drawInterfaceQuad(m_backingImage, Vec2F(screenPosition()));
List<Drawable> iconDrawables = m_item->iconDrawables();
if (m_showRarity) {
String border = rarityBorder(m_item->rarity());
context()->drawInterfaceQuad(border, Vec2F(screenPosition()));
}
if (m_showLinkIndicator) {
// TODO: Hardcoded
context()->drawInterfaceQuad("/interface/inventory/itemlinkindicator.png",
Vec2F(screenPosition() - Vec2I(1, 1)));
}
for (auto i : iconDrawables)
context()->drawInterfaceDrawable(i, Vec2F(screenPosition() + size() / 2));
m_newItemIndicator.update(WorldTimestep);
if (!m_newItemIndicator.isComplete())
context()->drawInterfaceDrawable(m_newItemIndicator.drawable(1.0), Vec2F(screenPosition() + size() / 2), Color::White.toRgba());
if (m_showDurability) {
if (auto durabilityItem = as<DurabilityItem>(m_item)) {
float amount = durabilityItem->durabilityStatus();
m_durabilityBar->setCurrentProgressLevel(amount);
if (amount < 1)
m_durabilityBar->show();
else
m_durabilityBar->hide();
} else {
m_durabilityBar->hide();
}
}
int frame = (int)roundf(m_progress * 18); // TODO: Hardcoded lol
context()->drawInterfaceQuad(strf("/interface/cooldown.png:%d", frame), Vec2F(screenPosition()));
if (m_item->count() > 1 && m_showCount) { // we don't need to tell people that there's only 1 of something
context()->setFontSize(m_fontSize);
context()->setFontColor(m_fontColor.toRgba());
context()->setFontMode(m_countFontMode);
context()->renderInterfaceText(strf("%d", m_item->count()), m_countPosition.translated(Vec2F(screenPosition())));
}
} else if (m_drawBackingImageWhenEmpty && m_backingImage != "") {
context()->drawInterfaceQuad(m_backingImage, Vec2F(screenPosition()));
int frame = (int)roundf(m_progress * 18); // TODO: Hardcoded lol
context()->drawInterfaceQuad(strf("/interface/cooldown.png:%d", frame), Vec2F(screenPosition()));
}
if (m_highlightEnabled) {
m_highlightAnimation.update(WorldTimestep);
context()->drawInterfaceDrawable(m_highlightAnimation.drawable(1.0), Vec2F(screenPosition() + size() / 2), Color::White.toRgba());
}
if (!m_item)
m_durabilityBar->hide();
}
}

View file

@ -0,0 +1,76 @@
#ifndef STAR_ITEMSLOT_WIDGET_HPP
#define STAR_ITEMSLOT_WIDGET_HPP
#include "StarWidget.hpp"
#include "StarProgressWidget.hpp"
#include "StarAnimation.hpp"
namespace Star {
STAR_CLASS(Item);
STAR_CLASS(ItemSlotWidget);
static float const ItemIndicateNewTime = 1.5f;
class ItemSlotWidget : public Widget {
public:
ItemSlotWidget(ItemPtr const& item, String const& backingImage);
virtual void update() override;
bool sendEvent(InputEvent const& event) override;
void setCallback(WidgetCallbackFunc callback);
void setRightClickCallback(WidgetCallbackFunc callback);
void setItem(ItemPtr const& item);
ItemPtr item() const;
void setProgress(float progress);
void setBackingImageAffinity(bool full, bool empty);
void setCountPosition(TextPositioning textPositioning);
void setCountFontMode(FontMode fontMode);
void showDurability(bool show);
void showCount(bool show);
void showRarity(bool showRarity);
void showLinkIndicator(bool showLinkIndicator);
void indicateNew();
void setHighlightEnabled(bool highlight);
protected:
virtual void renderImpl() override;
private:
ItemPtr m_item;
String m_backingImage;
bool m_drawBackingImageWhenFull;
bool m_drawBackingImageWhenEmpty;
bool m_showDurability;
bool m_showCount;
bool m_showRarity;
bool m_showLinkIndicator;
TextPositioning m_countPosition;
FontMode m_countFontMode;
Vec2I m_durabilityOffset;
RectI m_itemDraggableArea;
int m_fontSize;
Color m_fontColor;
WidgetCallbackFunc m_callback;
WidgetCallbackFunc m_rightClickCallback;
float m_progress;
ProgressWidgetPtr m_durabilityBar;
Animation m_newItemIndicator;
bool m_highlightEnabled;
Animation m_highlightAnimation;
};
}
#endif

View file

@ -0,0 +1,212 @@
#include "StarKeyBindings.hpp"
#include "StarRoot.hpp"
#include "StarConfiguration.hpp"
#include "StarLogging.hpp"
#include "StarJsonExtra.hpp"
#include <bitset>
namespace Star {
HashMap<Key, KeyMod> const KeyChordMods{
{Key::LShift, KeyMod::LShift},
{Key::RShift, KeyMod::RShift},
{Key::LCtrl, KeyMod::LCtrl},
{Key::RCtrl, KeyMod::RCtrl},
{Key::LAlt, KeyMod::LAlt},
{Key::RAlt, KeyMod::RAlt},
{Key::LGui, KeyMod::LGui},
{Key::RGui, KeyMod::RGui},
{Key::AltGr, KeyMod::AltGr}
};
EnumMap<InterfaceAction> const InterfaceActionNames{
{InterfaceAction::None, "None"},
{InterfaceAction::PlayerUp, "PlayerUp"},
{InterfaceAction::PlayerDown, "PlayerDown"},
{InterfaceAction::PlayerLeft, "PlayerLeft"},
{InterfaceAction::PlayerRight, "PlayerRight"},
{InterfaceAction::PlayerJump, "PlayerJump"},
{InterfaceAction::PlayerMainItem, "PlayerMainItem"},
{InterfaceAction::PlayerAltItem, "PlayerAltItem"},
{InterfaceAction::PlayerDropItem, "PlayerDropItem"},
{InterfaceAction::PlayerInteract, "PlayerInteract"},
{InterfaceAction::PlayerShifting, "PlayerShifting"},
{InterfaceAction::PlayerTechAction1, "PlayerTechAction1"},
{InterfaceAction::PlayerTechAction2, "PlayerTechAction2"},
{InterfaceAction::PlayerTechAction3, "PlayerTechAction3"},
{InterfaceAction::EmoteBlabbering, "EmoteBlabbering"},
{InterfaceAction::EmoteShouting, "EmoteShouting"},
{InterfaceAction::EmoteHappy, "EmoteHappy"},
{InterfaceAction::EmoteSad, "EmoteSad"},
{InterfaceAction::EmoteNeutral, "EmoteNeutral"},
{InterfaceAction::EmoteLaugh, "EmoteLaugh"},
{InterfaceAction::EmoteAnnoyed, "EmoteAnnoyed"},
{InterfaceAction::EmoteOh, "EmoteOh"},
{InterfaceAction::EmoteOooh, "EmoteOooh"},
{InterfaceAction::EmoteBlink, "EmoteBlink"},
{InterfaceAction::EmoteWink, "EmoteWink"},
{InterfaceAction::EmoteEat, "EmoteEat"},
{InterfaceAction::EmoteSleep, "EmoteSleep"},
{InterfaceAction::ShowLabels, "ShowLabels"},
{InterfaceAction::CameraShift, "CameraShift"},
{InterfaceAction::TitleBack, "TitleBack"},
{InterfaceAction::CinematicSkip, "CinematicSkip"},
{InterfaceAction::CinematicNext, "CinematicNext"},
{InterfaceAction::GuiClose, "GuiClose"},
{InterfaceAction::GuiShifting, "GuiShifting"},
{InterfaceAction::KeybindingClear, "KeybindingClear"},
{InterfaceAction::KeybindingCancel, "KeybindingCancel"},
{InterfaceAction::ChatPageUp, "ChatPageUp"},
{InterfaceAction::ChatPageDown, "ChatPageDown"},
{InterfaceAction::ChatPreviousLine, "ChatPreviousLine"},
{InterfaceAction::ChatNextLine, "ChatNextLine"},
{InterfaceAction::ChatSendLine, "ChatSendLine"},
{InterfaceAction::ChatBegin, "ChatBegin"},
{InterfaceAction::ChatBeginCommand, "ChatBeginCommand"},
{InterfaceAction::ChatStop, "ChatStop"},
{InterfaceAction::InterfaceShowHelp, "InterfaceShowHelp"},
{InterfaceAction::InterfaceHideHud, "InterfaceHideHud"},
{InterfaceAction::InterfaceChangeBarGroup, "InterfaceChangeBarGroup"},
{InterfaceAction::InterfaceDeselectHands, "InterfaceDeselectHands"},
{InterfaceAction::InterfaceBar1, "InterfaceBar1"},
{InterfaceAction::InterfaceBar2, "InterfaceBar2"},
{InterfaceAction::InterfaceBar3, "InterfaceBar3"},
{InterfaceAction::InterfaceBar4, "InterfaceBar4"},
{InterfaceAction::InterfaceBar5, "InterfaceBar5"},
{InterfaceAction::InterfaceBar6, "InterfaceBar6"},
{InterfaceAction::InterfaceBar7, "InterfaceBar7"},
{InterfaceAction::InterfaceBar8, "InterfaceBar8"},
{InterfaceAction::InterfaceBar9, "InterfaceBar9"},
{InterfaceAction::InterfaceBar10, "InterfaceBar10"},
{InterfaceAction::EssentialBar1, "EssentialBar1"},
{InterfaceAction::EssentialBar2, "EssentialBar2"},
{InterfaceAction::EssentialBar3, "EssentialBar3"},
{InterfaceAction::EssentialBar4, "EssentialBar4"},
{InterfaceAction::InterfaceRepeatCommand, "InterfaceRepeatCommand"},
{InterfaceAction::InterfaceToggleFullscreen, "InterfaceToggleFullscreen"},
{InterfaceAction::InterfaceReload, "InterfaceReload"},
{InterfaceAction::InterfaceEscapeMenu, "InterfaceEscapeMenu"},
{InterfaceAction::InterfaceInventory, "InterfaceInventory"},
{InterfaceAction::InterfaceCodex, "InterfaceCodex"},
{InterfaceAction::InterfaceQuest, "InterfaceQuest"},
{InterfaceAction::InterfaceCrafting, "InterfaceCrafting"},
};
bool KeyChord::operator<(KeyChord const& rhs) const {
return tie(key, mods) < tie(rhs.key, rhs.mods);
}
KeyChord inputDescriptorFromJson(Json const& json) {
Key key;
auto type = json.getString("type");
if (type == "key") {
auto value = json.get("value");
if (value.isType(Json::Type::String)) {
key = KeyNames.getLeft(value.toString());
} else if (value.canConvert(Json::Type::Int)) {
key = (Key)value.toUInt();
} else {
throw StarException::format("Improper key value '%s'", value);
}
} else {
throw StarException::format("Improper bindings type '%s'", type);
}
KeyMod mods = KeyMod::NoMod;
for (auto mod : json.get("mods").iterateArray())
mods |= KeyModNames.getLeft(mod.toString());
return {key, mods};
}
Json inputDescriptorToJson(KeyChord const& chord) {
JsonArray modNames;
for (auto const& p : KeyModNames) {
if ((chord.mods & p.first) != KeyMod::NoMod)
modNames.append(p.second);
}
return JsonObject{
{"type", "key"},
{"value", KeyNames.getRight(chord.key)},
{"mods", modNames}
};
}
String printInputDescriptor(KeyChord chord) {
StringList modNames;
for (auto const& p : KeyModNames) {
if ((chord.mods & p.first) != KeyMod::NoMod)
modNames.append(p.second);
}
return String::joinWith(" + ", modNames.join(" + "), KeyNames.getRight(chord.key));
}
KeyBindings::KeyBindings() {}
KeyBindings::KeyBindings(Json const& json) {
Map<Key, List<pair<KeyMod, InterfaceAction>>> actions;
try {
for (auto const& kvpair : json.iterateObject()) {
InterfaceAction action = InterfaceActionNames.getLeft(kvpair.first);
for (auto const& input : kvpair.second.iterateArray()) {
try {
auto chord = inputDescriptorFromJson(input);
actions[chord.key].append({chord.mods, action});
} catch (StarException const& e) {
Logger::warn("Could not load keybinding for %s: %s\n",
InterfaceActionNames.getRight(action),
outputException(e, false));
}
}
}
m_actions = move(actions);
} catch (StarException const& e) {
throw StarException(strf("Could not set keybindings from configuration. %s", outputException(e, false)));
}
}
Set<InterfaceAction> KeyBindings::actions(Key key) const {
return actions(KeyChord{key, KeyMod::NoMod});
}
Set<InterfaceAction> KeyBindings::actions(InputEvent const& event) const {
if (auto keyDown = event.ptr<KeyDownEvent>())
return actions(KeyChord{keyDown->key, keyDown->mods});
return {};
}
Set<InterfaceAction> KeyBindings::actions(KeyChord chord) const {
size_t mostMatchedMods = 0;
Set<InterfaceAction> matching;
for (auto const& pair : m_actions.value(chord.key)) {
// first make sure that all required mods for the binding are held
if ((pair.first & chord.mods) == pair.first) {
// now count the number of mods in the binding
size_t matchedMods = 0;
for (auto modPair : KeyChordMods) {
if ((modPair.second & pair.first) == modPair.second)
++matchedMods;
}
if (matchedMods > mostMatchedMods) {
matching.clear();
mostMatchedMods = matchedMods;
}
// only activate the binding(s) with the most mods
if (matchedMods == mostMatchedMods)
matching.add(pair.second);
}
}
return matching;
}
Set<InterfaceAction> KeyBindings::actionsForKey(Key key) const {
return Set<InterfaceAction>::from(m_actions.value(key).transformed([](auto p){ return p.second; }));
}
}

View file

@ -0,0 +1,119 @@
#ifndef STAR_KEY_BINDINGS_HPP
#define STAR_KEY_BINDINGS_HPP
#include "StarInputEvent.hpp"
#include "StarSet.hpp"
#include "StarJson.hpp"
namespace Star {
enum class InterfaceAction {
None,
PlayerUp,
PlayerDown,
PlayerLeft,
PlayerRight,
PlayerJump,
PlayerMainItem,
PlayerAltItem,
PlayerDropItem,
PlayerInteract,
PlayerShifting,
PlayerTechAction1,
PlayerTechAction2,
PlayerTechAction3,
EmoteBlabbering,
EmoteShouting,
EmoteHappy,
EmoteSad,
EmoteNeutral,
EmoteLaugh,
EmoteAnnoyed,
EmoteOh,
EmoteOooh,
EmoteBlink,
EmoteWink,
EmoteEat,
EmoteSleep,
ShowLabels,
CameraShift,
TitleBack,
CinematicSkip,
CinematicNext,
GuiClose,
GuiShifting,
KeybindingClear,
KeybindingCancel,
ChatPageUp,
ChatPageDown,
ChatPreviousLine,
ChatNextLine,
ChatSendLine,
ChatBegin,
ChatBeginCommand,
ChatStop,
InterfaceShowHelp,
InterfaceHideHud,
InterfaceChangeBarGroup,
InterfaceDeselectHands,
InterfaceBar1,
InterfaceBar2,
InterfaceBar3,
InterfaceBar4,
InterfaceBar5,
InterfaceBar6,
InterfaceBar7,
InterfaceBar8,
InterfaceBar9,
InterfaceBar10,
EssentialBar1,
EssentialBar2,
EssentialBar3,
EssentialBar4,
InterfaceRepeatCommand,
InterfaceToggleFullscreen,
InterfaceReload,
InterfaceEscapeMenu,
InterfaceInventory,
InterfaceCodex,
InterfaceQuest,
InterfaceCrafting,
};
extern EnumMap<InterfaceAction> const InterfaceActionNames;
// Maps the mod keys that can used in key chords to its associated KeyMod.
extern HashMap<Key, KeyMod> const KeyChordMods;
struct KeyChord {
Key key;
KeyMod mods;
bool operator<(KeyChord const& rhs) const;
};
KeyChord inputDescriptorFromJson(Json const& json);
Json inputDescriptorToJson(KeyChord const& chord);
String printInputDescriptor(KeyChord chord);
STAR_CLASS(KeyBindings);
class KeyBindings {
public:
KeyBindings();
explicit KeyBindings(Json const& json);
Set<InterfaceAction> actions(Key key) const;
Set<InterfaceAction> actions(InputEvent const& event) const;
Set<InterfaceAction> actions(KeyChord chord) const;
Set<InterfaceAction> actionsForKey(Key key) const;
private:
// Maps the primary key to a list of InterfaceActions, and any mods that they
// require to be held.
Map<Key, List<pair<KeyMod, InterfaceAction>>> m_actions;
};
}
#endif

View file

@ -0,0 +1,112 @@
#include "StarLabelWidget.hpp"
#include "StarRoot.hpp"
#include "StarAssets.hpp"
namespace Star {
LabelWidget::LabelWidget(String text,
Color const& color,
HorizontalAnchor const& hAnchor,
VerticalAnchor const& vAnchor,
Maybe<unsigned> wrapWidth,
Maybe<float> lineSpacing)
: m_color(color),
m_hAnchor(hAnchor),
m_vAnchor(vAnchor),
m_wrapWidth(move(wrapWidth)),
m_lineSpacing(move(lineSpacing)) {
auto assets = Root::singleton().assets();
m_fontSize = assets->json("/interface.config:font").getInt("baseSize");
setText(move(text));
}
String const& LabelWidget::text() const {
return m_text;
}
Maybe<unsigned> LabelWidget::getTextCharLimit() const {
return m_textCharLimit;
}
void LabelWidget::setText(String newText) {
m_text = move(newText);
updateTextRegion();
}
void LabelWidget::setFontSize(int fontSize) {
m_fontSize = fontSize;
updateTextRegion();
}
void LabelWidget::setColor(Color newColor) {
m_color = move(newColor);
}
void LabelWidget::setAnchor(HorizontalAnchor hAnchor, VerticalAnchor vAnchor) {
m_hAnchor = hAnchor;
m_vAnchor = vAnchor;
updateTextRegion();
}
void LabelWidget::setWrapWidth(Maybe<unsigned> wrapWidth) {
m_wrapWidth = move(wrapWidth);
updateTextRegion();
}
void LabelWidget::setLineSpacing(Maybe<float> lineSpacing) {
m_lineSpacing = move(lineSpacing);
updateTextRegion();
}
void LabelWidget::setDirectives(String const& directives) {
m_processingDirectives = directives;
updateTextRegion();
}
void LabelWidget::setTextCharLimit(Maybe<unsigned> charLimit) {
m_textCharLimit = charLimit;
updateTextRegion();
}
RectI LabelWidget::relativeBoundRect() const {
return RectI(m_textRegion).translated(relativePosition());
}
RectI LabelWidget::getScissorRect() const {
return noScissor();
}
void LabelWidget::renderImpl() {
context()->setFontSize(m_fontSize);
context()->setFontColor(m_color.toRgba());
context()->setFontProcessingDirectives(m_processingDirectives);
if (m_lineSpacing)
context()->setLineSpacing(*m_lineSpacing);
else
context()->setDefaultLineSpacing();
context()->renderInterfaceText(m_text, {Vec2F(screenPosition()), m_hAnchor, m_vAnchor, m_wrapWidth, m_textCharLimit});
context()->setFontProcessingDirectives("");
context()->setDefaultLineSpacing();
}
void LabelWidget::updateTextRegion() {
context()->setFontSize(m_fontSize);
context()->setFontColor(m_color.toRgba());
context()->setFontProcessingDirectives(m_processingDirectives);
if (m_lineSpacing)
context()->setLineSpacing(*m_lineSpacing);
else
context()->setDefaultLineSpacing();
m_textRegion = RectI(context()->determineInterfaceTextSize(m_text, {Vec2F(), m_hAnchor, m_vAnchor, m_wrapWidth, m_textCharLimit}));
setSize(m_textRegion.size());
context()->setFontProcessingDirectives("");
context()->setDefaultLineSpacing();
}
}

View file

@ -0,0 +1,52 @@
#ifndef STAR_LABEL_WIDGET_HPP
#define STAR_LABEL_WIDGET_HPP
#include "StarWidget.hpp"
namespace Star {
STAR_CLASS(LabelWidget);
class LabelWidget : public Widget {
public:
LabelWidget(String text = String(),
Color const& color = Color::White,
HorizontalAnchor const& hAnchor = HorizontalAnchor::LeftAnchor,
VerticalAnchor const& vAnchor = VerticalAnchor::BottomAnchor,
Maybe<unsigned> wrapWidth = {},
Maybe<float> lineSpacing = {});
String const& text() const;
Maybe<unsigned> getTextCharLimit() const;
void setText(String newText);
void setFontSize(int fontSize);
void setColor(Color newColor);
void setAnchor(HorizontalAnchor hAnchor, VerticalAnchor vAnchor);
void setWrapWidth(Maybe<unsigned> wrapWidth);
void setLineSpacing(Maybe<float> lineSpacing);
void setDirectives(String const& directives);
void setTextCharLimit(Maybe<unsigned> charLimit);
RectI relativeBoundRect() const override;
protected:
virtual RectI getScissorRect() const override;
virtual void renderImpl() override;
private:
void updateTextRegion();
String m_text;
int m_fontSize;
Color m_color;
HorizontalAnchor m_hAnchor;
VerticalAnchor m_vAnchor;
String m_processingDirectives;
Maybe<unsigned> m_wrapWidth;
Maybe<float> m_lineSpacing;
Maybe<unsigned> m_textCharLimit;
RectI m_textRegion;
};
}
#endif

View file

@ -0,0 +1,160 @@
#include "StarLargeCharPlateWidget.hpp"
#include "StarRoot.hpp"
#include "StarJsonExtra.hpp"
#include "StarPlayer.hpp"
#include "StarAssets.hpp"
namespace Star {
LargeCharPlateWidget::LargeCharPlateWidget(WidgetCallbackFunc mainCallback, PlayerPtr player) : m_player(player) {
m_portraitScale = 0;
setSize(ButtonWidget::size());
auto assets = Root::singleton().assets();
auto charPlateImage = assets->json("/interface.config:largeCharPlate.backingImage").toString();
setCallback(mainCallback);
setImages(charPlateImage);
m_playerPlate = charPlateImage;
m_playerPlateHover = assets->json("/interface.config:largeCharPlate.playerHover").toString();
m_noPlayerPlate = assets->json("/interface.config:largeCharPlate.noPlayer").toString();
m_noPlayerPlateHover = assets->json("/interface.config:largeCharPlate.noPlayerHover").toString();
m_portraitOffset = jsonToVec2I(assets->json("/interface.config:largeCharPlate.portraitOffset"));
m_portraitScale = assets->json("/interface.config:largeCharPlate.portraitScale").toFloat();
String switchText = assets->json("/interface.config:largeCharPlate.switchText").toString();
String createText = assets->json("/interface.config:largeCharPlate.createText").toString();
m_portrait = make_shared<PortraitWidget>();
m_portrait->setScale(m_portraitScale);
m_portrait->setPosition(m_portraitOffset);
addChild("portrait", m_portrait);
String modeLabelText = assets->json("/interface.config:largeCharPlate.modeText").toString();
m_regularTextColor = Color::rgb(jsonToVec3B(assets->json("/interface.config:largeCharPlate.textColor")));
m_disabledTextColor = Color::rgb(jsonToVec3B(assets->json("/interface.config:largeCharPlate.textColorDisabled")));
m_modeNameOffset = jsonToVec2I(assets->json("/interface.config:largeCharPlate.modeNameOffset"));
m_modeOffset = jsonToVec2I(assets->json("/interface.config:largeCharPlate.modeOffset"));
m_modeName = make_shared<LabelWidget>(modeLabelText, Color::White, HorizontalAnchor::HMidAnchor);
addChild("modeName", m_modeName);
m_modeName->setPosition(m_modeNameOffset);
m_modeName->setAnchor(HorizontalAnchor::HMidAnchor, VerticalAnchor::BottomAnchor);
m_mode = make_shared<LabelWidget>();
addChild("mode", m_mode);
m_mode->setPosition(m_modeOffset);
m_mode->setAnchor(HorizontalAnchor::LeftAnchor, VerticalAnchor::BottomAnchor);
m_createCharText = assets->json("/interface.config:largeCharPlate.noPlayerText").toString();
m_createCharTextColor = Color::rgb(jsonToVec3B(assets->json("/interface.config:largeCharPlate.noPlayerTextColor")));
m_playerNameOffset = jsonToVec2I(assets->json("/interface.config:largeCharPlate.playerNameOffset"));
m_playerName = make_shared<LabelWidget>();
m_playerName->setColor(m_createCharTextColor);
m_playerName->setPosition(m_playerNameOffset);
m_playerName->setAnchor(HorizontalAnchor::HMidAnchor, VerticalAnchor::BottomAnchor);
addChild("player", m_playerName);
}
void LargeCharPlateWidget::renderImpl() {
Vec2I pressedOffset = isPressed() ? ButtonWidget::pressedOffset() : Vec2I(0, 0);
m_portrait->setPosition(m_portraitOffset + pressedOffset);
m_mode->setPosition(m_modeOffset + pressedOffset);
m_modeName->setPosition(m_modeNameOffset + pressedOffset);
m_playerName->setPosition(m_playerNameOffset + pressedOffset);
if (m_delete) {
m_delete->setPosition(m_deleteOffset + pressedOffset);
}
if (m_player) {
ButtonWidget::setImages(m_playerPlate, m_playerPlateHover);
ButtonWidget::renderImpl();
m_modeName->setColor(m_regularTextColor);
m_playerName->setColor(m_regularTextColor);
m_playerName->setText(m_player->name());
} else {
ButtonWidget::setImages(m_noPlayerPlate, m_noPlayerPlateHover);
ButtonWidget::enable();
ButtonWidget::renderImpl();
m_modeName->setColor(m_disabledTextColor);
m_playerName->setColor(m_createCharTextColor);
m_playerName->setText(m_createCharText);
}
}
void LargeCharPlateWidget::mouseOut() {
if (m_delete)
m_delete->mouseOut();
ButtonWidget::mouseOut();
}
void LargeCharPlateWidget::setPlayer(PlayerPtr player) {
m_player = player;
m_portrait->setEntity(m_player);
if (m_player) {
m_playerName->setText(m_player->name());
} else {
m_playerName->setText(m_createCharText);
}
auto modeTypeTextAndColor = Root::singleton().assets()->json("/interface.config:modeTypeTextAndColor").toArray();
int modeType;
if (m_player) {
modeType = 1 + (int)m_player->modeType();
} else {
modeType = 0;
}
auto thisModeType = modeTypeTextAndColor[modeType].toArray();
String modeTypeText = thisModeType[0].toString();
Color modeTypeColor = Color::rgb(jsonToVec3B(thisModeType[1]));
m_mode->setText(modeTypeText);
m_mode->setColor(modeTypeColor);
}
void LargeCharPlateWidget::enableDelete(WidgetCallbackFunc const& callback) {
disableDelete();
auto assets = Root::singleton().assets();
auto baseImage = assets->json("/interface.config:largeCharPlate.trashButton.baseImage").toString();
auto hoverImage = assets->json("/interface.config:largeCharPlate.trashButton.hoverImage").toString();
auto pressedImage = assets->json("/interface.config:largeCharPlate.trashButton.pressedImage").toString();
auto disabledImage = assets->json("/interface.config:largeCharPlate.trashButton.disabledImage").toString();
auto offset = jsonToVec2I(assets->json("/interface.config:largeCharPlate.trashButton.offset"));
m_delete = make_shared<ButtonWidget>(callback, baseImage, hoverImage, pressedImage, disabledImage);
addChild("trashButton", m_delete);
m_delete->setPosition(offset);
m_deleteOffset = offset;
}
void LargeCharPlateWidget::disableDelete() {
if (m_delete) {
removeChild(m_delete.get());
}
m_delete = {};
}
bool LargeCharPlateWidget::sendEvent(InputEvent const& event) {
if (event.is<MouseMoveEvent>() && m_delete) {
if (m_delete->inMember(*m_context->mousePosition(event)))
m_delete->mouseOver();
else
m_delete->mouseOut();
}
if (Widget::sendEvent(event))
return true;
return ButtonWidget::sendEvent(event);
}
}

View file

@ -0,0 +1,64 @@
#ifndef STAR_LARGE_CHAR_PLATE_WIDGET_HPP
#define STAR_LARGE_CHAR_PLATE_WIDGET_HPP
#include "StarWidget.hpp"
#include "StarButtonWidget.hpp"
#include "StarPortraitWidget.hpp"
#include "StarLabelWidget.hpp"
namespace Star {
STAR_CLASS(Player);
STAR_CLASS(LargeCharPlateWidget);
class LargeCharPlateWidget : public ButtonWidget {
public:
LargeCharPlateWidget(WidgetCallbackFunc mainCallback, PlayerPtr player = PlayerPtr());
void mouseOut() override;
void setPlayer(PlayerPtr player = PlayerPtr());
void enableDelete(WidgetCallbackFunc const& callback);
void disableDelete();
virtual bool sendEvent(InputEvent const& event) override;
protected:
virtual void renderImpl() override;
private:
PlayerPtr m_player;
PortraitWidgetPtr m_portrait;
Vec2I m_portraitOffset;
float m_portraitScale;
String m_playerPlateHover;
String m_noPlayerPlate;
String m_noPlayerPlateHover;
String m_playerPlate;
LabelWidgetPtr m_playerName;
LabelWidgetPtr m_playerPhrase;
LabelWidgetPtr m_modeName;
LabelWidgetPtr m_mode;
ButtonWidgetPtr m_delete;
Vec2I m_playerNameOffset;
Vec2I m_playerPhraseOffset;
Vec2I m_modeNameOffset;
Vec2I m_modeOffset;
Vec2I m_deleteOffset;
String m_createCharText;
Color m_createCharTextColor;
Color m_regularTextColor;
Color m_disabledTextColor;
};
}
#endif

View file

@ -0,0 +1,13 @@
#include "StarLayout.hpp"
namespace Star {
Layout::Layout() {
markAsContainer();
}
void Layout::update() {
Widget::update();
}
}

View file

@ -0,0 +1,16 @@
#ifndef STAR_LAYOUT_HPP
#define STAR_LAYOUT_HPP
#include "StarWidget.hpp"
namespace Star {
// VERY simple base class for a layout container object.
class Layout : public Widget {
public:
Layout();
virtual void update() override;
};
}
#endif

View file

@ -0,0 +1,262 @@
#include "StarListWidget.hpp"
#include "StarGuiReader.hpp"
#include "StarJsonExtra.hpp"
#include "StarRandom.hpp"
#include "StarImageWidget.hpp"
#include "StarAssets.hpp"
#include "StarRoot.hpp"
namespace Star {
ListWidget::ListWidget(Json const& schema) : m_schema(schema) {
m_selectedItem = NPos;
m_columns = 1;
setSchema(m_schema);
updateSizeAndPosition();
}
ListWidget::ListWidget() {
m_selectedItem = NPos;
m_columns = 1;
updateSizeAndPosition();
}
RectI ListWidget::relativeBoundRect() const {
if (m_fillDown)
return RectI::withSize(relativePosition() - Vec2I(0, size()[1]), size());
else
return RectI::withSize(relativePosition(), size());
}
void ListWidget::setCallback(WidgetCallbackFunc callback) {
m_callback = move(callback);
}
bool ListWidget::sendEvent(InputEvent const& event) {
if (!m_visible)
return false;
for (size_t i = m_members.size(); i != 0; --i) {
auto child = m_members[i - 1];
if (child->sendEvent(event)
|| (event.is<MouseButtonDownEvent>() && child->inMember(*context()->mousePosition(event))
&& event.get<MouseButtonDownEvent>().mouseButton == MouseButton::Left)) {
setSelected(i - 1);
return true;
}
setHovered(i - 1, event.is<MouseMoveEvent>() && child->inMember(*context()->mousePosition(event)));
}
return false;
}
void ListWidget::setSchema(Json const& schema) {
clear();
m_schema = schema;
try {
m_selectedBG = schema.getString("selectedBG", "");
m_unselectedBG = schema.getString("unselectedBG", "");
m_hoverBG = schema.getString("hoverBG", "");
m_disabledBG = schema.getString("disabledBG", "");
if (m_disabledBG.empty() && !m_unselectedBG.empty())
m_disabledBG = m_unselectedBG + Root::singleton().assets()->json("/interface.config:disabledButton").toString();
m_spacing = jsonToVec2I(schema.get("spacing"));
m_memberSize = jsonToVec2I(schema.get("memberSize"));
} catch (JsonException const& e) {
throw GuiException(strf("Missing required value in map: %s", outputException(e, false)));
}
updateSizeAndPosition();
}
WidgetPtr ListWidget::addItem() {
auto newItem = constructWidget();
addChild(strf("%d", Random::randu64()), newItem);
updateSizeAndPosition();
return newItem;
}
WidgetPtr ListWidget::addItem(size_t at) {
auto newItem = constructWidget();
addChildAt(strf("%d", Random::randu64()), newItem, at);
updateSizeAndPosition();
if (m_selectedItem != NPos && at <= m_selectedItem)
setSelected(m_selectedItem + 1);
return newItem;
}
WidgetPtr ListWidget::addItem(WidgetPtr existingItem) {
addChild(strf("%d", Random::randu64()), existingItem);
updateSizeAndPosition();
return existingItem;
}
WidgetPtr ListWidget::constructWidget() {
WidgetPtr newItem = make_shared<Widget>();
m_reader.construct(m_schema.get("listTemplate"), newItem.get());
newItem->setSize(m_memberSize);
m_doScissor ? newItem->enableScissoring() : newItem->disableScissoring();
return newItem;
}
void ListWidget::updateSizeAndPosition() {
int rows = m_members.size() % m_columns ? m_members.size() / m_columns + 1 : m_members.size() / m_columns;
for (size_t i = 0; i < m_members.size(); i++) {
Vec2I currentOffset;
int col = i % m_columns;
int row = rows - (i / m_columns) - 1;
if (m_fillDown)
row -= rows;
currentOffset = Vec2I((m_memberSize[0] + m_spacing[0]) * col, (m_memberSize[1] + m_spacing[1]) * row);
if (!m_fillDown)
currentOffset[1] += m_spacing[1];
m_members[i]->setPosition(currentOffset);
}
if (m_members.size()) {
auto width = (m_memberSize[0] + m_spacing[0]) * m_columns;
auto height = (m_memberSize[1] + m_spacing[1]) * rows;
setSize(Vec2I(width, height));
} else {
setSize(Vec2I());
}
}
void ListWidget::setEnabled(size_t pos, bool enabled) {
if (pos != NPos && pos < listSize()) {
if (enabled) {
m_disabledItems.remove(pos);
if (auto bgWidget = itemAt(pos)->fetchChild<ImageWidget>("background"))
bgWidget->setImage(pos == m_selectedItem ? m_selectedBG : m_unselectedBG);
} else {
m_disabledItems.add(pos);
if (m_selectedItem == pos)
clearSelected();
if (auto bgWidget = itemAt(pos)->fetchChild<ImageWidget>("background"))
bgWidget->setImage(m_disabledBG);
}
}
}
void ListWidget::setHovered(size_t pos, bool hovered) {
if (m_hoverBG == "")
return;
if (pos != m_selectedItem && pos < listSize() && !m_disabledItems.contains(pos)) {
if (auto bgWidget = itemAt(pos)->fetchChild<ImageWidget>("background")) {
if (hovered)
bgWidget->setImage(m_hoverBG);
else
bgWidget->setImage(m_unselectedBG);
}
}
}
void ListWidget::setSelected(size_t pos) {
if ((m_selectedItem != NPos) && (m_selectedItem < listSize())) {
if (auto bgWidget = selectedWidget()->fetchChild<ImageWidget>("background"))
bgWidget->setImage(m_unselectedBG);
}
if (!m_disabledItems.contains(pos) && m_selectedItem != pos) {
m_selectedItem = pos;
if (m_callback)
m_callback(this);
}
if (m_selectedItem != NPos) {
if (auto bgWidget = selectedWidget()->fetchChild<ImageWidget>("background"))
bgWidget->setImage(m_selectedBG);
}
}
void ListWidget::clearSelected() {
setSelected(NPos);
}
void ListWidget::setSelectedWidget(WidgetPtr selected) {
auto offset = itemPosition(selected);
if (offset == NPos) {
throw GuiException("Attempted to select item not in list.");
}
setSelected(offset);
}
void ListWidget::registerMemberCallback(String const& name, WidgetCallbackFunc const& callback) {
m_reader.registerCallback(name, callback);
}
void ListWidget::setFillDown(bool fillDown) {
m_fillDown = fillDown;
}
void ListWidget::setColumns(uint64_t columns) {
m_columns = columns;
}
void ListWidget::removeItem(size_t at) {
removeChildAt(at);
if (m_selectedItem == at)
setSelected(NPos);
else if (m_selectedItem != NPos && m_selectedItem > at)
setSelected(m_selectedItem - 1);
updateSizeAndPosition();
}
void ListWidget::removeItem(WidgetPtr item) {
auto offset = itemPosition(item);
if (offset == NPos) {
throw GuiException("Attempted to remove item not in list.");
}
removeItem(offset);
}
void ListWidget::clear() {
setSelected(NPos);
removeAllChildren();
updateSizeAndPosition();
}
size_t ListWidget::selectedItem() const {
return m_selectedItem;
}
size_t ListWidget::itemPosition(WidgetPtr item) const {
size_t offset = NPos;
for (size_t i = 0; i < m_members.size(); ++i) {
if (m_members[i] == item) {
offset = i;
break;
}
}
return offset;
}
WidgetPtr ListWidget::itemAt(size_t n) const {
if (n < m_members.size()) {
return m_members[n];
} else {
return {};
}
}
WidgetPtr ListWidget::selectedWidget() const {
return itemAt(m_selectedItem);
}
List<WidgetPtr> const& ListWidget::list() const {
return m_members;
}
size_t ListWidget::listSize() const {
return numChildren();
}
}

View file

@ -0,0 +1,70 @@
#ifndef STAR_LIST_WIDGET_HPP
#define STAR_LIST_WIDGET_HPP
#include "StarWidget.hpp"
#include "StarGuiReader.hpp"
namespace Star {
STAR_CLASS(GuiReader);
STAR_CLASS(ListWidget);
class ListWidget : public Widget {
public:
ListWidget(Json const& schema);
ListWidget();
RectI relativeBoundRect() const override;
// Callback is called when the selection changes
void setCallback(WidgetCallbackFunc callback);
bool sendEvent(InputEvent const& event) override;
void setSchema(Json const& schema);
WidgetPtr constructWidget();
WidgetPtr addItem();
WidgetPtr addItem(size_t at);
WidgetPtr addItem(WidgetPtr existingItem);
void removeItem(size_t at);
void removeItem(WidgetPtr item);
void clear();
size_t selectedItem() const;
size_t itemPosition(WidgetPtr item) const;
WidgetPtr itemAt(size_t n) const;
WidgetPtr selectedWidget() const;
List<WidgetPtr> const& list() const;
size_t listSize() const;
void setEnabled(size_t pos, bool enabled);
void setHovered(size_t pos, bool hovered);
void setSelected(size_t pos);
void clearSelected();
void setSelectedWidget(WidgetPtr selected);
void registerMemberCallback(String const& name, WidgetCallbackFunc const& callback);
void setFillDown(bool fillDown);
void setColumns(uint64_t columns);
private:
void updateSizeAndPosition();
Json m_schema;
GuiReader m_reader;
Set<size_t> m_disabledItems;
size_t m_selectedItem;
WidgetCallbackFunc m_callback;
String m_selectedBG;
String m_unselectedBG;
String m_hoverBG;
String m_disabledBG;
Vec2I m_spacing;
bool m_fillDown;
uint64_t m_columns;
};
}
#endif

View file

@ -0,0 +1,369 @@
#include "StarPane.hpp"
#include "StarRoot.hpp"
#include "StarJsonExtra.hpp"
#include "StarAssets.hpp"
namespace Star {
EnumMap<PaneAnchor> const PaneAnchorNames{
{PaneAnchor::None, "none"},
{PaneAnchor::BottomLeft, "bottomLeft"},
{PaneAnchor::BottomRight, "bottomRight"},
{PaneAnchor::TopLeft, "topLeft"},
{PaneAnchor::TopRight, "topRight"},
{PaneAnchor::CenterBottom, "centerBottom"},
{PaneAnchor::CenterTop, "centerTop"},
{PaneAnchor::CenterLeft, "centerLeft"},
{PaneAnchor::CenterRight, "centerRight"},
{PaneAnchor::Center, "center"},
};
Pane::Pane() {
m_dragActive = m_lockPosition = false;
m_dismissed = true;
m_centerOffset = Vec2I();
m_anchor = PaneAnchor::None;
m_anchorOffset = Vec2I();
m_visible = false;
m_hasDisplayed = false;
auto assets = Root::singleton().assets();
m_fontSize = assets->json("/interface.config:font.baseSize").toInt();
m_iconOffset = jsonToVec2I(assets->json("/interface.config:paneIconOffset"));
m_titleOffset = jsonToVec2I(assets->json("/interface.config:paneTitleOffset"));
m_subTitleOffset = jsonToVec2I(assets->json("/interface.config:paneSubTitleOffset"));
m_titleColor = jsonToColor(assets->json("/interface.config:paneTitleColor"));
m_subTitleColor = jsonToColor(assets->json("/interface.config:paneSubTitleColor"));
}
void Pane::displayed() {
m_dismissed = false;
m_hasDisplayed = true;
show();
}
void Pane::dismissed() {
if (m_clickDown)
m_clickDown->mouseOut();
m_clickDown.reset();
if (m_mouseOver)
m_mouseOver->mouseOut();
m_mouseOver.reset();
hide();
m_dismissed = true;
}
void Pane::dismiss() {
m_dismissed = true;
}
bool Pane::isDismissed() const {
return m_dismissed;
}
bool Pane::isDisplayed() const {
return !m_dismissed;
}
bool Pane::sendEvent(InputEvent const& event) {
if (m_visible) {
if (event.is<MouseButtonDownEvent>() || event.is<MouseButtonUpEvent>() || event.is<MouseMoveEvent>()
|| event.is<MouseWheelEvent>()) {
Vec2I mousePos = *m_context->mousePosition(event);
// First, handle preliminary mouse out / click up events
if (m_mouseOver) {
if (!m_mouseOver->inMember(mousePos) || !m_mouseOver->active()) {
m_mouseOver->mouseOut();
m_mouseOver.reset();
}
}
if (event.is<MouseButtonUpEvent>())
m_clickDown.reset();
WidgetPtr newClickDown;
WidgetPtr newMouseOver;
WidgetPtr newFocusWidget;
// Then, go through widgets in highest to lowest z-order and handle mouse
// over, focus, and capture events.
for (auto const& widget : reverseIterate(m_members)) {
if (widget->inMember(mousePos) && widget->active() && widget->interactive()) {
WidgetPtr topWidget = widget;
WidgetPtr child = getChildAt(mousePos);
if (child->active() && child->interactive()) {
if (event.is<MouseButtonDownEvent>()
&& (event.get<MouseButtonDownEvent>().mouseButton == MouseButton::Left
|| event.get<MouseButtonDownEvent>().mouseButton == MouseButton::Right)) {
if (!newClickDown)
newClickDown = child;
if (!newFocusWidget)
newFocusWidget = child;
}
if (!newMouseOver)
newMouseOver = child;
}
}
}
if (m_clickDown != newClickDown)
m_clickDown = newClickDown;
if (m_mouseOver != newMouseOver) {
if (m_mouseOver)
m_mouseOver->mouseOut();
m_mouseOver = newMouseOver;
if (m_mouseOver) {
if (m_clickDown == m_mouseOver)
m_mouseOver->mouseReturnStillDown();
else
m_mouseOver->mouseOver();
}
}
if (newFocusWidget && m_focusWidget != newFocusWidget) {
if (auto focusWidget = m_focusWidget)
focusWidget->blur();
m_focusWidget = newFocusWidget;
m_focusWidget->focus();
}
// Finally go through widgets in highest to lowest z-order and send the
// raw event, stopping further processing if the widget consumes it.
for (auto const& widget : reverseIterate(m_members)) {
if (widget->inMember(mousePos) && widget->active() && widget->interactive()) {
if (widget->sendEvent(event))
return true;
}
}
}
if (event.is<MouseButtonDownEvent>()) {
Vec2I mousePos = *m_context->mousePosition(event);
if (inDragArea(mousePos) && !m_lockPosition) {
setDragActive(true, mousePos);
return true;
}
if (inWindow(mousePos))
return true;
}
if (m_focusWidget) {
if (m_focusWidget->sendEvent(event))
return true;
}
}
return false;
}
void Pane::setFocus(Widget const* focus) {
if (m_focusWidget.get() == focus)
return;
if (m_focusWidget)
m_focusWidget->blur();
if (auto c = childPtr(focus))
m_focusWidget = move(c);
else
throw GuiException("Cannot set focus on a widget which is not a child of this pane");
}
void Pane::removeFocus(Widget const* focus) {
if (m_focusWidget.get() == focus)
m_focusWidget.reset();
}
void Pane::removeFocus() {
m_focusWidget.reset();
}
Pane const* Pane::window() const {
return this;
}
Pane* Pane::window() {
return this;
}
void Pane::renderImpl() {
if (m_bgFooter != "")
m_context->drawInterfaceQuad(m_bgFooter, Vec2F(position()));
if (m_bgBody != "")
m_context->drawInterfaceQuad(m_bgBody, Vec2F(position()) + Vec2F(0, m_footerSize[1]));
if (m_bgHeader != "") {
auto headerPos = Vec2F(position()) + Vec2F(0, m_footerSize[1] + m_bodySize[1]);
m_context->drawInterfaceQuad(m_bgHeader, headerPos);
if (m_icon) {
m_icon->setPosition(Vec2I(0, m_footerSize[1] + m_bodySize[1]) + m_iconOffset);
m_icon->render(m_drawingArea);
m_context->resetInterfaceScissorRect();
}
m_context->setFontSize(m_fontSize);
m_context->setFontColor(m_titleColor.toRgba());
m_context->setFontMode(FontMode::Shadow);
m_context->renderInterfaceText(m_title, {headerPos + Vec2F(m_titleOffset)});
m_context->setFontColor(m_subTitleColor.toRgba());
m_context->renderInterfaceText(m_subTitle, {headerPos + Vec2F(m_subTitleOffset)});
m_context->setFontMode(FontMode::Normal);
}
}
void Pane::update() {
if (m_visible) {
for (auto const& widget : m_members) {
widget->update();
if ((m_focusWidget == widget) != widget->hasFocus()) {
m_focusWidget.reset();
widget->blur();
}
}
}
}
void Pane::tick() {}
bool Pane::dragActive() const {
return m_dragActive;
}
Vec2I Pane::dragMouseOrigin() const {
return m_dragMouseOrigin;
}
void Pane::setDragActive(bool dragActive, Vec2I dragMouseOrigin) {
m_dragActive = dragActive;
m_dragMouseOrigin = dragMouseOrigin;
}
void Pane::drag(Vec2I mousePosition) {
Vec2I delta = mousePosition - m_dragMouseOrigin;
setPosition(position() + delta);
m_dragMouseOrigin += delta;
}
bool Pane::inWindow(Vec2I const& position) const {
return screenBoundRect().contains(position);
}
bool Pane::inDragArea(Vec2I const& position) const {
return inWindow(position) && (position[1] < (this->position()[1] + m_footerSize[1])
|| position[1] > (this->position()[1] + (m_footerSize[1] + m_bodySize[1])));
}
Vec2I Pane::cursorRelativeToPane(Vec2I const& position) const {
return position - this->position();
}
Vec2I Pane::centerOffset() const {
return m_centerOffset;
}
void Pane::setBG(BGResult const& res) {
setBG(res.header, res.body, res.footer);
}
void Pane::setBG(String const& header, String const& body, String const& footer) {
m_bgHeader = header;
m_bgBody = body;
m_bgFooter = footer;
if (m_bgHeader != "") {
m_headerSize = Vec2I(m_context->textureSize(m_bgHeader));
} else {
m_headerSize = {};
}
if (m_bgBody != "") {
m_bodySize = Vec2I(m_context->textureSize(m_bgBody));
} else {
m_bodySize = {};
}
if (m_bgFooter != "") {
m_footerSize = Vec2I(m_context->textureSize(m_bgFooter));
} else {
m_footerSize = {};
}
setSize(Vec2I(std::max(std::max(m_headerSize[0], m_bodySize[0]), m_footerSize[0]),
m_headerSize[1] + m_bodySize[1] + m_footerSize[1]));
}
Pane::BGResult Pane::getBG() const {
return {m_bgHeader, m_bgBody, m_bgFooter};
}
void Pane::lockPosition() {
m_lockPosition = true;
}
void Pane::unlockPosition() {
m_lockPosition = false;
}
void Pane::setTitle(WidgetPtr icon, String const& title, String const& subTitle) {
m_icon = icon;
m_title = title;
m_subTitle = subTitle;
if (m_icon) {
m_icon->setParent(this);
m_icon->show();
}
}
void Pane::setTitleString(String const& title, String const& subTitle) {
m_title = title;
m_subTitle = subTitle;
}
void Pane::setTitleIcon(WidgetPtr icon) {
m_icon = icon;
if (m_icon) {
m_icon->setParent(this);
m_icon->show();
}
}
String Pane::title() const {
return m_title;
}
String Pane::subTitle() const {
return m_subTitle;
}
WidgetPtr Pane::titleIcon() const {
return m_icon;
}
PaneAnchor Pane::anchor() {
return m_anchor;
}
void Pane::setAnchor(PaneAnchor anchor) {
m_anchor = anchor;
}
Vec2I Pane::anchorOffset() const {
return m_anchorOffset;
}
void Pane::setAnchorOffset(Vec2I anchorOffset) {
m_anchorOffset = anchorOffset;
}
bool Pane::hasDisplayed() const {
return m_hasDisplayed;
}
PanePtr Pane::createTooltip(Vec2I const&) {
return {};
}
Maybe<String> Pane::cursorOverride(Vec2I const&) {
return {};
}
}

View file

@ -0,0 +1,130 @@
#ifndef STAR_PANE_HPP
#define STAR_PANE_HPP
#include "StarWidget.hpp"
#include "StarBiMap.hpp"
namespace Star {
STAR_CLASS(Pane);
enum class PaneAnchor {
None,
BottomLeft,
BottomRight,
TopLeft,
TopRight,
CenterBottom,
CenterTop,
CenterLeft,
CenterRight,
Center
};
extern EnumMap<PaneAnchor> const PaneAnchorNames;
class Pane : public Widget {
public:
Pane();
struct BGResult {
String header;
String body;
String footer;
};
virtual void displayed();
virtual void dismissed();
void dismiss();
bool isDismissed() const;
bool isDisplayed() const;
Vec2I centerOffset() const;
// members are drawn strictly in the order they are added,
// so add them in the correct order.
virtual bool sendEvent(InputEvent const& event);
virtual void setFocus(Widget const* focus);
virtual void removeFocus(Widget const* focus);
virtual void removeFocus();
virtual void update();
virtual void tick();
bool dragActive() const;
Vec2I dragMouseOrigin() const;
void setDragActive(bool dragActive, Vec2I dragMouseOrigin);
void drag(Vec2I mousePosition);
bool inWindow(Vec2I const& position) const;
bool inDragArea(Vec2I const& position) const;
Vec2I cursorRelativeToPane(Vec2I const& position) const;
void setBG(BGResult const& res);
void setBG(String const& header, String const& body = "", String const& footer = "");
BGResult getBG() const;
void lockPosition();
void unlockPosition();
void setTitle(WidgetPtr icon, String const& title, String const& subTitle);
void setTitleString(String const& title, String const& subTitle);
void setTitleIcon(WidgetPtr icon);
String title() const;
String subTitle() const;
WidgetPtr titleIcon() const;
virtual Pane* window();
virtual Pane const* window() const;
PaneAnchor anchor();
void setAnchor(PaneAnchor anchor);
Vec2I anchorOffset() const;
void setAnchorOffset(Vec2I anchorOffset);
bool hasDisplayed() const;
// If a tooltip popup should be created at the given mouse position, return a
// new pane to be used as the tooltip.
virtual PanePtr createTooltip(Vec2I const& screenPosition);
virtual Maybe<String> cursorOverride(Vec2I const& screenPosition);
protected:
virtual void renderImpl();
String m_bgHeader;
String m_bgBody;
String m_bgFooter;
Vec2I m_footerSize;
Vec2I m_bodySize;
Vec2I m_headerSize;
bool m_dismissed;
bool m_dragActive;
Vec2I m_dragMouseOrigin;
bool m_lockPosition;
Vec2I m_centerOffset;
WidgetPtr m_mouseOver;
WidgetPtr m_clickDown;
WidgetPtr m_focusWidget;
WidgetPtr m_icon;
String m_title;
String m_subTitle;
unsigned m_fontSize;
Vec2I m_iconOffset;
Vec2I m_titleOffset;
Vec2I m_subTitleOffset;
Color m_titleColor;
Color m_subTitleColor;
PaneAnchor m_anchor;
Vec2I m_anchorOffset;
bool m_hasDisplayed;
};
}
#endif

View file

@ -0,0 +1,374 @@
#include "StarPaneManager.hpp"
#include "StarGameTypes.hpp"
#include "StarJsonExtra.hpp"
#include "StarAssets.hpp"
#include "StarRoot.hpp"
namespace Star {
PaneManager::PaneManager()
: m_context(GuiContext::singletonPtr()), m_prevInterfaceScale(1) {
auto assets = Root::singleton().assets();
m_tooltipMouseoverTime = assets->json("/panes.config:tooltipMouseoverTime").toFloat();
m_tooltipMouseoverRadius = assets->json("/panes.config:tooltipMouseoverRadius").toFloat();
m_tooltipMouseOffset = jsonToVec2I(assets->json("/panes.config:tooltipMouseoverOffset"));
m_tooltipShowTimer = m_tooltipMouseoverTime;
}
void PaneManager::displayPane(PaneLayer paneLayer, PanePtr const& pane, DismissCallback onDismiss) {
if (!m_displayedPanes[paneLayer].insertFront(move(pane), move(onDismiss)).second)
throw GuiException("Pane displayed twice in PaneManager::displayPane");
if (!pane->hasDisplayed() && pane->anchor() == PaneAnchor::None)
pane->setPosition(Vec2I((windowSize() - pane->size()) / 2) + pane->centerOffset()); // center it
pane->displayed();
}
bool PaneManager::isDisplayed(PanePtr const& pane) const {
for (auto const& layerPair : m_displayedPanes) {
if (layerPair.second.contains(pane))
return true;
}
return false;
}
void PaneManager::dismissPane(PanePtr const& pane) {
if (!dismiss(pane))
throw GuiException("No such pane in PaneManager::dismissPane");
}
void PaneManager::dismissAllPanes(Set<PaneLayer> const& paneLayers) {
for (auto const& paneLayer : paneLayers) {
for (auto const& panePair : copy(m_displayedPanes[paneLayer]))
dismiss(panePair.first);
}
}
void PaneManager::dismissAllPanes() {
for (auto layerPair : copy(m_displayedPanes)) {
for (auto const& panePair : layerPair.second)
dismiss(panePair.first);
}
}
PanePtr PaneManager::topPane(Set<PaneLayer> const& paneLayers) const {
for (auto const& layerPair : m_displayedPanes) {
if (paneLayers.contains(layerPair.first) && !layerPair.second.empty())
return layerPair.second.firstKey();
}
return {};
}
PanePtr PaneManager::topPane() const {
for (auto const& layerPair : m_displayedPanes) {
if (!layerPair.second.empty())
return layerPair.second.firstKey();
}
return {};
}
void PaneManager::bringToTop(PanePtr const& pane) {
for (auto& layerPair : m_displayedPanes) {
if (layerPair.second.contains(pane)) {
layerPair.second.toFront(pane);
return;
}
}
throw GuiException("Pane was not displayed in PaneManager::bringToTop");
}
void PaneManager::bringPaneAdjacent(PanePtr const& anchor, PanePtr const& adjacent, int gap) {
Vec2I centerAdjacent = anchor->position() + (anchor->size() / 2) - (adjacent->size() / 2);
centerAdjacent = centerAdjacent.piecewiseClamp(Vec2I(), windowSize() - adjacent->size()); // keeps pane inside window
if (anchor->position()[0] + anchor->size()[0] + gap + adjacent->size()[0] <= windowSize()[0])
adjacent->setPosition(Vec2I(anchor->position()[0] + anchor->size()[0] + gap, centerAdjacent[1])); // place to the right
else if (anchor->position()[0] - gap - adjacent->size()[0] >= 0)
adjacent->setPosition(Vec2I(anchor->position()[0] - gap - adjacent->size()[0], centerAdjacent[1])); // place to the left
else if (anchor->position()[1] + anchor->size()[1] + gap + adjacent->size()[1] <= windowSize()[1])
adjacent->setPosition(Vec2I(centerAdjacent[0], anchor->position()[1] + anchor->size()[1] + gap)); // place above
else if (anchor->position()[1] - gap - adjacent->size()[1] >= 0)
adjacent->setPosition(Vec2I(centerAdjacent[0], anchor->position()[1] - gap - adjacent->size()[1])); // place below
else
adjacent->setPosition(centerAdjacent);
bringToTop(adjacent);
}
PanePtr PaneManager::getPaneAt(Set<PaneLayer> const& paneLayers, Vec2I const& position) const {
for (auto const& layerPair : m_displayedPanes) {
if (!paneLayers.contains(layerPair.first))
continue;
for (auto const& panePair : layerPair.second) {
if (panePair.first->inWindow(position) && panePair.first->active())
return panePair.first;
}
}
return {};
}
PanePtr PaneManager::getPaneAt(Vec2I const& position) const {
for (auto const& layerPair : m_displayedPanes) {
for (auto const& panePair : layerPair.second) {
if (panePair.first->inWindow(position) && panePair.first->active())
return panePair.first;
}
}
return {};
}
void PaneManager::setBackgroundWidget(WidgetPtr bg) {
m_backgroundWidget = bg;
}
PanePtr PaneManager::keyboardCapturedPane() const {
for (auto const& layerPair : m_displayedPanes) {
for (auto const& panePair : layerPair.second) {
if (panePair.first->active() && panePair.first->keyboardCaptured() != KeyboardCaptureMode::None)
return panePair.first;
}
}
return {};
}
bool PaneManager::keyboardCapturedForTextInput() const {
if (auto pane = keyboardCapturedPane())
return pane->keyboardCaptured() == KeyboardCaptureMode::TextInput;
return false;
}
bool PaneManager::sendInputEvent(InputEvent const& event) {
if (event.is<MouseMoveEvent>()) {
m_tooltipLastMousePos = *m_context->mousePosition(event);
for (auto const& layerPair : m_displayedPanes) {
for (auto const& panePair : layerPair.second) {
if (panePair.first->dragActive()) {
panePair.first->drag(*m_context->mousePosition(event));
return true;
}
}
}
}
if (event.is<MouseButtonDownEvent>() || vmag(m_tooltipInitialPosition - m_tooltipLastMousePos) > m_tooltipMouseoverRadius) {
m_tooltipShowTimer = m_tooltipMouseoverTime;
if (m_activeTooltip) {
dismiss(m_activeTooltip);
m_activeTooltip.reset();
m_tooltipParentPane.reset();
m_tooltipShowTimer = m_tooltipMouseoverTime;
}
}
if (event.is<MouseButtonUpEvent>()) {
for (auto const& layerPair : m_displayedPanes) {
for (auto const& panePair : layerPair.second) {
if (panePair.first->dragActive()) {
panePair.first->setDragActive(false, {});
return true;
}
}
}
}
// The gui close event can only be intercepted by a pane that has captured
// the keyboard otherwise it will always be used to close first before being
// a normal event. This is so a window can control its own closing if it
// really needs to (like the keybindings window).
if (event.is<KeyDownEvent>() && m_context->actions(event).contains(InterfaceAction::GuiClose)) {
if (auto top = topPane({PaneLayer::ModalWindow, PaneLayer::Window})) {
dismiss(top);
return true;
}
}
// If there is a pane that has captured the keyboard, keyboard events will
// ONLY be sent to it.
auto keyCapturePane = keyboardCapturedPane();
if (keyCapturePane && (event.is<KeyDownEvent>() || event.is<KeyUpEvent>() || event.is<TextInputEvent>()))
return keyCapturePane->sendEvent(event);
bool foundModal = false;
for (auto& layerPair : m_displayedPanes) {
for (auto const& panePair : copy(layerPair.second)) {
if (panePair.first->sendEvent(event)) {
if (event.is<MouseButtonDownEvent>())
layerPair.second.toFront(panePair.first);
return true;
}
// If any modal windows are shown, Only the first modal window should
// have a chance to consume the input event and all other panes below it
// including different layers should ignore it.
if (layerPair.first == PaneLayer::ModalWindow) {
foundModal = true;
break;
}
}
if (foundModal)
break;
}
return false;
}
void PaneManager::render() {
if (m_backgroundWidget) {
auto size = m_backgroundWidget->size();
m_backgroundWidget->setPosition(Vec2I((windowSize()[0] - size[0]) / 2, (windowSize()[1] - size[1]) / 2));
m_backgroundWidget->render(RectI(Vec2I(), windowSize()));
}
for (auto const& layerPair : reverseIterate(m_displayedPanes)) {
for (auto const& panePair : reverseIterate(layerPair.second)) {
if (panePair.first->active()) {
if (m_prevInterfaceScale != m_context->interfaceScale())
panePair.first->setPosition(
calculateNewInterfacePosition(panePair.first, (float)m_context->interfaceScale() / m_prevInterfaceScale));
panePair.first->setDrawingOffset(calculatePaneOffset(panePair.first));
panePair.first->render(RectI(Vec2I(), windowSize()));
}
}
}
m_context->resetInterfaceScissorRect();
m_prevInterfaceScale = m_context->interfaceScale();
}
void PaneManager::update() {
m_tooltipShowTimer -= WorldTimestep;
if (m_tooltipShowTimer < 0 && !m_activeTooltip) {
if (auto parentPane = getPaneAt(m_tooltipLastMousePos)) {
if (auto tooltip = parentPane->createTooltip(m_tooltipLastMousePos)) {
m_activeTooltip = move(tooltip);
m_tooltipParentPane = move(parentPane);
m_tooltipInitialPosition = m_tooltipLastMousePos;
displayPane(PaneLayer::Tooltip, m_activeTooltip);
Vec2I offsetDirection = Vec2I::filled(1);
Vec2I offsetAdjust = Vec2I();
if (m_tooltipLastMousePos[0] + m_tooltipMouseOffset[0] + m_activeTooltip->size()[0] > (int)m_context->windowWidth() / m_context->interfaceScale()) {
offsetDirection[0] = -1;
offsetAdjust[0] = -m_activeTooltip->size()[0];
}
if (m_tooltipLastMousePos[1] + m_tooltipMouseOffset[1] - m_activeTooltip->size()[1] < 0)
offsetDirection[1] = -1;
else
offsetAdjust[1] = -m_activeTooltip->size()[1];
m_activeTooltip->setPosition(m_tooltipLastMousePos + (offsetAdjust + m_tooltipMouseOffset.piecewiseMultiply(offsetDirection)));
} else {
m_tooltipShowTimer = m_tooltipMouseoverTime;
}
}
} else if (m_activeTooltip && !m_tooltipParentPane->isDisplayed()) {
dismiss(m_activeTooltip);
m_activeTooltip.reset();
m_tooltipParentPane.reset();
}
for (auto const& layerPair : m_displayedPanes) {
for (auto const& panePair : copy(layerPair.second)) {
if (panePair.first->isDismissed())
dismiss(panePair.first);
}
}
for (auto const& layerPair : reverseIterate(m_displayedPanes)) {
for (auto const& panePair : reverseIterate(layerPair.second)) {
panePair.first->tick();
if (panePair.first->active())
panePair.first->update();
}
}
}
Vec2I PaneManager::windowSize() const {
return Vec2I(m_context->windowInterfaceSize());
}
Vec2I PaneManager::calculatePaneOffset(PanePtr const& pane) const {
Vec2I size = pane->size();
switch (pane->anchor()) {
case PaneAnchor::None:
return pane->anchorOffset();
case PaneAnchor::BottomLeft:
return pane->anchorOffset();
case PaneAnchor::BottomRight:
return pane->anchorOffset() + Vec2I{windowSize()[0] - size[0], 0};
case PaneAnchor::TopLeft:
return pane->anchorOffset() + Vec2I{0, windowSize()[1] - size[1]};
case PaneAnchor::TopRight:
return pane->anchorOffset() + (windowSize() - size);
case PaneAnchor::CenterTop:
return pane->anchorOffset() + Vec2I{(windowSize()[0] - size[0]) / 2, windowSize()[1] - size[1]};
case PaneAnchor::CenterBottom:
return pane->anchorOffset() + Vec2I{(windowSize()[0] - size[0]) / 2, 0};
case PaneAnchor::CenterLeft:
return pane->anchorOffset() + Vec2I{0, (windowSize()[1] - size[1]) / 2};
case PaneAnchor::CenterRight:
return pane->anchorOffset() + Vec2I{windowSize()[0] - size[0], (windowSize()[1] - size[1]) / 2};
case PaneAnchor::Center:
return pane->anchorOffset() + ((windowSize() - size) / 2);
default:
return pane->anchorOffset();
}
}
Vec2I PaneManager::calculateNewInterfacePosition(PanePtr const& pane, float interfaceScaleRatio) const {
Vec2F position(pane->relativePosition());
Vec2F size(pane->size());
Mat3F scale;
switch (pane->anchor()) {
case PaneAnchor::None:
scale = Mat3F::scaling(interfaceScaleRatio, Vec2F(windowSize()) / 2);
case PaneAnchor::BottomLeft:
scale = Mat3F::scaling(interfaceScaleRatio);
case PaneAnchor::BottomRight:
scale = Mat3F::scaling(interfaceScaleRatio, {size[0], 0});
case PaneAnchor::TopLeft:
scale = Mat3F::scaling(interfaceScaleRatio, {0, size[1]});
case PaneAnchor::TopRight:
scale = Mat3F::scaling(interfaceScaleRatio, size);
case PaneAnchor::CenterTop:
scale = Mat3F::scaling(interfaceScaleRatio, {size[0] / 2, size[1]});
case PaneAnchor::CenterBottom:
scale = Mat3F::scaling(interfaceScaleRatio, {size[0] / 2, 0});
case PaneAnchor::CenterLeft:
scale = Mat3F::scaling(interfaceScaleRatio, {0, size[1] / 2});
case PaneAnchor::CenterRight:
scale = Mat3F::scaling(interfaceScaleRatio, {size[0], size[1] / 2});
case PaneAnchor::Center:
scale = Mat3F::scaling(interfaceScaleRatio, size / 2);
default:
scale = Mat3F::scaling(interfaceScaleRatio, Vec2F(windowSize()) / 2);
}
return Vec2I::round((scale * Vec3F(position, 0)).vec2());
}
bool PaneManager::dismiss(PanePtr const& pane) {
bool dismissed = false;
for (auto& layerPair : m_displayedPanes) {
if (auto panePair = layerPair.second.maybeTake(pane)) {
dismissed = true;
panePair->first->dismissed();
if (panePair->second)
panePair->second(pane);
}
}
return dismissed;
}
}

View file

@ -0,0 +1,104 @@
#ifndef STAR_PANE_MANAGER_HPP
#define STAR_PANE_MANAGER_HPP
#include "StarPane.hpp"
#include "StarOrderedMap.hpp"
namespace Star {
STAR_CLASS(PaneManager);
enum class PaneLayer {
// A special class of window only meant to be used by PaneManager to display
// tooltips given by Pane::createTooltip
Tooltip,
// A special class of window that is displayed above all other windows and
// turns off input to other windows and the hud until it is dismissed.
ModalWindow,
// Window layer for regular windows that are regularly displayed and
// dismissed and dragged around.
Window,
// The bottom GUI layer, for persistent hud elements that are always or almost
// always shown. Not key dismissable.
Hud,
// Layer for interface elements which are logically part of the world but
// handled by GUI panes (such as wires)
World
};
// This class handles a set of panes to be drawn as a collective windowing
// interface. It is a set of panes on separate distinct layers, where each
// layer contains a z-ordered list of panes to display.
class PaneManager {
public:
typedef function<void(PanePtr const&)> DismissCallback;
PaneManager();
// Display a pane on any given layer. The pane lifetime in this class is
// only during display, once dismissed, the pane is forgotten completely.
void displayPane(PaneLayer paneLayer, PanePtr const& pane, DismissCallback onDismiss = {});
bool isDisplayed(PanePtr const& pane) const;
// Dismiss a given displayed pane. Pane must already be displayed.
void dismissPane(PanePtr const& pane);
// Dismisses all panes in the given layers.
void dismissAllPanes(Set<PaneLayer> const& paneLayers);
void dismissAllPanes();
PanePtr topPane(Set<PaneLayer> const& paneLayers) const;
PanePtr topPane() const;
// Brign an already displayed pane to the top of its layer.
void bringToTop(PanePtr const& pane);
// Position a pane adjacent to an anchor pane in a direction where
// it will fit on the screen
void bringPaneAdjacent(PanePtr const& anchor, PanePtr const& adjacent, int gap);
PanePtr getPaneAt(Set<PaneLayer> const& paneLayers, Vec2I const& position) const;
PanePtr getPaneAt(Vec2I const& position) const;
void setBackgroundWidget(WidgetPtr bg);
// Returns the pane that has captured the keyboard, if any.
PanePtr keyboardCapturedPane() const;
// Returns true if the current pane that has captured the keyboard is
// accepting text input.
bool keyboardCapturedForTextInput() const;
bool sendInputEvent(InputEvent const& event);
void render();
void update();
private:
Vec2I windowSize() const;
Vec2I calculatePaneOffset(PanePtr const& pane) const;
Vec2I calculateNewInterfacePosition(PanePtr const& pane, float interfaceScaleRatio) const;
bool dismiss(PanePtr const& pane);
GuiContext* m_context;
int m_prevInterfaceScale;
// Map of each pane layer, where the 0th pane is the topmost pane in each layer.
Map<PaneLayer, OrderedMap<PanePtr, DismissCallback>> m_displayedPanes;
WidgetPtr m_backgroundWidget;
float m_tooltipMouseoverTime;
float m_tooltipMouseoverRadius;
Vec2I m_tooltipMouseOffset;
float m_tooltipShowTimer;
Vec2I m_tooltipLastMousePos;
Vec2I m_tooltipInitialPosition;
PanePtr m_activeTooltip;
PanePtr m_tooltipParentPane;
};
}
#endif

View file

@ -0,0 +1,117 @@
#include "StarPortraitWidget.hpp"
#include "StarJsonExtra.hpp"
#include "StarRoot.hpp"
#include "StarImageMetadataDatabase.hpp"
#include "StarAssets.hpp"
namespace Star {
PortraitWidget::PortraitWidget(PortraitEntityPtr entity, PortraitMode mode) : m_entity(entity), m_portraitMode(mode) {
m_scale = 1;
m_iconMode = false;
init();
}
PortraitWidget::PortraitWidget() {
m_entity = {};
m_portraitMode = PortraitMode::Full;
m_scale = 1;
m_iconMode = false;
init();
}
RectI PortraitWidget::getScissorRect() const {
return noScissor();
}
void PortraitWidget::renderImpl() {
auto imgMetadata = Root::singleton().imageMetadataDatabase();
Vec2I offset = Vec2I();
if (m_iconMode) {
auto imgSize = Vec2F(imgMetadata->imageSize(m_iconImage));
offset = Vec2I(m_scale * imgSize / 2);
offset += m_iconOffset;
context()->drawInterfaceQuad(m_iconImage, Vec2F(screenPosition()), m_scale);
}
if (m_entity) {
List<Drawable> portrait = m_entity->portrait(m_portraitMode);
for (auto i : portrait) {
i.scale(m_scale);
context()->drawInterfaceDrawable(i, Vec2F(screenPosition() + offset));
}
} else {
if (m_portraitMode == PortraitMode::Bust || m_portraitMode == PortraitMode::Head) {
Vec2I pos = offset;
auto imgSize = Vec2F(imgMetadata->imageSize(m_noEntityImagePart));
pos -= Vec2I(m_scale * imgSize * 0.5);
context()->drawInterfaceQuad(m_noEntityImagePart, Vec2F(screenPosition() + pos), m_scale);
} else {
Vec2I pos = offset;
auto imgSize = Vec2F(imgMetadata->imageSize(m_noEntityImageFull));
pos -= Vec2I(m_scale * imgSize * 0.5);
context()->drawInterfaceQuad(m_noEntityImageFull, Vec2F(screenPosition() + pos), m_scale);
}
}
}
void PortraitWidget::init() {
auto assets = Root::singleton().assets();
m_noEntityImageFull = assets->json("/interface.config:portraitNullPlayerImageFull").toString();
m_noEntityImagePart = assets->json("/interface.config:portraitNullPlayerImagePart").toString();
m_iconImage = assets->json("/interface.config:portraitIconImage").toString();
m_iconOffset = jsonToVec2I(assets->json("/interface.config:portraitIconOffset"));
updateSize();
}
void PortraitWidget::setEntity(PortraitEntityPtr entity) {
m_entity = entity;
updateSize();
}
void PortraitWidget::setMode(PortraitMode mode) {
m_portraitMode = mode;
updateSize();
}
void PortraitWidget::setScale(float scale) {
m_scale = scale;
updateSize();
}
void PortraitWidget::setIconMode() {
m_iconMode = true;
updateSize();
}
bool PortraitWidget::sendEvent(InputEvent const&) {
return false;
}
void PortraitWidget::updateSize() {
auto imgMetadata = Root::singleton().imageMetadataDatabase();
if (m_iconMode) {
setSize(Vec2I(imgMetadata->imageSize(m_iconImage) * m_scale));
} else {
if (m_entity) {
setSize(Vec2I(
(Drawable::boundBoxAll(m_entity->portrait(m_portraitMode), false)
.size()
* TilePixels
* m_scale)
.ceil()));
} else {
if (m_portraitMode == PortraitMode::Bust || m_portraitMode == PortraitMode::Head)
setSize(Vec2I(imgMetadata->imageSize(m_iconImage) * m_scale));
else
setSize(Vec2I(imgMetadata->imageSize(m_noEntityImageFull) * m_scale));
}
}
}
}

View file

@ -0,0 +1,44 @@
#ifndef STAR_PORTRAIT_WIDGET_HPP
#define STAR_PORTRAIT_WIDGET_HPP
#include "StarWidget.hpp"
#include "StarPlayer.hpp"
namespace Star {
STAR_CLASS(Player);
STAR_CLASS(PortraitWidget);
class PortraitWidget : public Widget {
public:
PortraitWidget(PortraitEntityPtr entity, PortraitMode mode = PortraitMode::Full);
PortraitWidget();
virtual ~PortraitWidget() {}
void setEntity(PortraitEntityPtr entity);
void setMode(PortraitMode mode);
void setScale(float scale);
void setIconMode();
bool sendEvent(InputEvent const& event);
protected:
virtual RectI getScissorRect() const;
virtual void renderImpl();
private:
void init();
void updateSize();
PortraitEntityPtr m_entity;
PortraitMode m_portraitMode;
String m_noEntityImageFull;
String m_noEntityImagePart;
float m_scale;
bool m_iconMode;
String m_iconImage;
Vec2I m_iconOffset;
};
}
#endif

View file

@ -0,0 +1,71 @@
#include "StarProgressWidget.hpp"
#include "StarLexicalCast.hpp"
#include "StarInterpolation.hpp"
namespace Star {
ProgressWidget::ProgressWidget(String const& background,
String const& overlay,
ImageStretchSet const& progressSet,
GuiDirection direction)
: m_background(background),
m_overlay(overlay),
m_bar(progressSet),
m_direction(direction) {
m_progressLevel = 0;
m_maxLevel = 1;
if (!m_background.empty())
setSize(Vec2I(context()->textureSize(m_background)));
else if (!m_overlay.empty())
setSize(Vec2I(context()->textureSize(m_overlay)));
m_color = Color::White;
}
void ProgressWidget::renderImpl() {
float progress = 1;
if (m_maxLevel > 0)
progress = m_progressLevel / m_maxLevel;
auto shift = [&](float begin, float end, RectF templ) {
RectF result = templ;
if (m_direction == GuiDirection::Horizontal) {
result.min()[0] = lerp(begin, templ.min()[0], templ.max()[0]);
result.max()[0] = lerp(end, templ.min()[0], templ.max()[0]);
} else {
result.min()[1] = lerp(begin, templ.min()[1], templ.max()[1]);
result.max()[1] = lerp(end, templ.min()[1], templ.max()[1]);
}
return result;
};
if (!m_background.empty())
context()->drawInterfaceQuad(m_background, shift(0, 1, RectF(Vec2F(), Vec2F(size()))), shift(0, 1, RectF(screenBoundRect())));
context()->drawImageStretchSet(m_bar, shift(0, progress, RectF(screenBoundRect())), m_direction, m_color.toRgba());
if (!m_overlay.empty())
context()->drawInterfaceQuad(m_overlay, shift(0, 1, RectF({}, Vec2F(size()))), shift(0, 1, RectF(screenBoundRect())));
}
void ProgressWidget::setCurrentProgressLevel(float amount) {
m_progressLevel = amount;
}
void ProgressWidget::setMaxProgressLevel(float amount) {
m_maxLevel = amount;
}
void ProgressWidget::setColor(Color const& color) {
m_color = color;
}
void ProgressWidget::setOverlay(String const& overlay) {
m_overlay = overlay;
}
}

View file

@ -0,0 +1,42 @@
#ifndef STAR_PROGRESS_WIDGET_HPP
#define STAR_PROGRESS_WIDGET_HPP
#include "StarWidget.hpp"
namespace Star {
STAR_CLASS(ProgressWidget);
class ProgressWidget : public Widget {
public:
ProgressWidget(String const& background,
String const& overlay,
ImageStretchSet const& progressSet,
GuiDirection direction);
virtual ~ProgressWidget() {}
void setCurrentProgressLevel(float amount);
void setMaxProgressLevel(float amount);
void setColor(Color const& color);
void setOverlay(String const& overlay);
protected:
virtual void renderImpl();
RectI shift(float begin, float end, RectI templ);
float m_progressLevel;
float m_maxLevel;
Color m_color;
String m_background;
String m_overlay;
ImageStretchSet m_bar;
GuiDirection m_direction;
private:
};
}
#endif

View file

@ -0,0 +1,126 @@
#ifndef STAR_REGISTERED_PANE_MANAGER_HPP
#define STAR_REGISTERED_PANE_MANAGER_HPP
#include "StarPaneManager.hpp"
namespace Star {
// This class inherits PaneManager to allow for registered panes that are kept
// internally by the class even when dismissed. They can be displayed,
// dismissed, and toggled between the two without being lost.
template <typename KeyT>
class RegisteredPaneManager : public PaneManager {
public:
typedef KeyT Key;
void registerPane(KeyT paneId, PaneLayer paneLayer, PanePtr pane, DismissCallback onDismiss = {});
PanePtr deregisterPane(KeyT const& paneId);
void deregisterAllPanes();
template <typename T = Pane>
shared_ptr<T> registeredPane(KeyT const& paneId) const;
// Displays a registred pane if it is not already displayed. Returns true
// if it is newly displayed.
bool displayRegisteredPane(KeyT const& paneId);
bool registeredPaneIsDisplayed(KeyT const& paneId) const;
// Dismisses a registred pane if it is displayed. Returns true if it
// has been dismissed.
bool dismissRegisteredPane(KeyT const& paneId);
// Returns whether the pane is now displayed.
bool toggleRegisteredPane(KeyT const& paneId);
private:
struct PaneInfo {
PaneLayer layer;
PanePtr pane;
DismissCallback dismissCallback;
};
PaneInfo const& getRegisteredPaneInfo(KeyT const& paneId) const;
// Map of registered panes by name.
HashMap<KeyT, PaneInfo> m_registeredPanes;
};
template <typename KeyT>
template <typename T>
shared_ptr<T> RegisteredPaneManager<KeyT>::registeredPane(KeyT const& paneId) const {
if (auto v = m_registeredPanes.ptr(paneId))
return convert<T>(v->pane);
throw GuiException(strf("No pane named '%s' found in RegisteredPaneManager", outputAny(paneId)));
}
template <typename KeyT>
void RegisteredPaneManager<KeyT>::registerPane(
KeyT paneId, PaneLayer paneLayer, PanePtr pane, DismissCallback onDismiss) {
if (!m_registeredPanes.insert(move(paneId), {move(paneLayer), move(pane), move(onDismiss)}).second)
throw GuiException(
strf("Registered pane with name '%s' registered a second time in RegisteredPaneManager::registerPane",
outputAny(paneId)));
}
template <typename KeyT>
PanePtr RegisteredPaneManager<KeyT>::deregisterPane(KeyT const& paneId) {
if (auto v = m_registeredPanes.maybeTake(paneId)) {
if (isDisplayed(v->pane))
dismissPane(v->pane);
return v->pane;
}
throw GuiException(strf("No pane named '%s' found in RegisteredPaneManager::deregisterPane", outputAny(paneId)));
}
template <typename KeyT>
void RegisteredPaneManager<KeyT>::deregisterAllPanes() {
for (auto const& k : m_registeredPanes.keys())
deregisterPane(k);
}
template <typename KeyT>
bool RegisteredPaneManager<KeyT>::displayRegisteredPane(KeyT const& paneId) {
auto const& paneInfo = getRegisteredPaneInfo(paneId);
if (!isDisplayed(paneInfo.pane)) {
displayPane(paneInfo.layer, paneInfo.pane, paneInfo.dismissCallback);
return true;
}
return false;
}
template <typename KeyT>
bool RegisteredPaneManager<KeyT>::registeredPaneIsDisplayed(KeyT const& paneId) const {
return isDisplayed(getRegisteredPaneInfo(paneId).pane);
}
template <typename KeyT>
bool RegisteredPaneManager<KeyT>::dismissRegisteredPane(KeyT const& paneId) {
auto const& paneInfo = getRegisteredPaneInfo(paneId);
if (isDisplayed(paneInfo.pane)) {
dismissPane(paneInfo.pane);
return true;
}
return false;
}
template <typename KeyT>
bool RegisteredPaneManager<KeyT>::toggleRegisteredPane(KeyT const& paneId) {
if (registeredPaneIsDisplayed(paneId)) {
dismissRegisteredPane(paneId);
return false;
} else {
displayRegisteredPane(paneId);
return true;
}
}
template <typename KeyT>
typename RegisteredPaneManager<KeyT>::PaneInfo const& RegisteredPaneManager<KeyT>::getRegisteredPaneInfo(
KeyT const& paneId) const {
if (auto p = m_registeredPanes.ptr(paneId))
return *p;
throw GuiException(strf("No registered pane with name '%s' found in RegisteredPaneManager", outputAny(paneId)));
}
}
#endif

View file

@ -0,0 +1,445 @@
#include "StarScrollArea.hpp"
#include "StarRoot.hpp"
#include "StarAssets.hpp"
namespace Star {
// TODO: I hate these hardcoded values. Please smite with fire.
static int const ScrollAreaBorder = 9;
static int const ScrollButtonStackSize = 6;
static int const ScrollThumbSize = 3;
static int const ScrollThumbOverhead = ScrollThumbSize + ScrollThumbSize;
static int const ScrollBarTrackOverhead = ScrollButtonStackSize + ScrollButtonStackSize + ScrollThumbOverhead;
static int64_t const ScrollAdvanceTimer = 100;
ScrollThumb::ScrollThumb(GuiDirection direction) {
m_hovered = false;
m_pressed = false;
m_direction = direction;
auto assets = Root::singleton().assets();
setImages(assets->json("/interface.config:scrollArea.thumbs"));
}
void ScrollThumb::setImages(ImageStretchSet const& base, ImageStretchSet const& hover, ImageStretchSet const& pressed) {
m_baseThumb = base;
m_hoverThumb = hover;
m_pressedThumb = pressed;
}
void ScrollThumb::setImages(Json const& images) {
String directionString;
if (m_direction == GuiDirection::Vertical) {
directionString = "vertical";
} else {
directionString = "horizontal";
}
m_baseThumb.begin = images.get(directionString).get("base").getString("begin");
m_baseThumb.end = images.get(directionString).get("base").getString("end");
m_baseThumb.inner = images.get(directionString).get("base").getString("inner");
m_hoverThumb.begin = images.get(directionString).get("hover").getString("begin");
m_hoverThumb.end = images.get(directionString).get("hover").getString("end");
m_hoverThumb.inner = images.get(directionString).get("hover").getString("inner");
m_pressedThumb.begin = images.get(directionString).get("pressed").getString("begin");
m_pressedThumb.end = images.get(directionString).get("pressed").getString("end");
m_pressedThumb.inner = images.get(directionString).get("pressed").getString("inner");
}
bool ScrollThumb::isHovered() const {
return m_hovered;
}
bool ScrollThumb::isPressed() const {
return m_pressed;
}
void ScrollThumb::setHovered(bool hovered) {
m_hovered = hovered;
}
void ScrollThumb::setPressed(bool pressed) {
m_pressed = pressed;
}
void ScrollThumb::mouseOver() {
setHovered(true);
}
void ScrollThumb::mouseOut() {
setHovered(false);
}
void ScrollThumb::renderImpl() {
ImageStretchSet workingSet = m_baseThumb;
if (isHovered())
workingSet = m_hoverThumb;
if (isPressed())
workingSet = m_pressedThumb;
if (workingSet.fullyPopulated()) {
context()->drawImageStretchSet(workingSet,
RectF::withSize(Vec2F(m_parent->screenPosition() + position()), Vec2F(size())),
m_direction);
}
}
Vec2U ScrollThumb::baseSize() const {
return context()->textureSize(m_baseThumb.begin);
}
ScrollBar::ScrollBar(GuiDirection direction, WidgetCallbackFunc forwardFunc, WidgetCallbackFunc backwardFunc)
: m_direction(direction) {
m_forward = make_shared<ButtonWidget>();
m_forward->setCallback(forwardFunc);
m_forward->setSustainCallbackOnDownHold(true);
m_forward->setPressedOffset({0, 0});
m_backward = make_shared<ButtonWidget>();
m_backward->setCallback(backwardFunc);
m_backward->setSustainCallbackOnDownHold(true);
m_backward->setPressedOffset({0, 0});
m_thumb = make_shared<ScrollThumb>(m_direction);
auto assets = Root::singleton().assets();
setButtonImages(assets->json("/interface.config:scrollArea.buttons"));
addChild("thumb", m_thumb);
addChild("forward", m_forward);
addChild("backward", m_backward);
}
void ScrollBar::setButtonImages(Json const& images) {
String directionString;
if (m_direction == GuiDirection::Vertical) {
directionString = "vertical";
} else {
directionString = "horizontal";
}
m_forward->setImages(images.get(directionString).get("forward").getString("base"),
images.get(directionString).get("forward").getString("hover"),
images.get(directionString).get("forward").getString("pressed"));
m_backward->setImages(images.get(directionString).get("backward").getString("base"),
images.get(directionString).get("backward").getString("hover"),
images.get(directionString).get("backward").getString("pressed"));
}
Vec2I ScrollBar::size() const {
if (m_parent)
return m_parent->size();
return {};
}
int ScrollBar::trackSize() const {
if (m_parent) {
auto scrollArea = convert<ScrollArea>(m_parent);
auto size = scrollArea->size();
if (m_direction == GuiDirection::Vertical) {
if (scrollArea->horizontalScroll()) {
return size[1] - (ScrollBarTrackOverhead + ScrollAreaBorder);
} else {
return size[1] - ScrollBarTrackOverhead;
}
} else {
return size[0] - ScrollBarTrackOverhead;
}
}
throw GuiException("Somehow have a Scroll Bar without a parent.");
}
float ScrollBar::sizeRatio() const {
if (m_parent) {
auto scrollArea = convert<ScrollArea>(m_parent);
if (m_direction == GuiDirection::Vertical) {
return scrollArea->contentSize()[1] / (float)(scrollArea->areaSize()[1]);
} else {
return scrollArea->contentSize()[0] / (float)(scrollArea->areaSize()[0]);
}
}
throw GuiException("Somehow have a Scroll Bar without a parent.");
}
float ScrollBar::scrollRatio() const {
if (m_parent) {
auto scrollArea = convert<ScrollArea>(m_parent);
if (m_direction == GuiDirection::Vertical) {
if (scrollArea->maxScrollPosition()[1] == 0)
return 0;
return scrollArea->scrollOffset()[1] / (float)(scrollArea->maxScrollPosition()[1]);
} else {
if (scrollArea->maxScrollPosition()[0] == 0)
return 0;
return scrollArea->scrollOffset()[0] / (float)(scrollArea->maxScrollPosition()[0]);
}
}
return 0;
}
ButtonWidgetPtr ScrollBar::forwardButton() const {
return m_forward;
}
ButtonWidgetPtr ScrollBar::backwardButton() const {
return m_backward;
}
ScrollThumbPtr ScrollBar::thumb() const {
return m_thumb;
}
void ScrollBar::drawChildren() {
if (m_parent) {
auto scrollArea = convert<ScrollArea>(m_parent);
float ratio = sizeRatio();
if (ratio < 1)
ratio = 1;
int innerSize = (int)max(0.0f, ceil(trackSize() / ratio));
int offsetBegin = (int)ceil((trackSize() - innerSize) * scrollRatio());
innerSize += ScrollThumbOverhead;
if (m_direction == GuiDirection::Vertical) {
if (scrollArea->horizontalScroll()) {
m_forward->setPosition(m_parent->size() - Vec2I(ScrollAreaBorder, ScrollButtonStackSize));
m_backward->setPosition({m_parent->size()[0] - ScrollAreaBorder, ScrollAreaBorder});
m_thumb->setPosition({m_parent->size()[0] - ScrollAreaBorder, ScrollAreaBorder + ScrollButtonStackSize + offsetBegin});
} else {
m_forward->setPosition(m_parent->size() - Vec2I(ScrollAreaBorder, ScrollButtonStackSize));
m_backward->setPosition({m_parent->size()[0] - ScrollAreaBorder, 0});
m_thumb->setPosition({m_parent->size()[0] - ScrollAreaBorder, ScrollButtonStackSize + offsetBegin});
}
m_thumb->setSize(Vec2I(m_thumb->baseSize()[0], innerSize));
} else {
m_forward->setPosition({m_parent->size()[0] - ScrollButtonStackSize, 0});
m_backward->setPosition({0, 0});
m_thumb->setPosition({ScrollButtonStackSize + offsetBegin, 0});
m_thumb->setSize(Vec2I(innerSize, m_thumb->baseSize()[1]));
}
for (auto child : m_members) {
child->render(m_drawingArea);
}
}
}
Vec2I ScrollBar::offsetFromThumbPosition(Vec2I const& thumbPosition) const {
auto scrollArea = convert<ScrollArea>(m_parent);
if (m_direction == GuiDirection::Vertical) {
int scrollSpan = trackSize() - m_thumb->size()[1];
int scrollOffset = clamp((thumbPosition[1] - ScrollButtonStackSize), 0, scrollSpan);
float scrollRatio = (float)scrollOffset / (float)scrollSpan;
return {scrollArea->scrollOffset()[0], ceil(scrollArea->maxScrollPosition()[1] * scrollRatio)};
} else {
int scrollSpan = trackSize() - m_thumb->size()[0];
int scrollOffset = clamp((thumbPosition[0] - ScrollButtonStackSize), 0, scrollSpan);
float scrollRatio = (float)scrollOffset / (float)scrollSpan;
return {(int)ceil(scrollArea->maxScrollPosition()[0] * scrollRatio), scrollArea->scrollOffset()[1]};
}
}
ScrollArea::ScrollArea() {
auto assets = Root::singleton().assets();
m_buttonAdvance = assets->json("/interface.config:scrollArea.buttonAdvance").toInt();
m_advanceLimiter = Time::monotonicMilliseconds();
WidgetCallbackFunc vAdvance = [this](Widget*) { scrollAreaBy({0, advanceFactorHelper()}); };
WidgetCallbackFunc vRetreat = [this](Widget*) { scrollAreaBy({0, -advanceFactorHelper()}); };
WidgetCallbackFunc hAdvance = [this](Widget*) { scrollAreaBy({advanceFactorHelper(), 0}); };
WidgetCallbackFunc hRetreat = [this](Widget*) { scrollAreaBy({-advanceFactorHelper(), 0}); };
m_vBar = make_shared<ScrollBar>(GuiDirection::Vertical, vAdvance, vRetreat);
m_hBar = make_shared<ScrollBar>(GuiDirection::Horizontal, hAdvance, hRetreat);
m_dragActive = false;
addChild("vScrollBar", m_vBar);
addChild("hScrollBar", m_hBar);
m_horizontalScroll = false;
m_verticalScroll = true;
}
void ScrollArea::setButtonImages(Json const& images) {
m_vBar->setButtonImages(images);
m_hBar->setButtonImages(images);
}
void ScrollArea::setThumbImages(Json const& images) {
m_vBar->thumb()->setImages(images);
m_hBar->thumb()->setImages(images);
}
RectI ScrollArea::contentBoundRect() const {
RectI res = RectI::null();
for (auto child : m_members) {
if (child == m_vBar || child == m_hBar) // scroll bars don't count
continue;
if (!child->active()) // neither do hidden members
continue;
res.combine(child->relativeBoundRect());
}
res.setMax({res.max()[0], res.max()[1] + 1});
return res;
}
Vec2I ScrollArea::contentSize() const {
return contentBoundRect().size();
}
Vec2I ScrollArea::areaSize() const {
Vec2I s = size();
if (horizontalScroll())
s[1] -= ScrollAreaBorder;
if (verticalScroll())
s[0] -= ScrollAreaBorder;
return s;
}
void ScrollArea::scrollAreaBy(Vec2I const& offset) {
m_scrollOffset += offset;
}
Vec2I ScrollArea::scrollOffset() const {
return m_scrollOffset;
}
bool ScrollArea::horizontalScroll() const {
return m_horizontalScroll;
}
void ScrollArea::setHorizontalScroll(bool horizontal) {
m_horizontalScroll = horizontal;
}
bool ScrollArea::verticalScroll() const {
return m_verticalScroll;
}
void ScrollArea::setVerticalScroll(bool vertical) {
m_verticalScroll = vertical;
}
bool ScrollArea::sendEvent(InputEvent const& event) {
if (!m_visible)
return false;
if (m_dragActive) {
if (event.is<MouseButtonUpEvent>()) {
blur();
m_dragActive = false;
m_vBar->thumb()->setPressed(false);
m_hBar->thumb()->setPressed(false);
return true;
} else if (event.is<MouseMoveEvent>()) {
auto thumbDragPosition = *context()->mousePosition(event) - screenPosition() - m_dragOffset;
if (m_dragDirection == GuiDirection::Vertical) {
m_scrollOffset = m_vBar->offsetFromThumbPosition(thumbDragPosition);
} else {
m_scrollOffset = m_hBar->offsetFromThumbPosition(thumbDragPosition);
}
return true;
}
}
auto mousePos = context()->mousePosition(event);
if (mousePos && !inMember(*mousePos))
return false;
if (event.is<MouseButtonDownEvent>()) {
if (m_vBar->thumb()->inMember(*mousePos)) {
focus();
m_dragOffset = *mousePos - screenPosition() - m_vBar->thumb()->position();
m_dragDirection = GuiDirection::Vertical;
m_dragActive = true;
m_vBar->thumb()->setPressed(true);
return true;
} else if (m_hBar->thumb()->inMember(*mousePos)) {
focus();
m_dragOffset = *mousePos - screenPosition() - m_hBar->thumb()->position();
m_dragDirection = GuiDirection::Horizontal;
m_dragActive = true;
m_hBar->thumb()->setPressed(true);
return true;
}
}
if (Widget::sendEvent(event))
return true;
if (auto mouseWheel = event.ptr<MouseWheelEvent>()) {
if (mouseWheel->mouseWheel == MouseWheel::Up)
scrollAreaBy({0, m_buttonAdvance * 3});
else
scrollAreaBy({0, -m_buttonAdvance * 3});
return true;
}
return true;
}
void ScrollArea::update() {
if (!m_visible)
return;
auto maxScroll = maxScrollPosition();
// keep vertical scroll bars same distance from the *top* on resize
if (m_verticalScroll && maxScroll != m_lastMaxScroll)
m_scrollOffset += (maxScroll - m_lastMaxScroll);
m_scrollOffset = m_scrollOffset.piecewiseClamp(Vec2I(), maxScroll);
m_lastMaxScroll = maxScroll;
}
Vec2I ScrollArea::maxScrollPosition() const {
return Vec2I().piecewiseMax(contentSize() - size());
}
void ScrollArea::drawChildren() {
RectI innerDrawingArea = m_drawingArea;
if (horizontalScroll())
innerDrawingArea.setYMin(ScrollAreaBorder);
if (verticalScroll())
innerDrawingArea.setXMax(innerDrawingArea.xMax() - ScrollAreaBorder);
auto contentBoundRect = ScrollArea::contentBoundRect();
auto contentOffset = contentBoundRect.min();
auto contentSize = contentBoundRect.size();
auto offset = contentOffset + scrollOffset();
auto areaSize = ScrollArea::areaSize();
if (contentSize[1] < areaSize[1])
offset[1] = offset[1] - (areaSize[1] - contentSize[1]);
for (auto child : m_members) {
if (child == m_vBar || child == m_hBar)
continue;
child->setDrawingOffset(-offset);
child->render(innerDrawingArea);
}
if (m_horizontalScroll)
m_hBar->render(m_drawingArea);
if (m_verticalScroll)
m_vBar->render(m_drawingArea);
}
int ScrollArea::advanceFactorHelper() {
auto t = Time::monotonicMilliseconds() - m_advanceLimiter;
m_advanceLimiter = Time::monotonicMilliseconds();
if ((t > ScrollAdvanceTimer) || (t < 0))
t = ScrollAdvanceTimer;
return (int)std::ceil((m_buttonAdvance * t) / (float)ScrollAdvanceTimer);
}
}

View file

@ -0,0 +1,132 @@
#ifndef _STAR_SCROLL_AREA_HPP_
#define _STAR_SCROLL_AREA_HPP_
#include "StarWidget.hpp"
#include "StarButtonWidget.hpp"
#include "StarImageWidget.hpp"
namespace Star {
class ScrollThumb : public Widget {
public:
ScrollThumb(GuiDirection direction);
virtual ~ScrollThumb() {}
void setDirection(GuiDirection direction);
void setImages(ImageStretchSet const& base,
ImageStretchSet const& hover = ImageStretchSet(),
ImageStretchSet const& pressed = ImageStretchSet());
void setImages(Json const& images);
bool isHovered() const;
bool isPressed() const;
void setHovered(bool hovered);
void setPressed(bool pressed);
void mouseOver() override;
void mouseOut() override;
Vec2U baseSize() const;
protected:
virtual void renderImpl() override;
private:
void readDefaults();
GuiDirection m_direction;
ImageStretchSet m_baseThumb;
ImageStretchSet m_hoverThumb;
ImageStretchSet m_pressedThumb;
bool m_hovered;
bool m_pressed;
};
typedef shared_ptr<ScrollThumb> ScrollThumbPtr;
class ScrollBar : public Widget {
public:
ScrollBar(GuiDirection direction, WidgetCallbackFunc forwardFunc, WidgetCallbackFunc backwardFunc);
void setButtonImages(Json const& images);
int trackSize() const;
float sizeRatio() const;
virtual Vec2I size() const override;
float scrollRatio() const;
Vec2I offsetFromThumbPosition(Vec2I const& thumbPosition) const;
ButtonWidgetPtr forwardButton() const;
ButtonWidgetPtr backwardButton() const;
ScrollThumbPtr thumb() const;
protected:
void drawChildren() override;
private:
GuiDirection m_direction;
ButtonWidgetPtr m_forward; // up or right, makes the offset higher
ButtonWidgetPtr m_backward; // down or left, makes the offset lower
ScrollThumbPtr m_thumb;
ImageStretchSet m_track;
};
typedef shared_ptr<ScrollBar> ScrollBarPtr;
class ScrollArea : public Widget {
public:
ScrollArea();
void setButtonImages(Json const& images);
void setThumbImages(Json const& images);
RectI contentBoundRect() const;
Vec2I contentSize() const;
Vec2I areaSize() const;
void scrollAreaBy(Vec2I const& offset);
Vec2I scrollOffset() const;
Vec2I maxScrollPosition() const;
bool horizontalScroll() const;
void setHorizontalScroll(bool horizontal);
bool verticalScroll() const;
void setVerticalScroll(bool vertical);
virtual bool sendEvent(InputEvent const& event) override;
virtual void update() override;
protected:
void drawChildren() override;
private:
int advanceFactorHelper();
int m_buttonAdvance;
int64_t m_advanceLimiter;
Vec2I m_scrollOffset;
Vec2I m_lastMaxScroll;
Vec2I m_contentSize;
bool m_dragActive;
GuiDirection m_dragDirection;
Vec2I m_dragOffset;
ScrollBarPtr m_vBar;
ScrollBarPtr m_hBar;
ImageWidgetPtr m_cornerBlock;
bool m_horizontalScroll;
bool m_verticalScroll;
};
typedef shared_ptr<ScrollArea> ScrollAreaPtr;
}
#endif

View file

@ -0,0 +1,183 @@
#include "StarSliderBar.hpp"
#include "StarRoot.hpp"
#include "StarAssets.hpp"
#include "StarGuiReader.hpp"
#include "StarImageMetadataDatabase.hpp"
namespace Star {
SliderBarWidget::SliderBarWidget(String const& grid, bool showSpinner)
: m_grid(make_shared<ImageWidget>(grid)),
m_low(0),
m_high(1),
m_delta(1),
m_val(0),
m_updateJog(true),
m_jogDragActive(false),
m_enabled(true) {
auto assets = Root::singleton().assets();
auto imgMetadata = Root::singleton().imageMetadataDatabase();
m_jog = make_shared<ButtonWidget>();
m_jog->setImages(assets->json("/interface.config:slider.jog").toString());
m_jog->setPressedOffset({0, 0});
if (showSpinner) {
GuiReader guiReader;
guiReader.registerCallback("spinner.down", bind(&SliderBarWidget::leftCallback, this));
guiReader.registerCallback("spinner.up", bind(&SliderBarWidget::rightCallback, this));
float rightButtonOffset = imgMetadata->imageSize(assets->json("/interface.config:slider.leftBase").toString())[0];
rightButtonOffset += assets->json("/interface.config:slider.defaultPadding").toInt();
float gridOffset = rightButtonOffset;
rightButtonOffset += imgMetadata->imageSize(grid)[0];
rightButtonOffset += assets->json("/interface.config:slider.defaultPadding").toInt();
// TODO: Make spinners a thing already
Json config = Json::parse(strf(
R"SPINNER(
{
"spinner" : {
"type" : "spinner",
"leftBase" : "%s",
"leftHover" : "%s",
"rightBase" : "%s",
"rightHover" : "%s",
"position" : [0, 0],
"upOffset" : %s
}
}
)SPINNER",
assets->json("/interface.config:slider.leftBase").toString(),
assets->json("/interface.config:slider.leftHover").toString(),
assets->json("/interface.config:slider.rightBase").toString(),
assets->json("/interface.config:slider.rightHover").toString(),
rightButtonOffset));
guiReader.construct(config, this);
m_grid->setPosition(Vec2I(gridOffset, 0));
m_leftButton = fetchChild<ButtonWidget>("spinner.down");
m_rightButton = fetchChild<ButtonWidget>("spinner.up");
}
addChild("grid", m_grid);
addChild("jog", m_jog);
markAsContainer();
disableScissoring();
}
void SliderBarWidget::setJogImages(String const& baseImage, String const& hoverImage, String const& pressedImage, String const& disabledImage) {
m_jog->setImages(baseImage, hoverImage, pressedImage, disabledImage);
}
void SliderBarWidget::setRange(int low, int high, int delta) {
m_low = low;
m_high = high;
m_high = std::max(m_low + 1, m_high);
if (delta <= 0)
m_delta = 1;
else
m_delta = delta;
setVal(m_val);
}
void SliderBarWidget::setRange(Vec2I const& range, int delta) {
setRange(range[0], range[1], delta);
}
void SliderBarWidget::setVal(int val, bool callbackIfChanged) {
bool doCallback = false;
if (m_callback && callbackIfChanged && m_val != val)
doCallback = true;
if (m_val != val)
m_updateJog = true;
m_val = val;
m_val = clamp(m_val, m_low, m_high);
if (doCallback)
m_callback(this);
}
int SliderBarWidget::val() const {
return m_val;
}
void SliderBarWidget::setEnabled(bool enabled) {
if (m_enabled != enabled) {
m_enabled = enabled;
m_jogDragActive = false;
m_jog->setEnabled(enabled);
if (m_rightButton)
m_rightButton->setEnabled(enabled);
if (m_leftButton)
m_leftButton->setEnabled(enabled);
}
}
void SliderBarWidget::setCallback(WidgetCallbackFunc callback) {
m_callback = callback;
}
void SliderBarWidget::update() {
float gridLow = m_grid->position()[0];
float gridHigh = gridLow + m_grid->size()[0];
if (m_jogDragActive) {
auto clampedPos = clamp<float>(m_jogDragPos[0], gridLow, gridHigh);
float clampedPercent = (clampedPos - gridLow) / (gridHigh - gridLow);
setVal(clampedPercent * (m_high - m_low));
}
if (m_updateJog) {
float percent = (float)m_val / (m_high - m_low);
float jogPos = (gridHigh - gridLow) * percent + gridLow - m_jog->size()[0] * 0.5;
m_jog->setPosition(Vec2I(jogPos, 0));
m_updateJog = false;
}
Widget::update();
}
bool SliderBarWidget::sendEvent(InputEvent const& event) {
if (event.is<MouseButtonUpEvent>()) {
blur();
m_jogDragActive = false;
}
if (m_enabled) {
if (event.is<MouseButtonDownEvent>()) {
if (m_jog->inMember(*context()->mousePosition(event))) {
focus();
m_jogDragPos = *context()->mousePosition(event) - screenPosition();
m_jogDragActive = true;
}
}
if (event.is<MouseMoveEvent>()) {
if (m_jogDragActive)
m_jogDragPos = *context()->mousePosition(event) - screenPosition();
}
}
return Widget::sendEvent(event);
}
void SliderBarWidget::leftCallback() {
setVal(std::max(m_low, m_val - m_delta));
}
void SliderBarWidget::rightCallback() {
setVal(std::min(m_high, m_val + m_delta));
}
}

View file

@ -0,0 +1,57 @@
#ifndef STAR_SLIDER_BAR_HPP
#define STAR_SLIDER_BAR_HPP
#include "StarWidget.hpp"
#include "StarButtonWidget.hpp"
#include "StarImageWidget.hpp"
namespace Star {
// This class only does left right right now. If you need advanced
// multiorientation physics please
// implement it in.
class SliderBarWidget : public Widget {
public:
SliderBarWidget(String const& grid, bool showSpinner = true);
void setJogImages(String const& baseImage, String const& hoverImage = "", String const& pressedImage = "", String const& disabledImage = "");
void setRange(int low, int high, int delta);
void setRange(Vec2I const& range, int delta);
void setVal(int val, bool callbackIfChanged = true);
int val() const;
void setEnabled(bool enabled);
void setCallback(WidgetCallbackFunc callback);
virtual void update() override;
virtual bool sendEvent(InputEvent const& event) override;
private:
void leftCallback();
void rightCallback();
ButtonWidgetPtr m_leftButton;
ButtonWidgetPtr m_rightButton;
ImageWidgetPtr m_grid;
ButtonWidgetPtr m_jog;
int m_low;
int m_high;
int m_delta;
int m_val;
bool m_updateJog;
Vec2I m_savedJogPos;
Vec2I m_jogDragPos;
bool m_jogDragActive;
bool m_enabled;
WidgetCallbackFunc m_callback;
};
typedef shared_ptr<SliderBarWidget> SliderBarWidgetPtr;
}
#endif

View file

@ -0,0 +1,35 @@
#include "StarStackWidget.hpp"
namespace Star {
void StackWidget::showPage(size_t page) {
if (m_shownPage)
m_shownPage->hide();
m_shownPage = m_members[page];
m_page = makeLeft(page);
if (m_shownPage)
m_shownPage->show();
}
void StackWidget::showPage(String const& name) {
if (m_shownPage)
m_shownPage->hide();
m_shownPage = m_memberHash.get(name);
m_page = makeRight(name);
if (m_shownPage)
m_shownPage->show();
}
Either<size_t, String> StackWidget::currentPage() const {
return m_page;
}
void StackWidget::addChild(String const& name, WidgetPtr member) {
Widget::addChild(name, member);
if (m_members.size() != 1)
member->hide();
else
showPage(0);
}
}

View file

@ -0,0 +1,25 @@
#ifndef STAR_STACK_WIDGET_HPP
#define STAR_STACK_WIDGET_HPP
#include "StarWidget.hpp"
#include "StarEither.hpp"
namespace Star {
STAR_CLASS(StackWidget);
class StackWidget : public Widget {
public:
void showPage(size_t page);
void showPage(String const& name);
Either<size_t, String> currentPage() const;
virtual void addChild(String const& name, WidgetPtr member) override;
private:
WidgetPtr m_shownPage;
Either<size_t, String> m_page;
};
}
#endif

View file

@ -0,0 +1,92 @@
#include "StarTabSet.hpp"
#include "StarButtonWidget.hpp"
#include "StarStackWidget.hpp"
#include "StarFlowLayout.hpp"
#include "StarGuiReader.hpp"
#include "StarRoot.hpp"
#include "StarLexicalCast.hpp"
#include "StarImageMetadataDatabase.hpp"
namespace Star {
TabSetWidget::TabSetWidget(TabSetConfig const& tabSetConfig) {
m_tabSetConfig = tabSetConfig;
m_tabBar = make_shared<FlowLayout>();
m_tabBar->setSpacing(m_tabSetConfig.tabButtonSpacing);
Widget::addChild("tabBar", m_tabBar);
m_stack = make_shared<StackWidget>();
addChild("tabs", m_stack);
markAsContainer();
}
void TabSetWidget::setSize(Vec2I const& size) {
auto imgMetadata = Root::singleton().imageMetadataDatabase();
auto tabHeight = max({imgMetadata->imageSize(m_tabSetConfig.tabButtonBaseImage).y(),
imgMetadata->imageSize(m_tabSetConfig.tabButtonHoverImage).y(),
imgMetadata->imageSize(m_tabSetConfig.tabButtonPressedImage).y(),
imgMetadata->imageSize(m_tabSetConfig.tabButtonBaseImageSelected).y(),
imgMetadata->imageSize(m_tabSetConfig.tabButtonHoverImageSelected).y(),
imgMetadata->imageSize(m_tabSetConfig.tabButtonPressedImageSelected).y()});
Widget::setSize(Vec2I(size.x(), max<int>(size.y(), tabHeight)));
m_tabBar->setSize({size.x(), tabHeight});
m_tabBar->setPosition({0, size.y() - tabHeight});
m_stack->setSize({size.x(), size.y() - tabHeight});
}
void TabSetWidget::addTab(String const& widgetName, WidgetPtr widget, String const& title) {
auto newButton = make_shared<ButtonWidget>();
newButton->setImages(
m_tabSetConfig.tabButtonBaseImage, m_tabSetConfig.tabButtonHoverImage, m_tabSetConfig.tabButtonPressedImage);
newButton->setCheckedImages(m_tabSetConfig.tabButtonBaseImageSelected,
m_tabSetConfig.tabButtonHoverImageSelected,
m_tabSetConfig.tabButtonPressedImageSelected);
newButton->setCheckable(true);
newButton->setText(title);
newButton->setTextOffset(m_tabSetConfig.tabButtonTextOffset);
newButton->setPressedOffset(m_tabSetConfig.tabButtonPressedOffset);
size_t pageForButton = m_tabBar->numChildren();
newButton->setCallback([this, pageForButton](Widget*) { tabSelect(pageForButton); });
m_tabBar->addChild(toString(pageForButton), newButton);
m_stack->addChild(widgetName, widget);
if (!m_lastSelected)
tabSelect(0);
}
size_t TabSetWidget::tabCount() const {
return m_tabBar->numChildren();
}
void TabSetWidget::tabSelect(size_t page) {
if (m_lastSelected != page) {
m_lastSelected = page;
m_stack->showPage(page);
for (size_t i = 0; i < m_tabBar->numChildren(); ++i) {
if (i == page)
m_tabBar->getChildNum<ButtonWidget>(i)->setChecked(true);
else
m_tabBar->getChildNum<ButtonWidget>(i)->setChecked(false);
}
if (m_callback)
m_callback(this);
} else {
m_tabBar->getChildNum<ButtonWidget>(page)->setChecked(true);
}
}
size_t TabSetWidget::selectedTab() const {
return m_lastSelected.value(NPos);
}
void TabSetWidget::setCallback(WidgetCallbackFunc callback) {
m_callback = move(callback);
}
}

View file

@ -0,0 +1,51 @@
#ifndef STAR_TAB_SET_HPP
#define STAR_TAB_SET_HPP
#include "StarButtonGroup.hpp"
namespace Star {
STAR_CLASS(FlowLayout);
STAR_CLASS(StackWidget);
STAR_CLASS(TabBarWidget);
STAR_CLASS(TabSetWidget);
struct TabSetConfig {
String tabButtonBaseImage;
String tabButtonHoverImage;
String tabButtonPressedImage;
String tabButtonBaseImageSelected;
String tabButtonHoverImageSelected;
String tabButtonPressedImageSelected;
Vec2I tabButtonPressedOffset;
Vec2I tabButtonTextOffset;
Vec2I tabButtonSpacing;
};
class TabSetWidget : public Widget {
public:
TabSetWidget(TabSetConfig const& tabSetconfig);
virtual void setSize(Vec2I const& size) override;
void addTab(String const& widgetName, WidgetPtr widget, String const& title);
size_t tabCount() const;
void tabSelect(size_t page);
size_t selectedTab() const;
// Callback is called when the tab changes
void setCallback(WidgetCallbackFunc callback);
private:
TabSetConfig m_tabSetConfig;
FlowLayoutPtr m_tabBar;
StackWidgetPtr m_stack;
WidgetCallbackFunc m_callback;
Maybe<size_t> m_lastSelected;
};
}
#endif

View file

@ -0,0 +1,393 @@
#include "StarTextBoxWidget.hpp"
#include "StarRoot.hpp"
#include "StarJsonExtra.hpp"
#include "StarAssets.hpp"
namespace Star {
TextBoxWidget::TextBoxWidget(String const& startingText, String const& hint, WidgetCallbackFunc callback)
: m_text(startingText), m_hint(hint), m_callback(callback) {
m_regex = ".*";
m_repeatKeyThreshold = 0;
m_repeatCode = SpecialRepeatKeyCodes::None;
m_isPressed = false;
m_isHover = false;
m_cursorOffset = startingText.size();
m_hAnchor = HorizontalAnchor::LeftAnchor;
m_vAnchor = VerticalAnchor::BottomAnchor;
m_color = Color::rgbf(1, 1, 1);
m_drawBorder = false;
m_cursorHoriz = Vec2I();
m_cursorVert = Vec2I();
m_overfillMode = true;
auto assets = Root::singleton().assets();
m_maxWidth = assets->json("/interface.config:textBoxDefaultWidth").toInt();
m_fontSize = assets->json("/interface.config:font.baseSize").toInt();
// Meh, padding is hard-coded here
setSize({m_maxWidth + 6, m_fontSize + 2});
m_cursorHoriz = jsonToVec2I(assets->json("/interface.config:textboxCursorHorizontal"));
m_cursorVert = jsonToVec2I(assets->json("/interface.config:textboxCursorVertical"));
}
void TextBoxWidget::renderImpl() {
float blueRate = 0;
if (m_isHover && !m_isPressed)
blueRate = 0.2f;
else
blueRate = 0.0f;
Vec2F pos(screenPosition());
if (m_hAnchor == HorizontalAnchor::HMidAnchor)
pos += Vec2F(size()[0] / 2.0f, 0);
else if (m_hAnchor == HorizontalAnchor::RightAnchor)
pos += Vec2F(size()[0], 0);
if ((m_maxWidth != -1) && m_overfillMode) {
context()->setFontSize(m_fontSize);
int shift = std::max(0, getCursorOffset() - m_maxWidth);
pos += Vec2F(-shift, 0);
}
context()->setFontSize(m_fontSize);
if (m_text.empty()) {
context()->setFontColor(m_color.mix(Color::rgbf(0.3f, 0.3f, 0.3f), 0.8f).mix(Color::rgbf(0.0f, 0.0f, 1.0f), blueRate).toRgba());
context()->renderInterfaceText(m_hint, {pos, m_hAnchor, m_vAnchor});
} else {
context()->setFontColor(m_color.mix(Color::rgbf(0, 0, 1), blueRate).toRgba());
context()->renderInterfaceText(m_text, {pos, m_hAnchor, m_vAnchor});
}
context()->setFontColor(Vec4B::filled(255));
if (hasFocus()) {
// render cursor
float cc = 0.6f + 0.4f * std::sin((double)Time::monotonicMilliseconds() / 300.0);
Color cursorColor = Color::rgbf(cc, cc, cc);
context()->drawInterfaceLine(
pos + Vec2F(getCursorOffset(), m_fontSize * m_cursorVert[0]),
pos + Vec2F(getCursorOffset(), m_fontSize * m_cursorVert[1]),
cursorColor.toRgba());
context()->drawInterfaceLine(
pos + Vec2F(getCursorOffset() + m_fontSize * m_cursorHoriz[0], m_fontSize * m_cursorVert[0]),
pos + Vec2F(getCursorOffset() + m_fontSize * m_cursorHoriz[1], m_fontSize * m_cursorVert[0]),
cursorColor.toRgba());
}
if (m_drawBorder)
context()->drawInterfacePolyLines(PolyF(screenBoundRect()), Color(Color::White).toRgba());
}
int TextBoxWidget::getCursorOffset() { // horizontal only
float scale;
context()->setFontSize(m_fontSize);
if (m_hAnchor == HorizontalAnchor::LeftAnchor) {
scale = 1.0;
} else if (m_hAnchor == HorizontalAnchor::HMidAnchor) {
scale = 0.5;
} else if (m_hAnchor == HorizontalAnchor::RightAnchor) {
scale = -1.0;
return context()->stringInterfaceWidth(m_text) * scale
+ context()->stringInterfaceWidth(m_text.substr(m_cursorOffset, m_text.size()));
} else {
throw GuiException("Somehow, the value of m_hAnchor became bad");
}
return (int)std::ceil(context()->stringInterfaceWidth(m_text) * scale
- context()->stringInterfaceWidth(m_text.substr(m_cursorOffset, m_text.size())));
}
void TextBoxWidget::update() {
Widget::update();
if (m_repeatCode != SpecialRepeatKeyCodes::None) {
if (Time::monotonicMilliseconds() >= m_repeatKeyThreshold) {
m_repeatKeyThreshold += 50;
if (m_repeatCode == SpecialRepeatKeyCodes::Delete) {
if (m_cursorOffset < (int)m_text.size())
modText(m_text.substr(0, m_cursorOffset) + m_text.substr(m_cursorOffset + 1));
} else if (m_repeatCode == SpecialRepeatKeyCodes::Backspace) {
if (m_cursorOffset > 0) {
if (modText(m_text.substr(0, m_cursorOffset - 1) + m_text.substr(m_cursorOffset)))
m_cursorOffset -= 1;
}
} else if (m_repeatCode == SpecialRepeatKeyCodes::Left) {
if (m_cursorOffset > 0) {
m_cursorOffset--;
if (m_cursorOffset < 0)
m_cursorOffset = 0;
}
} else if (m_repeatCode == SpecialRepeatKeyCodes::Right) {
if (m_cursorOffset < (int)m_text.size()) {
m_cursorOffset++;
if (m_cursorOffset > (int)m_text.size())
m_cursorOffset = m_text.size();
}
}
}
}
}
String TextBoxWidget::getText() {
return m_text;
}
bool TextBoxWidget::setText(String const& text, bool callback) {
if (m_text == text)
return true;
if (!newTextValid(text))
return false;
m_text = text;
m_cursorOffset = m_text.size();
m_repeatCode = SpecialRepeatKeyCodes::None;
if (callback)
m_callback(this);
return true;
}
String TextBoxWidget::getRegex() {
return m_regex;
}
void TextBoxWidget::setRegex(String const& regex) {
m_regex = regex;
}
void TextBoxWidget::setColor(Color const& color) {
m_color = color;
}
void TextBoxWidget::setFontSize(int fontSize) {
m_fontSize = fontSize;
}
void TextBoxWidget::setMaxWidth(int maxWidth) {
m_maxWidth = maxWidth;
setSize({m_maxWidth + 6, m_fontSize + 2});
}
void TextBoxWidget::setOverfillMode(bool overtype) {
m_overfillMode = overtype;
}
void TextBoxWidget::setOnBlurCallback(WidgetCallbackFunc onBlur) {
m_onBlur = onBlur;
}
void TextBoxWidget::setOnEnterKeyCallback(WidgetCallbackFunc onEnterKey) {
m_onEnterKey = onEnterKey;
}
void TextBoxWidget::setOnEscapeKeyCallback(WidgetCallbackFunc onEscapeKey) {
m_onEscapeKey = onEscapeKey;
}
void TextBoxWidget::setNextFocus(Maybe<String> nextFocus) {
m_nextFocus = nextFocus;
}
void TextBoxWidget::setPrevFocus(Maybe<String> prevFocus) {
m_prevFocus = prevFocus;
}
void TextBoxWidget::mouseOver() {
Widget::mouseOver();
m_isHover = true;
}
void TextBoxWidget::mouseOut() {
Widget::mouseOut();
m_isHover = false;
m_isPressed = false;
}
void TextBoxWidget::mouseReturnStillDown() {
Widget::mouseReturnStillDown();
m_isHover = true;
m_isPressed = true;
}
void TextBoxWidget::blur() {
Widget::blur();
if (m_onBlur)
m_onBlur(this);
m_repeatCode = SpecialRepeatKeyCodes::None;
}
KeyboardCaptureMode TextBoxWidget::keyboardCaptured() const {
if (active() && hasFocus())
return KeyboardCaptureMode::TextInput;
return KeyboardCaptureMode::None;
}
bool TextBoxWidget::sendEvent(InputEvent const& event) {
if (!hasFocus())
return false;
if (event.is<KeyUpEvent>()) {
m_repeatCode = SpecialRepeatKeyCodes::None;
m_callback(this);
return false;
}
if (innerSendEvent(event)) {
m_callback(this);
return true;
}
return false;
}
bool TextBoxWidget::innerSendEvent(InputEvent const& event) {
if (auto keyDown = event.ptr<KeyDownEvent>()) {
m_repeatKeyThreshold = Time::monotonicMilliseconds() + 300LL;
if (keyDown->key == Key::Escape) {
if (m_onEscapeKey) {
m_onEscapeKey(this);
return true;
}
return false;
}
if (keyDown->key == Key::Return || keyDown->key == Key::Kp_enter) {
if (m_onEnterKey) {
m_onEnterKey(this);
return true;
}
return false;
}
if (keyDown->key == Key::Tab) {
if ((KeyMod::LShift & keyDown->mods) == KeyMod::LShift || (KeyMod::RShift & keyDown->mods) == KeyMod::RShift) {
if (m_prevFocus) {
if (auto prevWidget = parent()->fetchChild(*m_prevFocus)) {
prevWidget->focus();
return true;
}
}
} else {
if (m_nextFocus) {
if (auto nextWidget = parent()->fetchChild(*m_nextFocus)) {
nextWidget->focus();
return true;
}
}
}
return false;
}
if ((keyDown->mods & (KeyMod::LCtrl | KeyMod::RCtrl)) != KeyMod::NoMod) {
if (keyDown->key == Key::C) {
context()->setClipboard(m_text);
return true;
}
if (keyDown->key == Key::X) {
context()->setClipboard(m_text);
if (modText(""))
m_cursorOffset = 0;
return true;
}
if (keyDown->key == Key::V) {
String clipboard = context()->getClipboard();
if (modText(m_text.substr(0, m_cursorOffset) + clipboard + m_text.substr(m_cursorOffset)))
m_cursorOffset += clipboard.size();
return true;
}
}
int rightSteps = 1;
int leftSteps = 1;
if ((keyDown->mods & (KeyMod::LCtrl | KeyMod::RCtrl)) != KeyMod::NoMod || (keyDown->mods & (KeyMod::LAlt | KeyMod::RAlt)) != KeyMod::NoMod) {
rightSteps = m_text.findNextBoundary(m_cursorOffset) - m_cursorOffset;
leftSteps = m_cursorOffset - m_text.findNextBoundary(m_cursorOffset, true);
if (rightSteps < 1)
rightSteps = 1;
if (leftSteps < 1)
leftSteps = 1;
}
if (keyDown->key == Key::Backspace) {
m_repeatCode = SpecialRepeatKeyCodes::Backspace;
for (int i = 0; i < leftSteps; i++) {
if (m_cursorOffset > 0) {
if (modText(m_text.substr(0, m_cursorOffset - 1) + m_text.substr(m_cursorOffset)))
m_cursorOffset -= 1;
}
}
return true;
}
if (keyDown->key == Key::Delete) {
m_repeatCode = SpecialRepeatKeyCodes::Delete;
for (int i = 0; i < rightSteps; i++) {
if (m_cursorOffset < (int)m_text.size())
modText(m_text.substr(0, m_cursorOffset) + m_text.substr(m_cursorOffset + 1));
}
return true;
}
if (keyDown->key == Key::Left) {
m_repeatCode = SpecialRepeatKeyCodes::Left;
for (int i = 0; i < leftSteps; i++) {
m_cursorOffset--;
if (m_cursorOffset < 0)
m_cursorOffset = 0;
}
return true;
}
if (keyDown->key == Key::Right) {
m_repeatCode = SpecialRepeatKeyCodes::Right;
for (int i = 0; i < rightSteps; i++) {
m_cursorOffset++;
if (m_cursorOffset > (int)m_text.size())
m_cursorOffset = m_text.size();
}
return true;
}
if (keyDown->key == Key::Home) {
m_cursorOffset = 0;
return true;
}
if (keyDown->key == Key::End) {
m_cursorOffset = m_text.size();
return true;
}
}
if (auto textInput = event.ptr<TextInputEvent>()) {
if (modText(m_text.substr(0, m_cursorOffset) + textInput->text + m_text.substr(m_cursorOffset)))
m_cursorOffset += textInput->text.size();
return true;
}
return false;
}
void TextBoxWidget::setDrawBorder(bool drawBorder) {
m_drawBorder = drawBorder;
}
void TextBoxWidget::setTextAlign(HorizontalAnchor hAnchor) {
m_hAnchor = hAnchor;
}
bool TextBoxWidget::modText(String const& text) {
if (m_text != text && newTextValid(text)) {
m_text = text;
return true;
} else {
return false;
}
}
bool TextBoxWidget::newTextValid(String const& text) const {
if (!text.regexMatch(m_regex))
return false;
if ((m_maxWidth != -1) && !m_overfillMode) {
context()->setFontSize(m_fontSize);
return context()->stringInterfaceWidth(text) <= m_maxWidth;
}
return true;
}
}

View file

@ -0,0 +1,85 @@
#ifndef STAR_TEXTBOX_WIDGET_HPP
#define STAR_TEXTBOX_WIDGET_HPP
#include "StarWidget.hpp"
namespace Star {
enum class SpecialRepeatKeyCodes : String::Char { None, Delete, Backspace, Left, Right };
STAR_CLASS(TextBoxWidget);
class TextBoxWidget : public Widget {
public:
TextBoxWidget(String const& startingText, String const& hint, WidgetCallbackFunc callback);
virtual void update() override;
String getText();
bool setText(String const& text, bool callback = true);
// Set the regex that the text-box must match. Defaults to .*
String getRegex();
void setRegex(String const& regex);
void setColor(Color const& color);
void setFontSize(int fontSize);
void setMaxWidth(int maxWidth);
void setOverfillMode(bool overfillMode);
void setOnBlurCallback(WidgetCallbackFunc onBlur);
void setOnEnterKeyCallback(WidgetCallbackFunc onEnterKey);
void setOnEscapeKeyCallback(WidgetCallbackFunc onEscapeKey);
void setNextFocus(Maybe<String> nextFocus);
void setPrevFocus(Maybe<String> prevFocus);
bool sendEvent(InputEvent const& event) override;
void setDrawBorder(bool drawBorder);
void setTextAlign(HorizontalAnchor hAnchor);
int getCursorOffset();
virtual void mouseOver() override;
virtual void mouseOut() override;
virtual void mouseReturnStillDown() override;
virtual void blur() override;
virtual KeyboardCaptureMode keyboardCaptured() const override;
protected:
virtual void renderImpl() override;
private:
bool innerSendEvent(InputEvent const& event);
bool modText(String const& text);
bool newTextValid(String const& text) const;
String m_text;
String m_hint;
String m_regex;
HorizontalAnchor m_hAnchor;
VerticalAnchor m_vAnchor;
Color m_color;
int m_fontSize;
int m_maxWidth;
int m_cursorOffset;
bool m_isHover;
bool m_isPressed;
SpecialRepeatKeyCodes m_repeatCode;
int64_t m_repeatKeyThreshold;
WidgetCallbackFunc m_callback;
WidgetCallbackFunc m_onBlur;
WidgetCallbackFunc m_onEnterKey;
WidgetCallbackFunc m_onEscapeKey;
Maybe<String> m_nextFocus;
Maybe<String> m_prevFocus;
bool m_drawBorder;
Vec2I m_cursorHoriz;
Vec2I m_cursorVert;
bool m_overfillMode;
};
}
#endif

View file

@ -0,0 +1,101 @@
#include "StarVerticalLayout.hpp"
namespace Star {
VerticalLayout::VerticalLayout(VerticalAnchor verticalAnchor, int verticalSpacing) {
setVerticalAnchor(verticalAnchor);
setVerticalSpacing(verticalSpacing);
disableScissoring();
}
void VerticalLayout::update() {
m_size = Vec2I(0, 0);
if (m_members.empty())
return;
for (auto const& child : m_members) {
auto childSize = child->size();
m_size[1] += childSize[1];
m_size[0] = max(m_size[0], childSize[0]);
}
m_size[1] += (m_members.size() - 1) * m_verticalSpacing;
auto bounds = contentBoundRect();
int verticalPos = m_fillDown ? bounds.yMax() : bounds.yMin();
for (auto const& child : reverseIterate(m_members)) {
auto childSize = child->size();
Vec2I targetPosition;
if (m_horizontalAnchor == HorizontalAnchor::LeftAnchor)
targetPosition[0] = bounds.xMin();
else if (m_horizontalAnchor == HorizontalAnchor::RightAnchor)
targetPosition[0] = bounds.xMax() - childSize[0];
else if (m_horizontalAnchor == HorizontalAnchor::HMidAnchor)
targetPosition[0] = -childSize[0] / 2;
if (m_fillDown) {
verticalPos -= childSize[1];
targetPosition[1] = verticalPos;
verticalPos -= m_verticalSpacing;
} else {
targetPosition[1] = verticalPos;
verticalPos -= childSize[1];
verticalPos -= m_verticalSpacing;
}
// needed because position is included in relativeBoundRect
child->setPosition(Vec2I(0, 0));
child->setPosition(targetPosition - child->relativeBoundRect().min());
}
}
Vec2I VerticalLayout::size() const {
return m_size;
}
RectI VerticalLayout::relativeBoundRect() const {
return contentBoundRect().translated(relativePosition());
}
void VerticalLayout::setHorizontalAnchor(HorizontalAnchor horizontalAnchor) {
m_horizontalAnchor = horizontalAnchor;
update();
}
void VerticalLayout::setVerticalAnchor(VerticalAnchor verticalAnchor) {
m_verticalAnchor = verticalAnchor;
update();
}
void VerticalLayout::setVerticalSpacing(int verticalSpacing) {
m_verticalSpacing = verticalSpacing;
update();
}
void VerticalLayout::setFillDown(bool fillDown) {
m_fillDown = fillDown;
update();
}
RectI VerticalLayout::contentBoundRect() const {
auto min = Vec2I(0, 0);
if (m_horizontalAnchor == HorizontalAnchor::RightAnchor)
min[0] -= m_size[0];
else if (m_horizontalAnchor == HorizontalAnchor::HMidAnchor)
min[0] -= m_size[0] / 2;
if (m_verticalAnchor == VerticalAnchor::TopAnchor)
min[1] -= m_size[1];
else if (m_verticalAnchor == VerticalAnchor::VMidAnchor)
min[1] -= m_size[1] / 2;
return RectI::withSize(min, m_size);
}
}

View file

@ -0,0 +1,35 @@
#ifndef STAR_VERTICAL_LAYOUT_HPP
#define STAR_VERTICAL_LAYOUT_HPP
#include "StarLayout.hpp"
namespace Star {
STAR_CLASS(VerticalLayout);
class VerticalLayout : public Layout {
public:
VerticalLayout(VerticalAnchor verticalAnchor = VerticalAnchor::TopAnchor, int verticalSpacing = 0);
void update() override;
Vec2I size() const override;
RectI relativeBoundRect() const override;
void setHorizontalAnchor(HorizontalAnchor horizontalAnchor);
void setVerticalAnchor(VerticalAnchor verticalAnchor);
void setVerticalSpacing(int verticalSpacing);
void setFillDown(bool fillDown);
private:
RectI contentBoundRect() const;
HorizontalAnchor m_horizontalAnchor;
VerticalAnchor m_verticalAnchor;
int m_verticalSpacing;
bool m_fillDown;
Vec2I m_size;
};
}
#endif

View file

@ -0,0 +1,474 @@
#include "StarWidget.hpp"
#include "StarPane.hpp"
#include "StarLabelWidget.hpp"
#include "StarFlowLayout.hpp"
namespace Star {
Widget::Widget() {
m_parent = nullptr;
m_context = GuiContext::singletonPtr();
m_visible = true;
m_focus = false;
m_doScissor = true;
m_container = false;
m_mouseTransparent = false;
}
Widget::~Widget() {
removeAllChildren();
}
void Widget::update() {
for (auto widget : m_members)
widget->update();
}
GuiContext* Widget::context() const {
return m_context;
}
void Widget::render(RectI const& region) {
if (!m_visible)
return;
if (!setupDrawRegion(region))
return;
renderImpl();
drawChildren();
}
void Widget::renderImpl() {}
void Widget::drawChildren() {
for (auto child : m_members)
child->render(m_drawingArea);
}
Vec2I Widget::position() const {
return m_position + m_drawingOffset;
}
Vec2I Widget::relativePosition() const {
return m_position;
}
bool Widget::setupDrawRegion(RectI const& region) {
RectI scissorRect;
if (m_doScissor) {
scissorRect = getScissorRect();
} else {
scissorRect = noScissor();
}
m_drawingArea = scissorRect.limited(region);
if (m_drawingArea.isEmpty())
return false;
m_context->setInterfaceScissorRect(m_drawingArea);
return true;
}
Vec2I Widget::drawingOffset() const {
return m_drawingOffset;
}
void Widget::setDrawingOffset(Vec2I const& offset) {
m_drawingOffset = offset;
}
RectI Widget::getScissorRect() const {
return screenBoundRect();
}
RectI Widget::noScissor() const {
return RectI::inf();
}
void Widget::disableScissoring() {
m_doScissor = false;
}
void Widget::enableScissoring() {
m_doScissor = true;
}
void Widget::setPosition(Vec2I const& position) {
m_position = position;
}
Vec2I Widget::size() const {
return m_size;
}
void Widget::setSize(Vec2I const& size) {
m_size = size;
}
RectI Widget::relativeBoundRect() const {
return RectI::withSize(relativePosition(), size());
}
RectI Widget::screenBoundRect() const {
return relativeBoundRect().translated(screenPosition() - relativePosition());
}
void Widget::determineSizeFromChildren() {
Vec2I max;
for (auto& child : m_members) {
Vec2I childMax = child->position() + child->size();
max = max.piecewiseMax(childMax);
}
setSize(max);
}
void Widget::markAsContainer() {
m_container = true;
}
KeyboardCaptureMode Widget::keyboardCaptured() const {
if (active()) {
for (auto const& member : m_members) {
auto mode = member->keyboardCaptured();
if (mode != KeyboardCaptureMode::None)
return mode;
}
}
return KeyboardCaptureMode::None;
}
void Widget::setData(Json const& data) {
m_data = data;
}
Json const& Widget::data() {
return m_data;
}
Vec2I Widget::screenPosition() const {
if (m_parent) {
return m_parent->screenPosition() + position();
} else {
return position();
}
}
bool Widget::inMember(Vec2I const& position) const {
if (!m_visible)
return false;
if (m_mouseTransparent)
return false;
if (!m_drawingArea.isNull() && !m_drawingArea.contains(Vec2I::floor(position)))
return false;
if (m_container) {
for (auto child : m_members)
if (child->inMember(position))
return true;
} else {
return screenBoundRect().contains(position);
}
return false;
}
bool Widget::sendEvent(InputEvent const& event) {
if (!m_visible)
return false;
for (auto child : reverseIterate(m_members)) {
if (child->sendEvent(event))
return true;
}
return false;
}
void Widget::mouseOver() {}
void Widget::mouseOut() {}
void Widget::mouseReturnStillDown() {}
void Widget::setMouseTransparent(bool transparent) {
m_mouseTransparent = transparent;
}
bool Widget::mouseTransparent() {
return m_mouseTransparent;
}
Widget* Widget::parent() const {
return m_parent;
}
void Widget::setParent(Widget* parent) {
m_parent = parent;
}
void Widget::show() {
m_visible = true;
}
void Widget::hide() {
m_visible = false;
}
void Widget::toggleVisibility() {
m_visible = !m_visible;
}
void Widget::setVisibility(bool visibility) {
if (visibility)
show();
else
hide();
}
bool Widget::active() const {
return m_visible;
}
bool Widget::interactive() const {
return true;
}
bool Widget::hasFocus() const {
return m_focus;
}
void Widget::focus() {
m_focus = true;
if (auto w = window())
w->setFocus(this);
}
void Widget::blur() {
m_focus = false;
if (window())
window()->removeFocus(this);
}
unsigned Widget::windowHeight() const {
return context()->windowHeight();
}
unsigned Widget::windowWidth() const {
return context()->windowWidth();
}
Vec2I Widget::windowSize() const {
return Vec2I(context()->windowSize());
}
Pane* Widget::window() {
if (m_parent)
return m_parent->window();
return nullptr;
}
Pane const* Widget::window() const {
if (m_parent)
return m_parent->window();
return nullptr;
}
void Widget::addChild(String const& name, WidgetPtr member) {
member->setName(name);
m_members.push_back(member);
m_memberHash[member->name()] = member;
member->setParent(this);
}
void Widget::addChildAt(String const& name, WidgetPtr member, size_t at) {
if (at > m_members.size())
throw GuiException("Attempted to insert item after the end of the list.");
m_members.insert(m_members.begin() + at, member);
m_memberHash[name] = member;
member->setName(name);
member->setParent(this);
}
bool Widget::removeChild(Widget* member) {
m_memberHash.erase(member->name());
for (auto child = m_members.begin(); child != m_members.end(); ++child) {
if (child->get() == member) {
(*child)->setParent(nullptr);
m_members.erase(child);
return true;
} else {
if ((*child)->removeChild(member))
return true;
}
}
return false;
}
bool Widget::removeChild(String const& name) {
m_memberHash.erase(name);
for (auto child = m_members.begin(); child != m_members.end(); ++child) {
auto ptr = *child;
if (ptr->name() == name) {
m_members.erase(child);
ptr->setParent(nullptr);
return true;
}
}
return false;
}
bool Widget::removeChildAt(size_t at) {
if (at >= m_members.size())
return false;
m_memberHash.erase(m_members.at(at)->name());
m_members.at(at)->setParent(nullptr);
m_members.erase(m_members.begin() + at);
return true;
}
void Widget::removeAllChildren() {
for (auto child : m_members)
child->setParent(nullptr);
m_members.clear();
m_memberHash.clear();
}
bool Widget::containsChild(String const& name) {
if (fetchChild(name))
return true;
return false;
}
WidgetPtr Widget::fetchChild(String const& name) {
WidgetPtr res;
if (name.contains(".")) {
StringList nameList = name.split(".", 1);
if (auto child = m_memberHash.value(nameList[0], {}))
return child->fetchChild(nameList[1]);
} else {
if (auto child = m_memberHash.value(name, {}))
return child;
}
return {};
}
WidgetPtr Widget::findChild(String const& name) {
if (auto found = fetchChild(name))
return found;
for (auto const& child : m_members) {
if (auto found = child->findChild(name))
return found;
}
return {};
}
WidgetPtr Widget::childPtr(Widget const* child) const {
for (auto const& m : m_members) {
if (m.get() == child)
return m;
if (auto c = m->childPtr(child))
return c;
}
return {};
}
WidgetPtr Widget::getChildAt(Vec2I const& pos) {
for (auto child : reverseIterate(m_members)) {
if (child->inMember(pos)) {
auto res = child->getChildAt(pos);
if (res) {
return res;
}
return child;
}
}
return {};
}
size_t Widget::numChildren() const {
return m_members.size();
}
WidgetPtr Widget::getChildNum(size_t num) const {
return m_members.at(num);
}
String const& Widget::name() const {
return m_name;
}
void Widget::setName(String const& name) {
m_name = name;
}
String Widget::fullName() const {
if (m_parent) {
return m_parent->fullName() + "." + name();
}
return name();
}
String Widget::toStringImpl(int indentLevel) const {
auto leader = String(" ") * indentLevel;
String childrenString;
for (auto child : m_members) {
childrenString.append(child->toStringImpl(indentLevel + 4));
}
String output = strf(R"OUTPUT(%s%s : {
%s address : %p,
%s visible : %s,
%s position : %s,
%s size : %s,
%s children : {
%s
%s }
%s}
)OUTPUT",
leader,
m_name,
leader,
this,
leader,
m_visible ? "true" : "false",
leader,
m_position,
leader,
m_size,
leader,
childrenString,
leader,
leader);
return output;
}
bool Widget::setLabel(String const& name, String const& value) {
if (containsChild(name)) {
auto child = fetchChild(name);
if (auto label = as<LabelWidget>(child)) {
label->setText(value);
return true;
}
}
return false;
}
std::ostream& operator<<(std::ostream& os, Widget const& widget) {
os << widget.toStringImpl(0);
return os;
}
}

View file

@ -0,0 +1,178 @@
#ifndef STAR_WIDGET_HPP
#define STAR_WIDGET_HPP
#include "StarVector.hpp"
#include "StarCasting.hpp"
#include "StarInputEvent.hpp"
#include "StarGuiContext.hpp"
namespace Star {
STAR_EXCEPTION(GuiException, StarException);
STAR_CLASS(Widget);
STAR_CLASS(Pane);
enum class KeyboardCaptureMode {
None,
KeyEvents,
TextInput
};
typedef function<void(Widget*)> WidgetCallbackFunc;
class Widget {
public:
Widget();
virtual ~Widget();
Widget(Widget const& copy) = delete;
Widget& operator=(Widget const&) = delete;
virtual void render(RectI const& region) final;
virtual void update();
GuiContext* context() const;
// Position of widget with drawing offset (useful for drawing)
virtual Vec2I position() const;
// Position of widget ignoring offset (useful for moving and placing widgets
// relative to its current position)
virtual Vec2I relativePosition() const;
// Set position of widget, ignoring offset
virtual void setPosition(Vec2I const& position);
virtual Vec2I drawingOffset() const;
virtual void setDrawingOffset(Vec2I const& offset);
virtual Vec2I size() const;
virtual void setSize(Vec2I const& size);
virtual RectI relativeBoundRect() const;
virtual RectI screenBoundRect() const;
virtual bool inMember(Vec2I const& position) const;
virtual bool sendEvent(InputEvent const& event);
virtual void show();
virtual void hide();
virtual void toggleVisibility();
virtual void setVisibility(bool visibility);
virtual void mouseOver();
virtual void mouseOut();
virtual void mouseReturnStillDown();
virtual void setMouseTransparent(bool transparent);
virtual bool mouseTransparent();
virtual bool active() const;
virtual bool interactive() const;
virtual bool hasFocus() const;
virtual void focus();
virtual void blur();
virtual Widget* parent() const;
virtual void setParent(Widget* parent);
virtual Pane const* window() const;
virtual Pane* window();
virtual void addChild(String const& name, WidgetPtr member);
virtual void addChildAt(String const& name, WidgetPtr member, size_t at);
virtual bool removeChild(Widget* member);
virtual bool removeChild(String const& name);
virtual bool removeChildAt(size_t at);
virtual WidgetPtr getChildAt(Vec2I const& pos);
virtual bool containsChild(String const& name);
virtual WidgetPtr fetchChild(String const& name);
template <typename WidgetType>
shared_ptr<WidgetType> fetchChild(String const& name);
virtual WidgetPtr findChild(String const& name);
template <typename WidgetType>
shared_ptr<WidgetType> findChild(String const& name);
WidgetPtr childPtr(Widget const* widget) const;
virtual size_t numChildren() const;
virtual WidgetPtr getChildNum(size_t num) const;
template <typename WidgetType>
shared_ptr<WidgetType> getChildNum(size_t num) const;
virtual void removeAllChildren();
virtual String const& name() const;
virtual void setName(String const& name);
String fullName() const;
unsigned windowHeight() const;
unsigned windowWidth() const;
Vec2I windowSize() const;
virtual Vec2I screenPosition() const;
void disableScissoring();
void enableScissoring();
void determineSizeFromChildren();
void markAsContainer();
virtual KeyboardCaptureMode keyboardCaptured() const;
void setData(Json const& data);
Json const& data();
bool setLabel(String const& name, String const& value);
protected:
friend std::ostream& operator<<(std::ostream& os, Widget const& widget);
String toStringImpl(int indentLevel) const;
virtual void renderImpl();
virtual void drawChildren();
bool setupDrawRegion(RectI const& region);
virtual RectI getScissorRect() const;
virtual RectI noScissor() const;
Widget* m_parent;
GuiContext* m_context;
bool m_visible;
PolyF m_boundPoly;
Vec2I m_position;
Vec2I m_size;
RectI m_drawingArea;
Vec2I m_drawingOffset;
String m_name;
List<WidgetPtr> m_members;
StringMap<WidgetPtr> m_memberHash;
Vec2I m_memberSize;
bool m_focus;
bool m_doScissor;
bool m_container;
bool m_mouseTransparent;
Json m_data;
};
std::ostream& operator<<(std::ostream& os, Widget const& widget);
template <typename WidgetType>
shared_ptr<WidgetType> Widget::getChildNum(size_t num) const {
return as<WidgetType>(getChildNum(num));
}
template <typename WidgetType>
shared_ptr<WidgetType> Widget::fetchChild(String const& name) {
return as<WidgetType>(fetchChild(name));
}
template <typename WidgetType>
shared_ptr<WidgetType> Widget::findChild(String const& name) {
return as<WidgetType>(findChild(name));
}
}
#endif

View file

@ -0,0 +1,815 @@
#include "StarWidgetParsing.hpp"
#include "StarRoot.hpp"
#include "StarJsonExtra.hpp"
#include "StarImageMetadataDatabase.hpp"
#include "StarPane.hpp"
#include "StarButtonGroup.hpp"
#include "StarButtonWidget.hpp"
#include "StarCanvasWidget.hpp"
#include "StarImageWidget.hpp"
#include "StarImageStretchWidget.hpp"
#include "StarPortraitWidget.hpp"
#include "StarFuelWidget.hpp"
#include "StarProgressWidget.hpp"
#include "StarLargeCharPlateWidget.hpp"
#include "StarTextBoxWidget.hpp"
#include "StarItemSlotWidget.hpp"
#include "StarItemGridWidget.hpp"
#include "StarListWidget.hpp"
#include "StarSliderBar.hpp"
#include "StarStackWidget.hpp"
#include "StarScrollArea.hpp"
#include "StarAssets.hpp"
#include "StarFlowLayout.hpp"
#include "StarVerticalLayout.hpp"
#include "StarTabSet.hpp"
namespace Star {
WidgetConstructResult::WidgetConstructResult() : zlevel() {}
WidgetConstructResult::WidgetConstructResult(WidgetPtr obj, String const& name, float zlevel)
: obj(obj), name(name), zlevel(zlevel) {}
WidgetParser::WidgetParser() {
// only the non-interactive ones by default
m_constructors["widget"] = [=](String const& name, Json const& config) { return widgetHandler(name, config); };
m_constructors["canvas"] = [=](String const& name, Json const& config) { return canvasHandler(name, config); };
m_constructors["image"] = [=](String const& name, Json const& config) { return imageHandler(name, config); };
m_constructors["imageStretch"] = [=](String const& name, Json const& config) { return imageStretchHandler(name, config); };
m_constructors["label"] = [=](String const& name, Json const& config) { return labelHandler(name, config); };
m_constructors["portrait"] = [=](String const& name, Json const& config) { return portraitHandler(name, config); };
m_constructors["fuelGauge"] = [=](String const& name, Json const& config) { return fuelGaugeHandler(name, config); };
m_constructors["progress"] = [=](String const& name, Json const& config) { return progressHandler(name, config); };
m_constructors["largeCharPlate"] = [=](
String const& name, Json const& config) { return largeCharPlateHandler(name, config); };
m_constructors["container"] = [=](String const& name, Json const& config) { return containerHandler(name, config); };
m_constructors["layout"] = [=](String const& name, Json const& config) { return layoutHandler(name, config); };
m_callbacks["null"] = [](Widget*) {};
}
void WidgetParser::construct(Json const& config, Widget* widget) {
m_pane = dynamic_cast<Pane*>(widget);
constructImpl(config, widget);
}
void WidgetParser::constructImpl(Json const& config, Widget* widget) {
List<WidgetConstructResult> widgets = constructor(config);
sort(widgets,
[&](WidgetConstructResult const& a, WidgetConstructResult const& b) -> bool {
if (a.zlevel == b.zlevel)
return a.obj->position() < b.obj->position();
return a.zlevel < b.zlevel;
});
for (auto const& res : widgets) {
widget->addChild(res.name, res.obj);
if (res.obj->hasFocus()) {
if (m_pane)
m_pane->setFocus(res.obj.get());
}
}
}
WidgetPtr WidgetParser::makeSingle(String const& name, Json const& config) {
if (!m_constructors.contains(config.getString("type"))) {
throw WidgetParserException(strf("Unknown type in gui json. %s", config.getString("type")));
}
auto constructResult = m_constructors.get(config.getString("type"))(name, config);
return constructResult.obj;
}
List<WidgetConstructResult> WidgetParser::constructor(Json const& config) {
List<WidgetConstructResult> widgets;
auto addWidget = [this, &widgets](Json const& memberConfig) {
if (memberConfig.type() != Json::Type::Object || !memberConfig.contains("type") || !memberConfig.contains("name"))
throw WidgetParserException(
"Malformed gui json: member configuration is either not a map, or does not specify a widget name and type");
String type = memberConfig.getString("type");
if (type == "include") {
widgets.appendAll(constructor(Root::singleton().assets()->json(memberConfig.getString("file"))));
} else {
if (!m_constructors.contains(type)) {
throw WidgetParserException(strf("Unknown type in gui json. %s", type));
}
auto constructResult =
m_constructors.get(memberConfig.getString("type"))(memberConfig.getString("name"), memberConfig);
if (constructResult.obj)
widgets.append(constructResult);
}
};
if (config.isType(Json::Type::Object)) {
for (auto const& kvpair : config.iterateObject())
addWidget(kvpair.second.set("name", kvpair.first));
} else if (config.isType(Json::Type::Array)) {
for (auto const& elem : config.iterateArray())
addWidget(elem);
} else {
throw WidgetParserException(strf("Malformed gui json, expected a Map or a List. Instead got %s", config));
}
return widgets;
}
void WidgetParser::registerCallback(String const& name, WidgetCallbackFunc callback) {
m_callbacks[name] = callback;
}
WidgetConstructResult WidgetParser::buttonHandler(String const& name, Json const& config) {
String baseImage;
bool invisible = config.getBool("invisible", false);
try {
if (!invisible)
baseImage = config.getString("base");
} catch (MapException const& e) {
throw WidgetParserException(
strf("Malformed gui json, missing a required value in the map. %s", outputException(e, false)));
}
String hoverImage = config.getString("hover", "");
String pressedImage = config.getString("pressed", "");
String disabledImage = config.getString("disabledImage", "");
String baseImageChecked = config.getString("baseImageChecked", "");
String hoverImageChecked = config.getString("hoverImageChecked", "");
String pressedImageChecked = config.getString("pressedImageChecked", "");
String disabledImageChecked = config.getString("disabledImageChecked", "");
String callback = config.getString("callback", name);
if (!m_callbacks.contains(callback))
throw WidgetParserException::format("Failed to find callback named: %s", callback);
WidgetCallbackFunc callbackFunc = m_callbacks.get(callback);
auto button = make_shared<ButtonWidget>(callbackFunc, baseImage, hoverImage, pressedImage, disabledImage);
button->setCheckedImages(baseImageChecked, hoverImageChecked, pressedImageChecked, disabledImageChecked);
common(button, config);
button->setInvisible(invisible);
if (config.contains("caption"))
button->setText(config.getString("caption"));
if (config.contains("pressedOffset"))
button->setPressedOffset(jsonToVec2I(config.get("pressedOffset")));
if (config.contains("textOffset"))
button->setTextOffset(jsonToVec2I(config.get("textOffset")));
if (config.contains("checkable"))
button->setCheckable(config.getBool("checkable"));
if (config.contains("checked"))
button->setChecked(config.getBool("checked"));
String hAnchor = config.getString("textAlign", "center");
if (hAnchor == "right") {
button->setTextAlign(HorizontalAnchor::RightAnchor);
} else if (hAnchor == "center") {
button->setTextAlign(HorizontalAnchor::HMidAnchor);
} else if (hAnchor == "left") {
button->setTextAlign(HorizontalAnchor::LeftAnchor);
} else {
throw WidgetParserException(strf(
"Malformed gui json, expected textAlign to be one of \"left\", \"right\", or \"center\", got %s", hAnchor));
}
if (config.contains("fontSize"))
button->setFontSize(config.getInt("fontSize"));
if (config.contains("fontColor"))
button->setFontColor(jsonToColor(config.get("fontColor")));
if (config.contains("fontColorDisabled"))
button->setFontColorDisabled(jsonToColor(config.get("fontColorDisabled")));
if (config.contains("disabled"))
button->setEnabled(!config.getBool("disabled"));
return WidgetConstructResult(button, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::imageHandler(String const& name, Json const& config) {
auto image = make_shared<ImageWidget>();
common(image, config);
if (config.contains("file"))
image->setImage(config.getString("file"));
if (config.contains("drawables"))
image->setDrawables(config.getArray("drawables").transformed([](Json const& d) { return Drawable(d); } ));
if (config.contains("scale"))
image->setScale(config.getFloat("scale"));
if (config.contains("rotation"))
image->setRotation(config.getFloat("rotation"));
if (config.contains("centered"))
image->setCentered(config.getBool("centered"));
if (config.contains("trim"))
image->setTrim(config.getBool("trim"));
else
image->setTrim(image->centered()); // once upon a time in a magical kingdom, these settings were linked
if (config.contains("offset"))
image->setOffset(jsonToVec2I(config.get("offset")));
if (config.contains("maxSize"))
image->setMaxSize(jsonToVec2I(config.get("maxSize")));
if (config.contains("minSize"))
image->setMinSize(jsonToVec2I(config.get("minSize")));
return WidgetConstructResult(image, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::imageStretchHandler(String const& name, Json const& config) {
ImageStretchSet stretchSet = parseImageStretchSet(config.get("stretchSet"));
GuiDirection direction = GuiDirectionNames.getLeft(config.getString("direction", "horizontal"));
auto imageStretch = make_shared<ImageStretchWidget>(stretchSet, direction);
common(imageStretch, config);
return WidgetConstructResult(imageStretch, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::spinnerHandler(String const& name, Json const& config) {
auto container = make_shared<Widget>();
common(container, config);
String callback = config.getString("callback", name);
if (!m_callbacks.contains(callback + ".up"))
throw WidgetParserException::format("Failed to find spinner callback named: '%s.up'", name);
if (!m_callbacks.contains(callback + ".down"))
throw WidgetParserException::format("Failed to find spinner callback named: '%s.down'", name);
WidgetCallbackFunc callbackDown = m_callbacks.get(callback + ".down");
WidgetCallbackFunc callbackUp = m_callbacks.get(callback + ".up");
auto assets = Root::singleton().assets();
auto imgMetadata = Root::singleton().imageMetadataDatabase();
auto leftBase = assets->json("/interface.config:spinner.leftBase").toString();
auto leftHover = assets->json("/interface.config:spinner.leftHover").toString();
auto rightBase = assets->json("/interface.config:spinner.rightBase").toString();
auto rightHover = assets->json("/interface.config:spinner.rightHover").toString();
auto imageSize = imgMetadata->imageSize(leftBase);
auto padding = assets->json("/interface.config:spinner.defaultPadding").toInt();
float upOffset = config.getFloat("upOffset", (float)imageSize[0] + padding);
auto down = make_shared<ButtonWidget>(
callbackDown, config.getString("leftBase", leftBase), config.getString("leftHover", leftHover));
auto up = make_shared<ButtonWidget>(
callbackUp, config.getString("rightBase", rightBase), config.getString("rightHover", rightHover));
up->setPosition(up->position() + Vec2I(upOffset, 0));
container->addChild("down", down);
container->addChild("up", up);
container->disableScissoring();
container->markAsContainer();
container->determineSizeFromChildren();
return WidgetConstructResult(container, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::radioGroupHandler(String const& name, Json const& config) {
auto buttonGroup = make_shared<ButtonGroupWidget>();
common(buttonGroup, config);
buttonGroup->markAsContainer();
buttonGroup->disableScissoring();
buttonGroup->setToggle(config.getBool("toggleMode", false));
String callback = config.getString("callback", name);
JsonArray buttons;
try {
buttons = config.getArray("buttons");
} catch (MapException const& e) {
throw WidgetParserException(
strf("Malformed gui json, missing a required value in the map. %s", outputException(e, false)));
}
if (!m_callbacks.contains(callback))
throw WidgetParserException::format("Failed to find callback named: '%s'", callback);
String baseImage = config.getString("baseImage", "");
String hoverImage = config.getString("hoverImage", "");
String baseImageChecked = config.getString("baseImageChecked", "");
String hoverImageChecked = config.getString("hoverImageChecked", "");
String pressedImage = config.getString("pressedImage", "");
String disabledImage = config.getString("disabledImage", "");
String pressedImageChecked = config.getString("pressedImageChecked", "");
String disabledImageChecked = config.getString("disabledImageChecked", "");
for (auto btnConfig : buttons) {
try {
auto overlayImage = btnConfig.getString("image", "");
auto id = btnConfig.getInt("id", ButtonGroup::NoButton);
auto button = make_shared<ButtonWidget>();
button->setButtonGroup(buttonGroup, id);
button->setImages(btnConfig.getString("baseImage", baseImage),
btnConfig.getString("hoverImage", hoverImage),
btnConfig.getString("pressedImage", pressedImage),
btnConfig.getString("disabledImage", disabledImage));
button->setCheckedImages(btnConfig.getString("baseImageChecked", baseImageChecked),
btnConfig.getString("hoverImageChecked", hoverImageChecked),
btnConfig.getString("pressedImageChecked", pressedImageChecked),
btnConfig.getString("hoverImageChecked", disabledImageChecked));
button->setOverlayImage(overlayImage);
if (btnConfig.getBool("disabled", false))
button->disable();
if (btnConfig.getBool("selected", false))
button->check();
if (btnConfig.contains("fontSize"))
button->setFontSize(btnConfig.getInt("fontSize"));
if (btnConfig.contains("fontColor"))
button->setFontColor(jsonToColor(btnConfig.get("fontColor")));
if (btnConfig.contains("fontColorChecked"))
button->setFontColorChecked(jsonToColor(btnConfig.get("fontColorChecked")));
if (btnConfig.contains("fontColorDisabled"))
button->setFontColorDisabled(jsonToColor(btnConfig.get("fontColorDisabled")));
if (btnConfig.contains("text"))
button->setText(btnConfig.getString("text"));
if (btnConfig.contains("pressedOffset"))
button->setPressedOffset(jsonToVec2I(btnConfig.get("pressedOffset")));
common(button, btnConfig);
buttonGroup->addChild(strf("%d", button->buttonGroupId()), button);
} catch (MapException const& e) {
throw WidgetParserException(
strf("Malformed gui json, missing a required value in the map. %s", outputException(e, false)));
}
}
// Set callback after all other buttons are loaded, to avoid callbacks being
// called during reading.
buttonGroup->setCallback(m_callbacks.get(callback));
return WidgetConstructResult(buttonGroup, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::portraitHandler(String const& name, Json const& config) {
auto portrait = make_shared<PortraitWidget>();
if (config.contains("portraitMode"))
portrait->setMode(PortraitModeNames.getLeft(config.getString("portraitMode")));
portrait->setScale(config.getFloat("scale", 1));
common(portrait, config);
return WidgetConstructResult(portrait, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::textboxHandler(String const& name, Json const& config) {
String callback = config.getString("callback", name);
if (!m_callbacks.contains(callback))
throw WidgetParserException::format("Failed to find textbox callback named: '%s'", name);
WidgetCallbackFunc callbackFunc = m_callbacks.get(callback);
String initialText = config.getString("value", "");
String hintText = config.getString("hint", "");
auto textbox = make_shared<TextBoxWidget>(initialText, hintText, callbackFunc);
if (config.contains("blur"))
textbox->setOnBlurCallback(m_callbacks.get(config.getString("blur")));
if (config.contains("enterKey"))
textbox->setOnEnterKeyCallback(m_callbacks.get(config.getString("enterKey")));
if (config.contains("escapeKey"))
textbox->setOnEscapeKeyCallback(m_callbacks.get(config.getString("escapeKey")));
if (config.contains("nextFocus"))
textbox->setNextFocus(config.getString("nextFocus"));
if (config.contains("prevFocus"))
textbox->setPrevFocus(config.getString("prevFocus"));
String hAnchor = config.getString("textAlign", "left");
if (hAnchor == "right") {
textbox->setTextAlign(HorizontalAnchor::RightAnchor);
} else if (hAnchor == "center") {
textbox->setTextAlign(HorizontalAnchor::HMidAnchor);
} else if (hAnchor != "left") {
throw WidgetParserException(strf(
"Malformed gui json, expected textAlign to be one of \"left\", \"right\", or \"center\", got %s", hAnchor));
}
if (config.contains("fontSize"))
textbox->setFontSize(config.getInt("fontSize"));
if (config.contains("color"))
textbox->setColor(jsonToColor(config.get("color")));
if (config.contains("border"))
textbox->setDrawBorder(config.getBool("border"));
if (config.contains("maxWidth"))
textbox->setMaxWidth(config.getInt("maxWidth"));
if (config.contains("regex"))
textbox->setRegex(config.getString("regex"));
common(textbox, config);
return WidgetConstructResult(textbox, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::labelHandler(String const& name, Json const& config) {
String text = config.getString("value", "");
Color color = Color::White;
if (config.contains("color"))
color = jsonToColor(config.get("color"));
HorizontalAnchor hAnchor = HorizontalAnchorNames.getLeft(config.getString("hAnchor", "left"));
VerticalAnchor vAnchor = VerticalAnchorNames.getLeft(config.getString("vAnchor", "bottom"));
auto label = make_shared<LabelWidget>(text, color, hAnchor, vAnchor);
common(label, config);
if (config.contains("fontSize"))
label->setFontSize(config.getInt("fontSize"));
if (config.contains("wrapWidth"))
label->setWrapWidth(config.getInt("wrapWidth"));
if (config.contains("charLimit"))
label->setTextCharLimit(config.getInt("charLimit"));
if (config.contains("lineSpacing"))
label->setLineSpacing(config.getFloat("lineSpacing"));
if (config.contains("directives"))
label->setDirectives(config.getString("directives"));
return WidgetConstructResult(label, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::itemSlotHandler(String const& name, Json const& config) {
String backingImage = config.getString("backingImage", "");
String callback = config.getString("callback", name);
String rightClickCallback;
if (callback.equals("null"))
rightClickCallback = callback;
else
rightClickCallback = callback + ".right";
rightClickCallback = config.getString("rightClickCallback", rightClickCallback);
auto itemSlot = make_shared<ItemSlotWidget>(ItemPtr(), backingImage);
if (!m_callbacks.contains(callback))
throw WidgetParserException::format("Failed to find itemSlot callback named: '%s'", callback);
itemSlot->setCallback(m_callbacks.get(callback));
if (!m_callbacks.contains(rightClickCallback))
throw WidgetParserException::format("Failed to find itemslot rightClickCallback named: '%s'", rightClickCallback);
itemSlot->setRightClickCallback(m_callbacks.get(rightClickCallback));
itemSlot->setBackingImageAffinity(config.getBool("showBackingImageWhenFull", false), config.getBool("showBackingImageWhenEmpty", true));
itemSlot->showDurability(config.getBool("showDurability", false));
itemSlot->showCount(config.getBool("showCount", true));
itemSlot->showRarity(config.getBool("showRarity", true));
common(itemSlot, config);
return WidgetConstructResult(itemSlot, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::itemGridHandler(String const& name, Json const& config) {
Vec2I dimensions;
Vec2I rowSpacing;
Vec2I columnSpacing;
try {
dimensions = jsonToVec2I(config.get("dimensions"));
if (config.contains("spacing")) {
auto spacing = jsonToVec2I(config.get("spacing"));
rowSpacing = {spacing[0], 0};
columnSpacing = {0, spacing[1]};
} else {
rowSpacing = jsonToVec2I(config.get("rowSpacing"));
columnSpacing = jsonToVec2I(config.get("columnSpacing"));
}
} catch (MapException const& e) {
throw WidgetParserException::format("Malformed gui json, missing a required value in the map. %s", outputException(e, false));
}
String backingImage = config.getString("backingImage", "");
String callback = config.getString("callback", name);
String rightClickCallback;
if (callback.equals("null"))
rightClickCallback = callback;
else
rightClickCallback = callback + ".right";
rightClickCallback = config.getString("rightClickCallback", rightClickCallback);
unsigned slotOffset = config.getInt("slotOffset", 0);
auto itemGrid = make_shared<ItemGridWidget>(ItemBagConstPtr(), dimensions, rowSpacing, columnSpacing, backingImage, slotOffset);
if (!m_callbacks.contains(callback))
throw WidgetParserException::format("Failed to find itemgrid callback named: '%s'", callback);
itemGrid->setCallback(m_callbacks.get(callback));
itemGrid->setBackingImageAffinity(
config.getBool("showBackingImageWhenFull", false), config.getBool("showBackingImageWhenEmpty", true));
itemGrid->showDurability(config.getBool("showDurability", false));
if (!m_callbacks.contains(rightClickCallback))
throw WidgetParserException::format("Failed to find itemgrid rightClickCallback named: '%s'", rightClickCallback);
itemGrid->setRightClickCallback(m_callbacks.get(rightClickCallback));
common(itemGrid, config);
return WidgetConstructResult(itemGrid, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::listHandler(String const& name, Json const& config) {
Json schema;
try {
schema = config.get("schema");
} catch (MapException const& e) {
throw WidgetParserException(
strf("Malformed gui json, missing a required value in the map. %s", outputException(e, false)));
}
auto list = make_shared<ListWidget>(schema);
common(list, config);
if (auto callback = m_callbacks.value(config.getString("callback", name)))
list->setCallback(callback);
list->setFillDown(config.getBool("fillDown", false));
list->setColumns(config.getUInt("columns", 1));
return WidgetConstructResult(list, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::sliderHandler(String const& name, Json const& config) {
try {
auto grid = config.getString("gridImage");
auto slider = make_shared<SliderBarWidget>(grid, config.getBool("showSpinner", true));
common(slider, config);
if (auto callback = m_callbacks.value(config.getString("callback", name)))
slider->setCallback(callback);
if (config.contains("range")) {
auto rangeConfig = config.getArray("range");
slider->setRange(rangeConfig.get(0).toInt(), rangeConfig.get(1).toInt(), rangeConfig.get(2).toInt());
}
if (config.contains("jogImages")) {
auto jogConfig = config.get("jogImages");
slider->setJogImages(jogConfig.getString("baseImage"),
jogConfig.getString("hoverImage", ""),
jogConfig.getString("pressedImage", ""),
jogConfig.getString("disabledImage", ""));
}
if (config.contains("disabled"))
slider->setEnabled(!config.getBool("disabled"));
return WidgetConstructResult(slider, name, config.getFloat("zlevel", 0));
} catch (MapException const& e) {
throw WidgetParserException::format(
"Malformed gui json, missing a required value in the map. %s", outputException(e, false));
}
}
WidgetConstructResult WidgetParser::largeCharPlateHandler(String const& name, Json const& config) {
String callback = config.getString("callback", name);
if (!m_callbacks.contains(callback))
throw WidgetParserException::format("Failed to find callback named: '%s'", name);
auto charPlate = make_shared<LargeCharPlateWidget>(m_callbacks.get(callback));
common(charPlate, config);
return WidgetConstructResult(charPlate, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::tabSetHandler(String const& name, Json const& config) {
TabSetConfig tabSetConfig;
tabSetConfig.tabButtonBaseImage = config.getString("tabButtonBaseImage");
tabSetConfig.tabButtonHoverImage = config.getString("tabButtonHoverImage");
tabSetConfig.tabButtonPressedImage = config.getString("tabButtonPressedImage", tabSetConfig.tabButtonHoverImage);
tabSetConfig.tabButtonBaseImageSelected =
config.getString("tabButtonBaseImageSelected", tabSetConfig.tabButtonBaseImage);
tabSetConfig.tabButtonHoverImageSelected =
config.getString("tabButtonHoverImageSelected", tabSetConfig.tabButtonHoverImage);
tabSetConfig.tabButtonPressedImageSelected =
config.getString("tabButtonPressedImageSelected", tabSetConfig.tabButtonHoverImageSelected);
Json defaultPressedOffset = Root::singleton().assets()->json("/interface.config:buttonPressedOffset");
tabSetConfig.tabButtonPressedOffset = jsonToVec2I(config.get("tabButtonPressedOffset", defaultPressedOffset));
tabSetConfig.tabButtonTextOffset = config.opt("tabButtonTextOffset").apply(jsonToVec2I).value();
tabSetConfig.tabButtonSpacing = config.opt("tabButtonSpacing").apply(jsonToVec2I).value();
auto tabSet = make_shared<TabSetWidget>(tabSetConfig);
common(tabSet, config);
try {
for (auto entry : config.get("tabs").iterateArray()) {
auto widget = make_shared<Widget>();
constructImpl(entry.get("children"), widget.get());
widget->determineSizeFromChildren();
tabSet->addTab(entry.getString("tabName"), widget, entry.getString("tabTitle"));
}
} catch (JsonException const& e) {
throw WidgetParserException(strf("Malformed gui json. %s", outputException(e, false)));
}
return WidgetConstructResult(tabSet, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::widgetHandler(String const& name, Json const& config) {
auto widget = make_shared<Widget>();
common(widget, config);
return WidgetConstructResult(widget, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::containerHandler(String const& name, Json const& config) {
auto widget = widgetHandler(name, config);
widget.obj->disableScissoring();
widget.obj->markAsContainer();
return widget;
}
WidgetConstructResult WidgetParser::layoutHandler(String const& name, Json const& config) {
String type;
try {
type = config.getString("layoutType");
} catch (JsonException const&) {
throw WidgetParserException("Failed to find layout type. Options are: \"basic\", \"flow\", \"vertical\".");
}
WidgetPtr widget;
if (type == "flow") {
widget = make_shared<FlowLayout>();
auto flow = convert<FlowLayout>(widget);
try {
flow->setSpacing(jsonToVec2I(config.get("spacing")));
} catch (JsonException const& e) {
throw WidgetParserException(strf("Parameter \"spacing\" in FlowLayout specification is invalid: %s.", outputException(e, false)));
}
} else if (type == "vertical") {
widget = make_shared<VerticalLayout>();
auto vert = convert<VerticalLayout>(widget);
vert->setHorizontalAnchor(HorizontalAnchorNames.getLeft(config.getString("hAnchor", "left")));
vert->setVerticalAnchor(VerticalAnchorNames.getLeft(config.getString("vAnchor", "top")));
vert->setVerticalSpacing(config.getInt("spacing", 0));
vert->setFillDown(config.getBool("fillDown", false));
} else if (type == "basic") {
widget = make_shared<Layout>();
} else {
throw WidgetParserException(strf("Invalid layout type \"%s\". Options are \"basic\", \"flow\", \"vertical\".", type));
}
common(widget, config);
if (config.contains("children"))
constructImpl(config.get("children"), widget.get());
widget->update();
return WidgetConstructResult(widget, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::canvasHandler(String const& name, Json const& config) {
auto canvas = make_shared<CanvasWidget>();
canvas->setCaptureKeyboardEvents(config.getBool("captureKeyboardEvents", false));
canvas->setCaptureMouseEvents(config.getBool("captureMouseEvents", false));
common(canvas, config);
return WidgetConstructResult(canvas, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::fuelGaugeHandler(String const& name, Json const& config) {
auto fuelGauge = make_shared<FuelWidget>();
common(fuelGauge, config);
return WidgetConstructResult(fuelGauge, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::progressHandler(String const& name, Json const& config) {
String background, overlay;
ImageStretchSet progressSet;
background = config.get("background", "").toString();
overlay = config.get("overlay", "").toString();
progressSet = parseImageStretchSet(config.get("progressSet"));
GuiDirection direction = GuiDirectionNames.getLeft(config.getString("direction", "horizontal"));
auto progress = make_shared<ProgressWidget>(background, overlay, progressSet, direction);
common(progress, config);
if (config.contains("barColor"))
progress->setColor(jsonToColor(config.get("barColor")));
if (config.contains("max"))
progress->setMaxProgressLevel(config.getFloat("max"));
if (config.contains("initial"))
progress->setCurrentProgressLevel(config.getFloat("initial"));
return WidgetConstructResult(progress, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::stackHandler(String const& name, Json const& config) {
auto stack = make_shared<StackWidget>();
if (config.contains("stack")) {
auto stackList = config.getArray("stack");
for (auto widgetCfg : stackList) {
auto widget = make_shared<Widget>();
constructImpl(widgetCfg, widget.get());
widget->determineSizeFromChildren();
stack->addChild(strf("%d", stack->numChildren()), widget);
}
}
stack->determineSizeFromChildren();
common(stack, config);
return WidgetConstructResult(stack, name, config.getFloat("zlevel", 0));
}
WidgetConstructResult WidgetParser::scrollAreaHandler(String const& name, Json const& config) {
auto scrollArea = make_shared<ScrollArea>();
if (config.contains("buttons"))
scrollArea->setButtonImages(config.get("buttons"));
if (config.contains("thumbs"))
scrollArea->setThumbImages(config.get("thumbs"));
if (config.contains("children"))
constructImpl(config.get("children"), scrollArea.get());
if (config.contains("horizontalScroll"))
scrollArea->setHorizontalScroll(config.getBool("horizontalScroll"));
if (config.contains("verticalScroll"))
scrollArea->setVerticalScroll(config.getBool("verticalScroll"));
common(scrollArea, config);
return WidgetConstructResult(scrollArea, name, config.getFloat("zlevel", 0));
}
void WidgetParser::common(WidgetPtr widget, Json const& config) {
if (config.contains("rect")) {
auto rect = jsonToRectI(config.get("rect"));
widget->setPosition(rect.min());
widget->setSize(rect.size());
} else {
if (config.contains("size")) {
widget->setSize(jsonToVec2I(config.get("size")));
}
if (config.contains("position")) {
widget->setPosition(jsonToVec2I(config.get("position")));
}
}
if (config.contains("visible"))
widget->setVisibility(config.getBool("visible"));
if (config.getBool("focus", false))
widget->focus();
if (config.contains("data"))
widget->setData(config.get("data"));
if (!config.getBool("scissoring", true))
widget->disableScissoring();
widget->setMouseTransparent(config.getBool("mouseTransparent", false));
}
ImageStretchSet WidgetParser::parseImageStretchSet(Json const& config) {
ImageStretchSet res;
res.begin = config.get("begin", "").toString();
res.inner = config.get("inner", "").toString();
res.end = config.get("end", "").toString();
String type = config.get("type", "stretch").toString();
if (type == "repeat") {
res.type = ImageStretchSet::ImageStretchType::Repeat;
} else if (type == "stretch") {
res.type = ImageStretchSet::ImageStretchType::Stretch;
} else {
throw WidgetParserException(strf("Could not parse Image Stretch Set, unknown type: %s"));
}
return res;
}
}

View file

@ -0,0 +1,76 @@
#ifndef STAR_PANE_OBJECT_PARSING_HPP
#define STAR_PANE_OBJECT_PARSING_HPP
#include "StarWidget.hpp"
namespace Star {
STAR_CLASS(Widget);
STAR_CLASS(Pane);
STAR_EXCEPTION(WidgetParserException, StarException);
struct WidgetConstructResult {
WidgetConstructResult();
WidgetConstructResult(WidgetPtr obj, String const& name, float zlevel);
WidgetPtr obj;
String name;
float zlevel;
};
typedef std::function<WidgetConstructResult(String const& name, Json const& config)> ConstuctorFunc;
class WidgetParser {
public:
WidgetParser();
virtual ~WidgetParser() {}
virtual void construct(Json const& config, Widget* widget = nullptr);
void registerCallback(String const& name, WidgetCallbackFunc callback);
WidgetPtr makeSingle(String const& name, Json const& config);
protected:
void constructImpl(Json const& config, Widget* widget);
List<WidgetConstructResult> constructor(Json const& config);
// Parents
WidgetConstructResult stackHandler(String const& name, Json const& config);
WidgetConstructResult scrollAreaHandler(String const& name, Json const& config);
// Interactive
WidgetConstructResult radioGroupHandler(String const& name, Json const& config);
WidgetConstructResult buttonHandler(String const& name, Json const& config);
WidgetConstructResult spinnerHandler(String const& name, Json const& config);
WidgetConstructResult textboxHandler(String const& name, Json const& config);
WidgetConstructResult itemSlotHandler(String const& name, Json const& config);
WidgetConstructResult itemGridHandler(String const& name, Json const& config);
WidgetConstructResult listHandler(String const& name, Json const& config);
WidgetConstructResult sliderHandler(String const& name, Json const& config);
WidgetConstructResult largeCharPlateHandler(String const& name, Json const& config);
WidgetConstructResult tabSetHandler(String const& name, Json const& config);
// Non-interactive
WidgetConstructResult widgetHandler(String const& name, Json const& config);
WidgetConstructResult imageHandler(String const& name, Json const& config);
WidgetConstructResult imageStretchHandler(String const& name, Json const& config);
WidgetConstructResult portraitHandler(String const& name, Json const& config);
WidgetConstructResult labelHandler(String const& name, Json const& config);
WidgetConstructResult canvasHandler(String const& name, Json const& config);
WidgetConstructResult fuelGaugeHandler(String const& name, Json const& config);
WidgetConstructResult progressHandler(String const& name, Json const& config);
WidgetConstructResult containerHandler(String const& name, Json const& config);
WidgetConstructResult layoutHandler(String const& name, Json const& config);
// Utilities
void common(WidgetPtr widget, Json const& config);
ImageStretchSet parseImageStretchSet(Json const& config);
Pane* m_pane;
StringMap<ConstuctorFunc> m_constructors;
StringMap<WidgetCallbackFunc> m_callbacks;
};
}
#endif