Annwan
3 months ago
9 changed files with 411 additions and 129 deletions
-
2.clang-format
-
4.gitignore
-
33CMakeLists.txt
-
203src/Application.cppm
-
52src/Base.cppm
-
32src/Button.cppm
-
58src/Node.cppm
-
140src/Port.cppm
-
14src/main.cpp
@ -1,5 +1,5 @@ |
|||||
build/* |
build/* |
||||
/ffmpegraph |
/ffmpegraph |
||||
/.idea |
/.idea |
||||
/cmake-build-debug |
|
||||
/cmake-build-release |
|
||||
|
/out |
||||
|
/release |
@ -1,116 +1,203 @@ |
|||||
module; |
module; |
||||
|
#include <algorithm> |
||||
|
#include <cmath> |
||||
|
#include <memory> |
||||
|
#include <print> |
||||
|
#include <ranges> |
||||
#include <raylib.h> |
#include <raylib.h> |
||||
#include <string> |
#include <string> |
||||
|
#include <unordered_map> |
||||
|
#include <unordered_set> |
||||
#include <vector> |
#include <vector> |
||||
#include <memory> |
|
||||
#include <cmath> |
|
||||
|
#include <deque> |
||||
export module Application; |
export module Application; |
||||
import Node; |
import Node; |
||||
import Base; |
import Base; |
||||
import Port; |
|
||||
|
import Button; |
||||
namespace ffmpegraph { |
namespace ffmpegraph { |
||||
export class Application { |
export class Application { |
||||
|
|
||||
public: |
public: |
||||
explicit Application(std::string _title = "FFmpeGraph"); |
explicit Application(std::string _title = "FFmpeGraph"); |
||||
|
|
||||
~Application(); |
~Application(); |
||||
|
|
||||
void Run(); |
void Run(); |
||||
|
|
||||
protected: |
protected: |
||||
|
void Tick(); |
||||
|
|
||||
void ProcessEvents(); |
void ProcessEvents(); |
||||
void Render() const; |
|
||||
|
void OnLeftClick(); |
||||
|
void OnKeyPressed(i32 key); |
||||
|
void Render(); |
||||
|
void ExecuteGraph(); |
||||
|
std::deque<std::string> message_queue; |
||||
std::string title; |
std::string title; |
||||
bool should_close; |
bool should_close; |
||||
std::vector<std::unique_ptr<Node>> nodes; |
std::vector<std::unique_ptr<Node>> nodes; |
||||
Node* selected_node = nullptr; |
|
||||
OutputPort* selected_port = nullptr; |
|
||||
|
Node *selected_node = nullptr; |
||||
|
OutputPort *selected_port = nullptr; |
||||
|
Button run_button; |
||||
|
u64 framecounter = 0; |
||||
}; |
}; |
||||
} |
|
||||
module : private; |
|
||||
namespace ffmpegraph { |
|
||||
|
} // namespace ffmpegraph |
||||
|
|
||||
Application::Application(std::string _title): title(std::move(_title)), should_close(false) { |
|
||||
|
module :private; |
||||
|
namespace ffmpegraph { |
||||
|
Application::Application(std::string _title) |
||||
|
: title(std::move(_title)), should_close(false), run_button(0, 0, 0, 20, "Run") { |
||||
SetConfigFlags(FLAG_WINDOW_RESIZABLE); |
SetConfigFlags(FLAG_WINDOW_RESIZABLE); |
||||
InitWindow(800, 600, this->title.c_str()); |
InitWindow(800, 600, this->title.c_str()); |
||||
SetTargetFPS(60); |
SetTargetFPS(60); |
||||
|
SetExitKey(KEY_NULL); |
||||
|
|
||||
nodes.push_back(std::make_unique<InputNode>()); |
nodes.push_back(std::make_unique<InputNode>()); |
||||
|
nodes.push_back(std::make_unique<StringConstantNode>()); |
||||
nodes.push_back(std::make_unique<InputNode>()); |
nodes.push_back(std::make_unique<InputNode>()); |
||||
nodes[0]->pos_x = 10; |
|
||||
|
nodes[0]->pos_x = 400; |
||||
nodes[0]->pos_y = 10; |
nodes[0]->pos_y = 10; |
||||
nodes[1]->pos_x = 200; |
nodes[1]->pos_x = 200; |
||||
nodes[1]->pos_y = 100; |
nodes[1]->pos_y = 100; |
||||
|
run_button.width = 10 + MeasureText("Run", 10); |
||||
} |
} |
||||
|
|
||||
|
|
||||
|
void Application::Tick() { |
||||
|
if (selected_node) { |
||||
|
selected_node->pos_x = GetMouseX(); |
||||
|
selected_node->pos_y = GetMouseY(); |
||||
|
} |
||||
|
ProcessEvents(); |
||||
|
} |
||||
|
|
||||
void Application::ProcessEvents() { |
void Application::ProcessEvents() { |
||||
|
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { OnLeftClick(); } |
||||
|
if (auto key = GetKeyPressed()) { OnKeyPressed(key); } |
||||
|
} |
||||
|
|
||||
|
void Application::OnLeftClick() { |
||||
auto GetPort = [&] -> Port * { |
auto GetPort = [&] -> Port * { |
||||
for(auto& node : nodes) { |
|
||||
for (auto* port: node->ports) { |
|
||||
|
for (auto &node : nodes) { |
||||
|
for (auto *port : node->ports) { |
||||
auto distx = double(GetMouseX() - port->pos_x); |
auto distx = double(GetMouseX() - port->pos_x); |
||||
auto disty = double(GetMouseY() - port->pos_y); |
auto disty = double(GetMouseY() - port->pos_y); |
||||
if (std::sqrt(distx * distx + disty * disty) < 10) { |
|
||||
return port; |
|
||||
} |
|
||||
|
if (std::sqrt(distx * distx + disty * disty) < 10) { return port; } |
||||
} |
} |
||||
} |
} |
||||
return nullptr; |
return nullptr; |
||||
}; |
}; |
||||
|
|
||||
if (selected_node) { |
if (selected_node) { |
||||
selected_node->pos_x = GetMouseX(); |
|
||||
selected_node->pos_y = GetMouseY(); |
|
||||
} |
|
||||
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { |
|
||||
if (selected_node) { |
|
||||
selected_node = nullptr; |
|
||||
} else if (selected_port) { |
|
||||
auto port = GetPort(); |
|
||||
auto iport = dynamic_cast<InputPort*>(port); |
|
||||
if (iport) { |
|
||||
selected_port->Connect(*iport); |
|
||||
} |
|
||||
selected_port = nullptr; |
|
||||
} |
|
||||
else { |
|
||||
i32 x = GetMouseX(); |
|
||||
i32 y = GetMouseY(); |
|
||||
auto port = GetPort(); |
|
||||
auto oport = dynamic_cast<OutputPort*>(port); |
|
||||
if (oport) { |
|
||||
selected_port = oport; |
|
||||
|
selected_node = nullptr; |
||||
|
} else if (selected_port) { |
||||
|
auto port = GetPort(); |
||||
|
auto iport = dynamic_cast<InputPort *>(port); |
||||
|
if (iport) { selected_port->TryConnect(*iport); } |
||||
|
selected_port = nullptr; |
||||
|
} else if (run_button.IsHovered()) { |
||||
|
ExecuteGraph(); |
||||
|
} else { |
||||
|
i32 x = GetMouseX(); |
||||
|
i32 y = GetMouseY(); |
||||
|
auto port = GetPort(); |
||||
|
auto oport = dynamic_cast<OutputPort *>(port); |
||||
|
if (oport) { |
||||
|
if (oport->GetConnected()) { |
||||
|
oport->Disconnect(); |
||||
} else { |
} else { |
||||
for (auto& node : nodes) { |
|
||||
if (node->pos_x < x and x < node->pos_x + node->Width() |
|
||||
and node->pos_y < y and y < node->pos_y + node->Height()) { |
|
||||
selected_node = node.get(); |
|
||||
break; |
|
||||
} |
|
||||
|
selected_port = oport; |
||||
|
} |
||||
|
} else { |
||||
|
for (auto &node : nodes) { |
||||
|
if (node->pos_x < x and x < node->pos_x + node->Width() and node->pos_y < y |
||||
|
and y < node->pos_y + node->Height()) { |
||||
|
selected_node = node.get(); |
||||
|
break; |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
void Application::Render() const { |
|
||||
|
void Application::ExecuteGraph() { |
||||
|
std::unordered_map<Node *, isz> index_map; |
||||
|
std::unordered_map<isz, std::unordered_set<isz>> adj_list; |
||||
|
for (auto [i, node] : vws::enumerate(nodes)) { |
||||
|
index_map[node.get()] = i; |
||||
|
adj_list[i] = {}; |
||||
|
} |
||||
|
for (auto [i, node] : vws::enumerate(nodes)) { |
||||
|
Node *raw_node = node.get(); |
||||
|
if (auto n = dynamic_cast<InputNode *>(raw_node)) { |
||||
|
if (auto c = n->output_port.GetConnected()) { adj_list[index_map[c->owner]].insert(index_map[raw_node]); } |
||||
|
} |
||||
|
if (auto n = dynamic_cast<StringConstantNode *>(raw_node)) { |
||||
|
if (auto c = n->out.GetConnected()) { adj_list[index_map[c->owner]].insert(index_map[raw_node]); } |
||||
|
} |
||||
|
// TODO add cases for all new types of nodes |
||||
|
} |
||||
|
|
||||
|
std::vector<Node *> sorted_nodes; |
||||
|
while (not adj_list.empty()) { |
||||
|
bool changed = false; |
||||
|
for (auto &[i, incoming] : adj_list) { |
||||
|
if (incoming.empty()) { |
||||
|
sorted_nodes.push_back(nodes[usz(i)].get()); |
||||
|
for (auto &[_, thing] : adj_list) { thing.erase(i); } |
||||
|
adj_list.erase(i); |
||||
|
changed = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (not changed) { |
||||
|
message_queue.push_back("ERROR: Graph has cycle"); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
for (auto node : sorted_nodes) { |
||||
|
node->Run(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void Application::OnKeyPressed(i32 key) { |
||||
|
auto selected = dynamic_cast<StringConstantNode *>(selected_node); |
||||
|
if (not selected) return; |
||||
|
if (key == KEY_ENTER) { |
||||
|
selected_node = nullptr; |
||||
|
} else if (key == KEY_BACKSPACE) { |
||||
|
if (not selected->in.data.empty()) selected->in.data.pop_back(); |
||||
|
} else if (0x20 <= key and key <= 0x7E) { |
||||
|
if (not IsKeyDown(KEY_LEFT_SHIFT) and not IsKeyDown(KEY_RIGHT_SHIFT) and 'A' <= key and key <= 'Z') { |
||||
|
key += 32; |
||||
|
} |
||||
|
selected->in.data += char(key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void Application::Render() { |
||||
|
framecounter++; |
||||
|
if (not (framecounter%300) and not message_queue.empty()) { |
||||
|
message_queue.pop_back(); |
||||
|
} |
||||
BeginDrawing(); |
BeginDrawing(); |
||||
ClearBackground(RAYWHITE); |
ClearBackground(RAYWHITE); |
||||
if (selected_port) { |
|
||||
DrawLine(selected_port->pos_x, selected_port->pos_y, GetMouseX(), GetMouseY(), BLACK); |
|
||||
} |
|
||||
for (auto &node : nodes) { |
|
||||
node->Render(); |
|
||||
|
message_queue.resize(std::min(message_queue.size(), usz(10))); |
||||
|
i32 offset = 15; |
||||
|
for (auto& message: message_queue) { |
||||
|
DrawText(message.c_str(), 5, GetScreenHeight() - offset, 10, RED); |
||||
|
offset += 20; |
||||
} |
} |
||||
|
if (selected_port) { DrawLine(selected_port->pos_x, selected_port->pos_y, GetMouseX(), GetMouseY(), BLACK); } |
||||
|
for (auto &node : nodes) { node->Render(); } |
||||
|
run_button.Render(); |
||||
EndDrawing(); |
EndDrawing(); |
||||
|
|
||||
} |
} |
||||
|
|
||||
void Application::Run() { |
void Application::Run() { |
||||
while(not WindowShouldClose()) { |
|
||||
this->ProcessEvents(); |
|
||||
|
while (not WindowShouldClose()) { |
||||
|
this->Tick(); |
||||
this->Render(); |
this->Render(); |
||||
} |
} |
||||
} |
} |
||||
Application::~Application(){ |
|
||||
CloseWindow(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
Application::~Application() { CloseWindow(); } |
||||
|
} // namespace ffmpegraph |
@ -0,0 +1,32 @@ |
|||||
|
module; |
||||
|
#include <string> |
||||
|
#include <raylib.h> |
||||
|
export module Button; |
||||
|
import Base; |
||||
|
|
||||
|
export namespace ffmpegraph { |
||||
|
struct Button { |
||||
|
i32 pos_x, pos_y; |
||||
|
i32 width, height; |
||||
|
std::string label; |
||||
|
void Render() const; |
||||
|
bool IsHovered() const; |
||||
|
Button(i32 x, i32 y, i32 w, i32 h, std::string label): pos_x(x), pos_y(y), width(w), height(h), label(std::move(label)) {} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
module: private; |
||||
|
namespace ffmpegraph { |
||||
|
void Button::Render() const { |
||||
|
auto color = IsHovered()? LIGHTGRAY : WHITE; |
||||
|
DrawRectangle(pos_x, pos_y, width, height, color); |
||||
|
DrawRectangleLines(pos_x, pos_y, width, height, BLACK); |
||||
|
DrawText(label.c_str(), pos_x + 5, pos_y + 5, 10, BLACK); |
||||
|
} |
||||
|
|
||||
|
bool Button::IsHovered() const { |
||||
|
return pos_x <= GetMouseX() and GetMouseX() <= pos_x + width |
||||
|
and pos_y <= GetMouseY() and GetMouseY() <= pos_y + height; |
||||
|
} |
||||
|
|
||||
|
} |
@ -1,87 +1,141 @@ |
|||||
module; |
module; |
||||
#include <optional> |
|
||||
#include <string> |
|
||||
|
#include <format> |
||||
|
#include <limits> |
||||
#include <raylib.h> |
#include <raylib.h> |
||||
export module Port; |
|
||||
|
#include <string> |
||||
|
#include <variant> |
||||
|
export module Node:Port; |
||||
import Base; |
import Base; |
||||
|
|
||||
|
using namespace std::literals; |
||||
export namespace ffmpegraph { |
export namespace ffmpegraph { |
||||
|
|
||||
|
struct Node; |
||||
|
|
||||
|
enum struct PortType { |
||||
|
INT, |
||||
|
STRING, |
||||
|
}; |
||||
|
|
||||
|
constexpr i32 min_port_width = 100; |
||||
|
using PortData = std::variant<i32, std::string, std::monostate>; |
||||
|
} |
||||
|
|
||||
|
template <> struct std::formatter<ffmpegraph::PortData> : std::formatter<std::string> { |
||||
|
template <class FormatContext> auto format(ffmpegraph::PortData const& data, FormatContext& ctx) const { |
||||
|
auto repr = std::visit( |
||||
|
utils::Overloaded{ |
||||
|
[](std::monostate) { return "()"s; }, |
||||
|
[](std::string const& x) { return x; }, |
||||
|
[](i32 x) { return std::to_string(x); }, |
||||
|
}, |
||||
|
data |
||||
|
); |
||||
|
return formatter<std::string>::format(repr, ctx); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export namespace ffmpegraph { |
||||
struct Port { |
struct Port { |
||||
protected: |
protected: |
||||
std::string name; |
std::string name; |
||||
explicit Port(std::string name): name(std::move(name)) {} |
|
||||
|
explicit Port(std::string name, Node* owner) : name(std::move(name)), owner(owner) {} |
||||
virtual ~Port() = default; |
virtual ~Port() = default; |
||||
public: |
public: |
||||
i32 pos_x{}, pos_y{}; |
i32 pos_x{}, pos_y{}; |
||||
virtual void Render(i32 x, i32 y) = 0; |
|
||||
|
Node* owner; |
||||
|
// Returns the desired width for the port. |
||||
|
virtual i32 Render(i32 x, i32 y, i32 width) = 0; |
||||
}; |
}; |
||||
|
|
||||
struct OutputPort; |
struct OutputPort; |
||||
|
|
||||
struct InputPort : Port { |
struct InputPort : Port { |
||||
protected: |
protected: |
||||
std::optional<int> value = std::nullopt; |
|
||||
|
PortType type; |
||||
|
PortData value = std::monostate{}; |
||||
public: |
public: |
||||
explicit InputPort(std::string name): Port(std::move(name)) {} |
|
||||
std::optional<int> GetValue(); |
|
||||
void Render(i32 x, i32 y) override; |
|
||||
|
InputPort(std::string name, Node* owner, PortType type) : Port(std::move(name), owner), type(type) {} |
||||
|
PortData const& GetValue() const; |
||||
|
i32 Render(i32 x, i32 y, i32 width) override; |
||||
|
|
||||
friend OutputPort; |
friend OutputPort; |
||||
}; |
}; |
||||
struct OutputPort : Port { |
struct OutputPort : Port { |
||||
protected: |
protected: |
||||
|
PortType type; |
||||
InputPort* connected = nullptr; |
InputPort* connected = nullptr; |
||||
public: |
public: |
||||
explicit OutputPort(std::string name): Port(std::move(name)) {} |
|
||||
void SetValue(std::optional<int>); |
|
||||
void Connect(InputPort& ip); |
|
||||
|
OutputPort(std::string name, Node* owner, PortType type) : Port(std::move(name), owner), type(type) {} |
||||
|
void SetValue(PortData value); |
||||
|
bool TryConnect(InputPort& ip); |
||||
|
InputPort* GetConnected() const { return connected; } |
||||
void Disconnect(); |
void Disconnect(); |
||||
void Render(i32 x, i32 y) override; |
|
||||
|
i32 Render(i32 x, i32 y, i32 width) override; |
||||
}; |
}; |
||||
|
|
||||
struct Label : Port { |
struct Label : Port { |
||||
void Render(i32 x, i32 y) override; |
|
||||
explicit Label(std::string name): Port(std::move(name)) { |
|
||||
pos_x = -1; |
|
||||
pos_y = -1; |
|
||||
|
i32 Render(i32 x, i32 y, i32 width) override; |
||||
|
explicit Label(std::string name, Node* owner) : Port(std::move(name), owner) { |
||||
|
pos_x = pos_y = std::numeric_limits<i32>::min(); |
||||
} |
} |
||||
}; |
}; |
||||
|
|
||||
std::optional<int> InputPort::GetValue(){ |
|
||||
return value; |
|
||||
} |
|
||||
void OutputPort::SetValue(std::optional<int> val){ |
|
||||
if (connected) { |
|
||||
connected->value = val; |
|
||||
|
struct StringUserInput : Port { |
||||
|
explicit StringUserInput(std::string name, Node* owner) : Port(std::move(name), owner), data("") { |
||||
|
pos_x = pos_y = std::numeric_limits<i32>::min(); |
||||
} |
} |
||||
|
i32 Render(i32 x, i32 y, i32 width) override; |
||||
|
std::string data; |
||||
|
}; |
||||
|
|
||||
|
PortData const& InputPort::GetValue() const { return value; } |
||||
|
|
||||
|
void OutputPort::SetValue(PortData value) { |
||||
|
if (connected) { connected->value = std::move(value); } |
||||
} |
} |
||||
void OutputPort::Connect(InputPort& ip){ |
|
||||
connected = &ip; |
|
||||
|
bool OutputPort::TryConnect(InputPort& ip) { |
||||
|
if (ip.type == type) { |
||||
|
connected = &ip; |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
} |
} |
||||
|
|
||||
void OutputPort::Disconnect(){ |
|
||||
connected = nullptr; |
|
||||
} |
|
||||
|
void OutputPort::Disconnect() { connected = nullptr; } |
||||
|
|
||||
void OutputPort::Render(i32 x, i32 y) { |
|
||||
pos_x = x+100; pos_y = y+10; |
|
||||
DrawRectangleLines(x, y, 100, 20, BLACK); |
|
||||
|
i32 OutputPort::Render(i32 x, i32 y, i32 width) { |
||||
|
pos_x = x + width; |
||||
|
pos_y = y + 10; |
||||
|
DrawRectangleLines(x, y, width, 20, BLACK); |
||||
DrawText(name.c_str(), x + 5, y + 5, 10, RED); |
DrawText(name.c_str(), x + 5, y + 5, 10, RED); |
||||
DrawCircle(x+100, y+10, 2.5f, BLACK); |
|
||||
if (connected) { |
|
||||
DrawLine(pos_x, pos_y, connected->pos_x, connected->pos_y, BLUE); |
|
||||
} |
|
||||
|
DrawCircle(x + width, y + 10, 2.5f, BLACK); |
||||
|
if (connected) { DrawLine(pos_x, pos_y, connected->pos_x, connected->pos_y, BLUE); } |
||||
|
return 10 + MeasureText(name.c_str(), 10); |
||||
} |
} |
||||
void InputPort::Render(i32 x, i32 y) { |
|
||||
pos_x = x; pos_y = y+10; |
|
||||
DrawRectangleLines(x, y, 100, 20, BLACK); |
|
||||
DrawText(name.c_str(), x + 5, y + 5, 10, BLUE); |
|
||||
DrawCircle(x, y+10, 2.5f, BLACK); |
|
||||
|
i32 InputPort::Render(i32 x, i32 y, i32 width) { |
||||
|
pos_x = x; |
||||
|
pos_y = y + 10; |
||||
|
auto formatted = std::format("{}: {}", name, value); |
||||
|
auto new_width = 10 + MeasureText(formatted.c_str(), 10); |
||||
|
DrawRectangleLines(x, y, width, 20, BLACK); |
||||
|
DrawText(formatted.c_str(), x + 5, y + 5, 10, BLUE); |
||||
|
DrawCircle(x, y + 10, 2.5f, BLACK); |
||||
|
return new_width; |
||||
} |
} |
||||
void Label::Render(i32 x, i32 y){ |
|
||||
DrawRectangleLines(x, y, 100, 20, BLACK); |
|
||||
|
i32 Label::Render(i32 x, i32 y, i32 width) { |
||||
|
DrawRectangleLines(x, y, width, 20, BLACK); |
||||
DrawText(name.c_str(), x + 5, y + 5, 10, BLACK); |
DrawText(name.c_str(), x + 5, y + 5, 10, BLACK); |
||||
|
return 10 + MeasureText(name.c_str(), 10); |
||||
} |
} |
||||
|
|
||||
|
i32 StringUserInput::Render(i32 x, i32 y, i32 width) { |
||||
|
DrawRectangleLines(x, y, width, 20, BLACK); |
||||
|
if (data.empty()) { |
||||
|
DrawText(name.c_str(), x + 5, y + 5, 10, GRAY); |
||||
|
return 10 + MeasureText(name.c_str(), 10); |
||||
|
} else { |
||||
|
DrawText(data.c_str(), x + 5, y + 5, 10, BLACK); |
||||
|
return 10 + MeasureText(data.c_str(), 10); |
||||
|
} |
||||
} |
} |
||||
|
} // namespace ffmpegraph |
@ -1,8 +1,20 @@ |
|||||
|
#include <clopts.hh>
|
||||
|
#include <print>
|
||||
import Base; |
import Base; |
||||
import Application; |
import Application; |
||||
using namespace ffmpegraph; |
using namespace ffmpegraph; |
||||
|
|
||||
int main() { |
|
||||
|
namespace detail { |
||||
|
using namespace command_line_options; |
||||
|
using options = clopts< |
||||
|
help<>, |
||||
|
option<"--ffmpeg-path", "Path to ffmpeg, default is `ffmpeg'">, |
||||
|
positional<"savefile", "Savefile to load", file<>, false> |
||||
|
>; |
||||
|
} |
||||
|
|
||||
|
int main(int argc, char *argv[]) { |
||||
|
auto opts = detail::options::parse(argc, argv); |
||||
Application app("FFMpeGraph"); |
Application app("FFMpeGraph"); |
||||
app.Run(); |
app.Run(); |
||||
return 0; |
return 0; |
||||
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue