diff --git a/src/client/gui/Element.cpp b/src/client/gui/Element.cpp index 33a73d93..714cbc11 100644 --- a/src/client/gui/Element.cpp +++ b/src/client/gui/Element.cpp @@ -17,46 +17,56 @@ void Gui::Element::setStyle(StyleRule style, const std::any& value) { props.styles.rules[style] = value; } -ivec2 Gui::Element::getComputedSize() { - let size = getStyle(StyleRule::SIZE, ivec2(-1)); - if (size.x == -1) size.x = std::max(layoutSize.x, 0); - if (size.y == -1) size.y = std::max(layoutSize.y, 0); - return size; -} - void Gui::Element::clear() { children.clear(); } -ivec2 Gui::Element::getComputedOuterSize() { +void Gui::Element::onClick(const std::function& cb) { + clickCb = cb; +} + +Gui::ExpressionInfo Gui::Element::getExpr() const { + return { + parent ? parent->getComputedSize() : ivec2 {}, + getComputedSize() + }; +} + +ivec2 Gui::Element::getComputedSize() const { + let size = getStyleWithExpr(StyleRule::SIZE, vec2(nanf("")), + { parent ? parent->getComputedSize() : ivec2 {}, {} }); + if (std::isnan(size.x)) size.x = std::max(layoutSize.x, 0); + if (std::isnan(size.y)) size.y = std::max(layoutSize.y, 0); + return size; +} + +ivec2 Gui::Element::getComputedOuterSize() const { let size = getComputedSize(); let margin = getStyle(StyleRule::MARGIN, {}); return ivec2 { size.x + margin.x + margin.z, size.y + margin.y + margin.w }; } -ivec2 Gui::Element::getComputedContentSize() { +ivec2 Gui::Element::getComputedContentSize() const { let size = getComputedSize(); let padding = getStyle(StyleRule::PADDING, {}); return glm::max(ivec2 { size.x - padding.x - padding.z, size.y - padding.y - padding.w }, 0); } -ivec2 Gui::Element::getExplicitSize() { +ivec2 Gui::Element::getExplicitSize() const { return getStyle(StyleRule::SIZE, ivec2(-1)); } -ivec2 Gui::Element::getComputedPos() { +ivec2 Gui::Element::getComputedPos() const { return getStyle(StyleRule::POS, layoutPosition); } -ivec2 Gui::Element::getComputedScreenPos() { +ivec2 Gui::Element::getComputedScreenPos() const { return getComputedPos() + parentOffset; } bool Gui::Element::handleMouseHover(ivec2 mousePos, bool& pointer) { bool childIntersects = false; - for (let& child : children) - if (child->handleMouseHover(mousePos, pointer)) - childIntersects = true; + for (let& child : children) if (child->handleMouseHover(mousePos, pointer)) childIntersects = true; if (childIntersects) { if (hovered) { @@ -81,9 +91,17 @@ bool Gui::Element::handleMouseHover(ivec2 mousePos, bool& pointer) { return intersects; } -bool Gui::Element::handleMouseClick(u32 button, bool down) { - for (let& child: children) if (child->handleMouseClick(button, down)) return true; - return false; +bool Gui::Element::handleMouseClick(ivec2 mousePos, u32 button, bool down) { + for (let& child : children) if (child->handleMouseClick(mousePos, button, down)) return true; + + ivec2 size = getComputedSize(); + ivec2 pos = getComputedScreenPos(); + bool intersects = mousePos.x >= pos.x && mousePos.x <= pos.x + size.x && + mousePos.y >= pos.y && mousePos.y <= pos.y + size.y; + if (!intersects) return false; + + if (clickCb) clickCb(button, down); + return clickCb != nullptr; } void Gui::Element::draw(Renderer& renderer) { @@ -160,8 +178,6 @@ void Gui::Element::layoutChildren() { /** * The amount of size each implicitly sized element should occupy. */ - -// std::cout << selfSize << ": " << (selfSize[primary] - explicitSize) << std::endl; i32 implicitElemSize = floor((selfSize[primary] - explicitSize) / (std::max)(implicitCount, 1)); @@ -193,10 +209,10 @@ void Gui::Element::layoutChildren() { + gap + childMargin[primary] + childMargin[primary + 2]; child->parentOffset = selfOffset; - + child->updateElement(); } break; } } -} +} \ No newline at end of file diff --git a/src/client/gui/Element.h b/src/client/gui/Element.h index b68c1de0..5145dea4 100644 --- a/src/client/gui/Element.h +++ b/src/client/gui/Element.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "client/gui/Gui.h" #include "client/gui/Style.h" @@ -20,7 +21,9 @@ namespace Gui { */ class Element { + friend class Root; friend class BoxElement; + friend class TextElement; public: struct Props {; @@ -81,23 +84,25 @@ namespace Gui { void clear(); + void onClick(const std::function& cb); + /** Returns the element's computed size. */ - virtual ivec2 getComputedSize(); + virtual ivec2 getComputedSize() const; /** Returns the element's computed size + margins. */ - virtual ivec2 getComputedOuterSize(); + virtual ivec2 getComputedOuterSize() const; /** Returns the element's computed content size, which is its size - padding. */ - virtual ivec2 getComputedContentSize(); + virtual ivec2 getComputedContentSize() const; /** Returns the element's explicit size. Unspecified dimensions are -1. */ - virtual ivec2 getExplicitSize(); + virtual ivec2 getExplicitSize() const; /** Returns the element's computed position relative to its parent. */ - virtual ivec2 getComputedPos(); + virtual ivec2 getComputedPos() const; /** Returns the element's computed position relative to the screen. */ - virtual ivec2 getComputedScreenPos(); + virtual ivec2 getComputedScreenPos() const; /** Gets a style value from the element's styles or the root's stylesheets. */ const optional getStyle(StyleRule rule) const { @@ -114,8 +119,10 @@ namespace Gui { return std::nullopt; } - /** Gets a style value from the element's styles or the root's stylesheets. */ - template + /** Gets a generic value from the element's styles or the root's stylesheets. */ + template = true> + const optional getStyle(StyleRule rule) const { const optional opt = props.styles.get(rule); if (opt) return *opt; @@ -129,14 +136,67 @@ namespace Gui { } return std::nullopt; } + + /** Gets a LENGTH value from the element's styles or the root's stylesheets. */ + template = true> + + const optional getStyle(StyleRule rule) const { + ExpressionInfo info = getExpr(); + const optional opt = props.styles.get(rule, info); + if (opt) return *opt; + for (const let& ss : stylesheets) { + for (const string& className : props.classes) { + const let& styles = ss.find(className); + if (styles == ss.end()) continue; + const optional opt = styles->second.get(rule, info); + if (opt) return *opt; + } + } + return std::nullopt; + } + + /** Gets a LENGTH value from the element's styles or the root's stylesheets. */ + template = true> + + const optional getStyleWithExpr(StyleRule rule, const ExpressionInfo& expr) const { + const optional opt = props.styles.get(rule, expr); + if (opt) return *opt; + for (const let& ss : stylesheets) { + for (const string& className : props.classes) { + const let& styles = ss.find(className); + if (styles == ss.end()) continue; + const optional opt = styles->second.get(rule, expr); + if (opt) return *opt; + } + } + return std::nullopt; + } /** Gets a style value from the element's styles or the root's stylesheets. */ template + const V getStyle(StyleRule rule, V def) const { const optional opt = getStyle(rule); if (opt) return *opt; return def; } + + /** Gets a LENGTH value from the element's styles or the root's stylesheets, with a custom ExpressionInfo. */ + template = true> + + const V getStyleWithExpr(StyleRule rule, V def, const ExpressionInfo& info) const { + const optional opt = getStyleWithExpr(rule, info); + if (opt) return *opt; + return def; + } + + protected: + + /** Returns an ExpressionInfo object for evaluating Lengths. */ + virtual ExpressionInfo getExpr() const; /** * Called by the root when the mouse position changes. @@ -150,9 +210,8 @@ namespace Gui { * Triggers a click interaction on the hovered element. */ - bool handleMouseClick(u32 button, bool down); + bool handleMouseClick(ivec2 mousePos, u32 button, bool down); - protected: Root& root; Props props; vec& stylesheets; @@ -162,6 +221,7 @@ namespace Gui { std::list> children; bool hovered = false; + std::function clickCb = nullptr; /** The screen offset of the parent. */ ivec2 parentOffset {}; diff --git a/src/client/gui/Expression.cpp b/src/client/gui/Expression.cpp index 5713343d..9355e373 100644 --- a/src/client/gui/Expression.cpp +++ b/src/client/gui/Expression.cpp @@ -31,14 +31,8 @@ void Gui::Expression::setExpression(string exp) { while (exp.size()) { let& c = exp[0]; - // Number or Unit or Keyword - if ((c >= '0' && c <= '9') || c == '.' || (c >= 97 && c <= 122) || - (nextOperatorIsUnary && (c == '+' || c == '-'))) { - temp += c; - nextOperatorIsUnary = false; - } // Binary Operator - else if (!nextOperatorIsUnary && (c == '+' || c == '-' || c == '*' || c == '/' || c == '^')) { + if (!nextOperatorIsUnary && (c == '+' || c == '-' || c == '*' || c == '/' || c == '^')) { if (temp.size()) { queue.emplace(temp); temp = {}; @@ -81,6 +75,11 @@ void Gui::Expression::setExpression(string exp) { operators.pop(); nextOperatorIsUnary = false; } + // Number or Unit or Keyword + else { + temp += c; + nextOperatorIsUnary = false; + } exp.erase(0, 1); } @@ -105,7 +104,11 @@ void Gui::Expression::setExpression(string exp) { } } -f32 Gui::Expression::eval() { +f32 Gui::Expression::eval(const ExpressionInfo& info) { + if (!expression.size()) { + return nanf(""); + } + std::stack eval {}; for (usize i = 0; i < expression.size(); i++) { @@ -124,28 +127,28 @@ f32 Gui::Expression::eval() { switch (t.unit) { default: - throw std::logic_error("Tried to operate with a non-operator token."); + throw std::logic_error("Tried to operate with a non-operator token! This is an engine error!"); case UnitOrOperator::ADD: - eval.emplace(a.evalValue() + b.evalValue(), UnitOrOperator::REAL_PIXEL); + eval.emplace(a.eval(info) + b.eval(info), UnitOrOperator::RAW); break; case UnitOrOperator::SUBTRACT: - eval.emplace(a.evalValue() - b.evalValue(), UnitOrOperator::REAL_PIXEL); + eval.emplace(a.eval(info) - b.eval(info), UnitOrOperator::RAW); break; case UnitOrOperator::MULTIPLY: - eval.emplace(a.evalValue() * b.evalValue(), UnitOrOperator::REAL_PIXEL); + eval.emplace(a.eval(info) * b.eval(info), UnitOrOperator::RAW); break; case UnitOrOperator::DIVIDE: - eval.emplace(a.evalValue() / b.evalValue(), UnitOrOperator::REAL_PIXEL); + eval.emplace(a.eval(info) / b.eval(info), UnitOrOperator::RAW); break; case UnitOrOperator::EXPONENT: - eval.emplace(pow(a.evalValue(), b.evalValue()), UnitOrOperator::REAL_PIXEL); + eval.emplace(pow(a.eval(info), b.eval(info)), UnitOrOperator::RAW); break; } } } if (!eval.size()) throw std::runtime_error("Eval stack is empty! This is an engine error!"); - return eval.top().evalValue(); + return eval.top().eval(info); } const std::unordered_map Gui::Expression::PRECEDENCE { @@ -178,9 +181,13 @@ Gui::Expression::Token::Token(const string& str) { switch (Util::hash(unitStr.data())) { default: throw std::logic_error("Unknown unit '" + unitStr + "'."); - case Util::hash("dp"): unit = UnitOrOperator::DISPLAY_PIXEL; return; case Util::hash(""): - case Util::hash("px"): unit = UnitOrOperator::REAL_PIXEL; return; + case Util::hash("px"): unit = UnitOrOperator::RAW; return; + case Util::hash("dp"): unit = UnitOrOperator::DISPLAY_PIXEL; return; + case Util::hash("cw"): unit = UnitOrOperator::CONTAINER_WIDTH; return; + case Util::hash("ch"): unit = UnitOrOperator::CONTAINER_HEIGHT; return; + case Util::hash("sw"): unit = UnitOrOperator::SELF_WIDTH; return; + case Util::hash("sh"): unit = UnitOrOperator::SELF_HEIGHT; return; case Util::hash("deg"): unit = UnitOrOperator::DEGREE; return; } } @@ -189,12 +196,19 @@ bool Gui::Expression::Token::isOperator() { return static_cast(unit) >= 128; } -f32 Gui::Expression::Token::evalValue() { +f32 Gui::Expression::Token::eval(const ExpressionInfo& info) { switch (unit) { - default: throw std::logic_error("Tried to evalValue() on an Operator token."); + default: throw std::logic_error("Tried to eval() on an Operator token! This is an engine error!"); case UnitOrOperator::DISPLAY_PIXEL: return val * Gui::PX_SCALE; - case UnitOrOperator::REAL_PIXEL: return val; + case UnitOrOperator::RAW: return val; + case UnitOrOperator::CONTAINER_WIDTH: { +// std::cout << info.containerSize << ":" << val << std::endl; + return (val / 100.f) * info.containerSize.x; + } + case UnitOrOperator::CONTAINER_HEIGHT: return (val / 100.f) * info.containerSize.y; + case UnitOrOperator::SELF_WIDTH: return (val / 100.f) * info.selfSize.x; + case UnitOrOperator::SELF_HEIGHT: return (val / 100.f) * info.selfSize.y; case UnitOrOperator::DEGREE: return val * M_PI / 180.f; } } diff --git a/src/client/gui/Expression.h b/src/client/gui/Expression.h index 2f51bd7b..55c46b9e 100644 --- a/src/client/gui/Expression.h +++ b/src/client/gui/Expression.h @@ -6,19 +6,31 @@ #include "util/Types.h" namespace Gui { - enum class UnitOrOperator: u8 { - DISPLAY_PIXEL, - REAL_PIXEL, - DEGREE, + struct ExpressionInfo { + ExpressionInfo() = default; + ExpressionInfo(ivec2 containerSize, ivec2 selfSize): containerSize(containerSize), selfSize(selfSize) {} - ADD = 128, - SUBTRACT, - MULTIPLY, - DIVIDE, - EXPONENT + ivec2 containerSize; + ivec2 selfSize; }; class Expression { + enum class UnitOrOperator: u8 { + RAW, + DISPLAY_PIXEL, + CONTAINER_WIDTH, + CONTAINER_HEIGHT, + SELF_WIDTH, + SELF_HEIGHT, + DEGREE, + + ADD = 128, + SUBTRACT, + MULTIPLY, + DIVIDE, + EXPONENT + }; + struct Token { Token() = default; explicit Token(const string& str); @@ -26,7 +38,7 @@ namespace Gui { bool isOperator(); - f32 evalValue(); + f32 eval(const ExpressionInfo& info); f32 val = 0; UnitOrOperator unit; @@ -38,12 +50,12 @@ namespace Gui { void setExpression(string exp); - f32 eval(); + f32 eval(const ExpressionInfo& info); private: usize hash = 0; - std::vector expression; + vec expression; const static std::unordered_map PRECEDENCE; }; diff --git a/src/client/gui/Root.cpp b/src/client/gui/Root.cpp index 4e286c4b..1b48ae6b 100644 --- a/src/client/gui/Root.cpp +++ b/src/client/gui/Root.cpp @@ -26,7 +26,10 @@ Gui::Root::Root(Window& window, TextureAtlas& atlas) : t.printElapsedMs(); }); -// window.input.bindMouseCallback() + window.input.bindMouseCallback([&](u32 button, i32 state) { + let pos = window.input.getMousePos(); + body->handleMouseClick(pos, button, state == GLFW_PRESS); + }); } Gui::Root::~Root() { diff --git a/src/client/gui/Style.cpp b/src/client/gui/Style.cpp index 664aec38..bf856ac5 100644 --- a/src/client/gui/Style.cpp +++ b/src/client/gui/Style.cpp @@ -5,7 +5,7 @@ const std::unordered_map Gui::Style::RULE_STRINGS_TO_ENU { "size", StyleRule::SIZE }, { "margin", StyleRule::MARGIN }, { "padding", StyleRule::PADDING }, - { "GAP", StyleRule::GAP }, + { "gap", StyleRule::GAP }, { "layout", StyleRule::LAYOUT }, { "direction", StyleRule::DIRECTION }, { "h_align", StyleRule::H_ALIGN }, diff --git a/src/client/gui/Style.h b/src/client/gui/Style.h index dba67711..3eed5ac0 100644 --- a/src/client/gui/Style.h +++ b/src/client/gui/Style.h @@ -143,10 +143,10 @@ namespace Gui { (std::is_integral_v || std::is_floating_point_v) && L == ValueType::LENGTH, bool> = true> - optional get(StyleRule rule) const { + optional get(StyleRule rule, const ExpressionInfo& info) const { let raw = get(rule); if (!raw) return std::nullopt; - return raw->eval(); + return raw->eval(info); } /** @@ -159,11 +159,11 @@ namespace Gui { std::is_same_v> && L == ValueType::LENGTH, bool> = true> - optional get(StyleRule rule) const { + optional get(StyleRule rule, const ExpressionInfo& info) const { let raw = get>(rule); if (!raw) return std::nullopt; VN vec; - for (usize i = 0; i < VN::length(); i++) vec[i] = (*raw)[i].eval(); + for (usize i = 0; i < VN::length(); i++) vec[i] = (*raw)[i].eval(info); return vec; } diff --git a/src/client/menu/MenuSandbox.cpp b/src/client/menu/MenuSandbox.cpp index b449baae..d4ad6033 100644 --- a/src/client/menu/MenuSandbox.cpp +++ b/src/client/menu/MenuSandbox.cpp @@ -23,7 +23,6 @@ MenuSandbox::MenuSandbox(Client& client, Gui::Root& root, sptr san sandboxRoot(sandboxRoot) {} void MenuSandbox::reset() { -// container->remove("error"); sandboxRoot->clear(); core = {}; mod = {}; @@ -58,22 +57,12 @@ void MenuSandbox::loadApi() { void MenuSandbox::load(const SubgameDef& subgame) { reset(); subgameName = subgame.config.name; - -// try { + loadAndRunMod(subgame.subgamePath + "/../../assets/base"); loadAndRunMod(subgame.subgamePath + "/menu"); -// } -// catch (const std::runtime_error& e) { -// showError(e.what(), subgame.config.name); -// } -} - -void MenuSandbox::windowResized() { -// builder.build(win); } void MenuSandbox::update(double delta) { -// builder.update(); core["__builtin"]["update_delayed_functions"](); } diff --git a/src/client/menu/MenuSandbox.h b/src/client/menu/MenuSandbox.h index 37ab1178..1d6a3609 100644 --- a/src/client/menu/MenuSandbox.h +++ b/src/client/menu/MenuSandbox.h @@ -25,8 +25,6 @@ public: void update(double delta) override; - void windowResized(); - using LuaParser::update; private: diff --git a/src/client/scene/MainMenuScene.cpp b/src/client/scene/MainMenuScene.cpp index 3da4fe10..b3d919b0 100644 --- a/src/client/scene/MainMenuScene.cpp +++ b/src/client/scene/MainMenuScene.cpp @@ -87,19 +87,20 @@ MainMenuScene::MainMenuScene(Client& client) : Scene(client), for (usize i = 0; i < subgames.size(); i++) { let& subgame = subgames[i]; - navigationList->append({ + + let elem = navigationList->append({ .classes = { "navigationButton" }, .styles = {{ { Gui::StyleRule::BACKGROUND, string("crop(0, 0, 16, 16, " + subgame.iconRef->name + ")") }, { Gui::StyleRule::BACKGROUND_HOVER, string("crop(16, 0, 16, 16, " + subgame.iconRef->name + ")") } }} }); -// -// button->setCallback(Element::CallbackType::PRIMARY, [&](bool down, ivec2) { -// if (!down) return; -// selectedSubgame = &subgame; -// sandbox.load(*selectedSubgame); -// }); + + elem->onClick([&](u32 button, bool down) { + if (button != GLFW_MOUSE_BUTTON_1) return; + selectedSubgame = &subgame; + sandbox.load(*selectedSubgame); + }); } if (subgames.size() > 0) { diff --git a/src/lua/usertype/LuaGuiElement.cpp b/src/lua/usertype/LuaGuiElement.cpp index 309f6f13..9f71444b 100644 --- a/src/lua/usertype/LuaGuiElement.cpp +++ b/src/lua/usertype/LuaGuiElement.cpp @@ -7,6 +7,7 @@ #include "client/gui/TextElement.h" static const Gui::Expression parseObjectToExpr(sol::object value) { + if (!value.valid()) return Gui::Expression(""); if (value.is()) return Gui::Expression(std::to_string(value.as()) + "dp"); if (value.is()) return Gui::Expression(value.as()); throw std::invalid_argument("Object cannot be converted to an expression."); @@ -21,7 +22,8 @@ static const array parseLengthTableVal(sol::object value) { vec exprs {}; exprs.reserve(t.size()); - for (let& v : t) exprs.emplace_back(parseObjectToExpr(v.second)); + for (usize i = 1; i <= t.size(); i++) + exprs.emplace_back(parseObjectToExpr(t.get(i))); for (usize i = 0; i < arr.size() / exprs.size(); i++) for (usize j = 0; j < exprs.size(); j++) diff --git a/subgames/parentheses/menu/script/init.lua b/subgames/parentheses/menu/script/init.lua index 83c8b197..1aa13ade 100644 --- a/subgames/parentheses/menu/script/init.lua +++ b/subgames/parentheses/menu/script/init.lua @@ -6,6 +6,7 @@ zepha.set_gui(zepha.gui(function() Gui.Text { size = { 64, 4 }, + pos = { "50cw", 50 }, content = "Parentheses" } } diff --git a/subgames/zeus/menu/script/init.lua b/subgames/zeus/menu/script/init.lua index 2edda682..40227b30 100644 --- a/subgames/zeus/menu/script/init.lua +++ b/subgames/zeus/menu/script/init.lua @@ -1,52 +1,63 @@ -local menu = zepha.build_gui(function() +local menu = zepha.gui(function() return Gui.Box { background = 'zeus_background_christmas_night', Gui.Box { - key = 'particle_wrap', - size = { '100%', '100%' } +-- id = 'particle_wrap', + size = { '100cw', '100ch' } }, Gui.Box { - pos = { '20% - 50s%', 0 }, - size = { 102, '100%' }, + gap = 4, + padding = 8, + size = { 102, '100ch' }, + pos = { '20cw - 50sw', 0 }, background = '#0135', Gui.Box { - pos = { 8, 8 }, - size = { 86, 30 }, + size = { nil, 30 }, + margin = { 0, 0, 0, 8 }, background = 'zeus_logo' }, - Gui.Button { - id = 'button_play', + Gui.Box { +-- id = 'button_play', -- callbacks = { -- primary = function() zepha.start_game_local() end -- }, - pos = { 6, 50 }, - size = { 90, 20 }, + padding = 5, + size = { nil, 20 }, - content = 'Local Play', + cursor = "pointer", background = 'crop(0, 0, 90, 20, zeus_button)', - background_hover = 'crop(0, 20, 90, 20, zeus_button)' + background_hover = 'crop(0, 20, 90, 20, zeus_button)', + + Gui.Text { + content = 'Local Play' + } }, - Gui.Button { - id = 'button_servers', + Gui.Box { +-- id = 'button_servers', -- callbacks = { -- primary = function() zepha.start_game() end -- }, - pos = { 6, 74 }, - size = { 90, 20 }, - content = 'Browse Servers', + padding = 5, + size = { nil, 20 }, + + cursor = "pointer", background = 'crop(0, 0, 90, 20, zeus_button)', - background_hover = 'crop(0, 20, 90, 20, zeus_button)' + background_hover = 'crop(0, 20, 90, 20, zeus_button)', + + Gui.Text { + content = 'Browse Servers' + } } } } @@ -62,17 +73,17 @@ end) -- ) end) -- end, 1) -local particle_wrap = menu:get('particle_wrap') -menu(function() - for _ = 1, 20 do - local scale = 6 + math.random() * 4 - particle_wrap:append(Gui.Rect { - pos = { math.floor(math.random() * 600), math.floor(math.random() * 320) }, - background = 'particle_dark', - size = { scale, scale } - }) - end -end) +-- local particle_wrap = menu:get('particle_wrap') +-- menu(function() +-- for _ = 1, 20 do +-- local scale = 6 + math.random() * 4 +-- particle_wrap:append(Gui.Rect { +-- pos = { math.floor(math.random() * 600), math.floor(math.random() * 320) }, +-- background = 'particle_dark', +-- size = { scale, scale } +-- }) +-- end +-- end) -- local tick = 0 -- zepha.after(function()