diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 4723649..bd18938 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -123,8 +123,6 @@ if(WIN32) requests.cppm events.cppm components.cppm - SOURCES - platform_windows.cpp DEPENDENCIES ecs app @@ -147,8 +145,6 @@ elseif(UNIX) requests.cppm events.cppm components.cppm - SOURCES - platform_linux.cpp DEPENDENCIES ecs app @@ -163,7 +159,6 @@ elseif(UNIX) time TESTS system.test.cpp - platform_linux.test.cpp ) diff --git a/modules/surface/platform_linux.cpp b/modules/surface/platform_linux.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/modules/surface/platform_linux.test.cpp b/modules/surface/platform_linux.test.cpp deleted file mode 100644 index 7dadbff..0000000 --- a/modules/surface/platform_linux.test.cpp +++ /dev/null @@ -1,28 +0,0 @@ -import test.test; -import test.expects; -import surface.system; -import surface.events; -import surface.requests; -import ecs.registry; -import memory.scope; -import memory.reference; -import logger; -import math.vec2; -import app.system; -import std; - -using ::lt::surface::SurfaceComponent; -using ::lt::surface::System; -using ::lt::test::Case; -using ::lt::test::expect_eq; -using ::lt::test::expect_ne; -using ::lt::test::expect_not_nullptr; -using ::lt::test::expect_throw; -using ::lt::test::Suite; -using ::std::ignore; -using ::lt::test::operator""_suite; - -Suite raii = "platform_linux_raii"_suite = [] { - auto registry = lt::memory::create_ref(); - std::ignore = System { registry }; -}; diff --git a/modules/surface/platform_windows.cpp b/modules/surface/platform_windows.cpp deleted file mode 100644 index f95fc8f..0000000 --- a/modules/surface/platform_windows.cpp +++ /dev/null @@ -1,527 +0,0 @@ -module; -#include -module surface.system; -import surface.constants; -import debug.assertions; -import memory.reference; -import surface.requests; -import surface.events; -import logger; -import ecs.registry; -import ecs.entity; -import time; -import std; - -namespace lt::surface { - -template -struct overloads: Ts... -{ - using Ts::operator()...; -}; - -void ensure_component_sanity(const SurfaceComponent &component); - -auto CALLBACK native_window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) -> LRESULT; - -System::System(memory::Ref registry): m_registry(std::move(registry)) -{ - debug::ensure(m_registry, "Failed to initialize surface system: null registry"); - - debug::ensure( - m_registry->view().get_size() == 0, - "Failed to initialize surface system: registry has surface component(s)" - ); - - m_registry->connect_on_destruct( - [this](ecs::Registry ®istry, ecs::EntityId entity) { - on_surface_destruct(registry, entity); - } - ); - - auto window_class = WNDCLASS { - .lpfnWndProc = native_window_proc, - .hInstance = GetModuleHandle(nullptr), - .lpszClassName = constants::class_name, - }; - RegisterClass(&window_class); -} - -System::~System() -{ - if (!m_registry) - { - return; - } - - try - { - // TODO(Light): make registry.remove not invalidate iterators - auto entities_to_remove = std::vector {}; - for (auto &[entity, surface] : m_registry->view()) - { - entities_to_remove.emplace_back(entity); - } - - for (auto entity : entities_to_remove) - { - m_registry->remove(entity); - } - - m_registry->disconnect_on_construct(); - m_registry->disconnect_on_destruct(); - } - catch (const std::exception &exp) - { - log::error("Uncaught exception in surface::~System:"); - log::error("\twhat: {}", exp.what()); - } -} - -void System::on_register() -{ -} - -void System::on_unregister() -{ -} - -void System::create_surface_component(ecs::EntityId entity, SurfaceComponent::CreateInfo info) -try -{ - auto &component = m_registry->add(entity, info); - auto &surface = m_registry->get(entity); - const auto &resolution = surface.get_resolution(); - const auto &position = surface.get_position(); - ensure_component_sanity(surface); - - surface.m_native_data.window = CreateWindowEx( - 0, - constants::class_name, - info.title.data(), - WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, - CW_USEDEFAULT, - CW_USEDEFAULT, - CW_USEDEFAULT, - nullptr, - nullptr, - GetModuleHandle(nullptr), - nullptr - ); - debug::ensure(surface.m_native_data.window, "Failed to create Windows surface component"); - - ShowWindow(surface.m_native_data.window, SW_NORMAL); - - // TODO(Light): refactor "environment" into standalone module - // NOLINTNEXTLINE(concurrency-mt-unsafe) - // auto *display_env = std::getenv("DISPLAY"); - // debug::ensure(display_env != nullptr, "DISPLAY env var not found!"); - // - // auto *display = XOpenDisplay(display_env); - // debug::ensure(display, "Failed to open XDisplay with DISPLAY: {}", display_env); - // - // auto root_window = XDefaultRootWindow(display); - // - // auto border_width = 0; - // auto depth = std::int32_t { CopyFromParent }; - // auto window_class = CopyFromParent; - // auto *visual = (Visual *)CopyFromParent; - // - // auto attribute_value_mask = CWBackPixel | CWEventMask; - // auto attributes = XSetWindowAttributes { - // .background_pixel = 0xffafe9af, - // .event_mask = all_events_mask, - // }; - // - // typedef struct Hints - // { - // unsigned long flags; - // unsigned long functions; - // unsigned long decorations; - // long inputMode; - // unsigned long status; - // } Hints; - // - // auto main_window = XCreateWindow( - // display, - // root_window, - // position.x, - // position.y, - // resolution.x, - // resolution.y, - // border_width, - // depth, - // window_class, - // visual, - // attribute_value_mask, - // &attributes - // ); - // surface.m_native_data.display = display; - // surface.m_native_data.window = main_window; - // - // surface.m_native_data.wm_delete_message = XInternAtom(display, "WM_DELETE_WINDOW", False); - // XSetWMProtocols(display, main_window, &surface.m_native_data.wm_delete_message, 1); - // - // // code to remove decoration - // auto hints = std::array { 2, 0, 0, 0, 0 }; - // const auto motif_hints = XInternAtom(display, "_MOTIF_WM_HINTS", False); - // - // XChangeProperty( - // display, - // surface.m_native_data.window, - // motif_hints, - // motif_hints, - // 32, - // PropModeReplace, - // hints.data(), - // 5 - // ); - // - // XMapWindow(display, main_window); - // XStoreName(display, main_window, surface.m_title.c_str()); - // XFlush(display); - // - // if (!surface.is_visible()) - // { - // XUnmapWindow(display, main_window); - // } -} -catch (const std::exception &exp) -{ - log::error("Exception thrown when on_constructing surface component"); - log::error("\tentity: {}", std::uint32_t { entity }); - log::error("\twhat: {}", exp.what()); - m_registry->remove(entity); -} - -void System::on_surface_destruct(ecs::Registry ®istry, ecs::EntityId entity) -{ - auto *window = registry.get(entity).get_native_data().window; - if (!window) - { - log::warn("Surface component destroyed with null window handle"); - return; - } - - DestroyWindow(window); -} - -void System::handle_events(SurfaceComponent &surface) -{ - auto &queue = surface.m_event_queue; - queue.clear(); - - auto message = MSG {}; - while (PeekMessage(&message, 0, {}, {}, PM_REMOVE)) - { - switch (message.message) - { - } - - log::debug("Window message type: {}", std::uint32_t { message.message }); - } - - // auto event = XEvent {}; - // auto &[display, window, wm_delete_message] = surface.m_native_data; - // - // XFlush(display); - // while (XEventsQueued(display, QueuedAlready) != 0) - // { - // XNextEvent(surface.m_native_data.display, &event); - // - // switch (event.type) - // { - // case KeyPress: - // { - // queue.emplace_back( - // static_cast(XLookupKeysym(&event.xkey, 0)) - // ); - // break; - // } - // case KeyRelease: - // { - // queue.emplace_back( - // static_cast(XLookupKeysym(&event.xkey, 0)) - // ); - // break; - // } - // case ButtonPress: - // { - // queue.emplace_back(static_cast(event.xbutton.button)); - // break; - // } - // case ButtonRelease: - // { - // queue.emplace_back(static_cast(event.xbutton.button)); - // break; - // } - // case FocusIn: - // { - // queue.emplace_back({}); - // break; - // } - // case FocusOut: - // { - // queue.emplace_back({}); - // break; - // } - // case ClientMessage: - // { - // if (event.xclient.data.l[0] == wm_delete_message) - // { - // queue.emplace_back({}); - // } - // - // break; - // } - // case MotionNotify: - // { - // queue.emplace_back(MouseMovedEvent { - // static_cast(event.xmotion.x), - // static_cast(event.xmotion.y), - // }); - // break; - // } - // case ConfigureNotify: - // { - // const auto [prev_width, prev_height] = surface.get_resolution(); - // const auto new_width = event.xconfigure.width; - // const auto new_height = event.xconfigure.height; - // if (prev_width != new_width || prev_height != new_height) - // { - // surface.m_resolution.x = new_width; - // surface.m_resolution.y = new_height; - // queue.emplace_back(ResizedEvent { - // static_cast(new_width), - // static_cast(new_height), - // }); - // } - // - // const auto [prev_x, prev_y] = surface.get_position(); - // const auto new_x = event.xconfigure.x; - // const auto new_y = event.xconfigure.y; - // if (prev_x != new_x || prev_y != new_y) - // { - // surface.m_position.x = new_x; - // surface.m_position.y = new_y; - // queue.emplace_back(MovedEvent { - // new_x, - // new_y, - // }); - // } - // break; - // } - // - // default: break; /* pass */ - // } - // } -} - -void System::handle_requests(SurfaceComponent &surface) -{ - const auto visitor = overloads { - [&](const ModifyTitleRequest &request) { modify_title(surface, request); }, - [&](const ModifyResolutionRequest &request) { modify_resolution(surface, request); }, - [&](const ModifyPositionRequest &request) { modify_position(surface, request); }, - [&](const ModifyVisibilityRequest &request) { modify_visiblity(surface, request); }, - [&](const auto &) { log::error("Unknown surface request"); }, - }; - - for (const auto &request : surface.peek_requests()) - { - std::visit(visitor, request); - } - - surface.m_requests.clear(); -} - -void System::modify_title(SurfaceComponent &surface, const ModifyTitleRequest &request) -{ - surface.m_title = request.title; - - // const auto &[display, window, _] = surface.get_native_data(); - // XStoreName(display, window, request.title.c_str()); -} - -void System::modify_resolution(SurfaceComponent &surface, const ModifyResolutionRequest &request) -{ - // surface.m_resolution = request.resolution; - - // auto &[display, window, _] = surface.m_native_data; - // const auto &[width, height] = request.resolution; - // // XResizeWindow(display, window, width, height); - // - // // get baseline serial number for X requests generated from XResizeWindow - // auto serial = NextRequest(display); - // - // // request a new window size from the X server - // XResizeWindow( - // display, - // window, - // static_cast(width), - // static_cast(height) - // ); - // - // // flush output queue and wait for X server to processes the request - // XSync(display, False); - // // The documentation for XResizeWindow includes this important note: - // // - // // If the override-redirect flag of the window is False and some - // // other client has selected SubstructureRedirectMask on the parent, - // // the X server generates a ConfigureRequest event, and no further - // // processing is performed. - // // - // // What this means, essentially, is that if this window is a top-level - // // window, then it's the window manager (the "other client") that is - // // responsible for changing this window's size. So when we call - // // XResizeWindow() on a top-level window, then instead of resizing - // // the window immediately, the X server informs the window manager, - // // and then the window manager sets our new size (usually it will be - // // the size we asked for). We receive a ConfigureNotify event when - // // our new size has been set. - // constexpr auto lifespan = std::chrono::milliseconds { 10 }; - // auto timer = time::Timer {}; - // auto event = XEvent {}; - // while (!XCheckIfEvent( - // display, - // &event, - // XEventTypeEquals, - // reinterpret_cast(&window) // NOLINT - // ) - // || event.xconfigure.serial < serial) - // { - // std::this_thread::sleep_for(std::chrono::microseconds { 100 }); - // if (timer.elapsed_time() > lifespan) - // { - // log::error("Timed out waiting for XResizeWindow's event"); - // return; - // } - // } - // // We don't need to update the component's state and handle the event in this funcion. - // // Since handle_requests is called before handle_events. - // // So we just put the event back into the queue and move on. - // XPutBackEvent(display, &event); - // XSync(display, False); - // XFlush(display); -} - -void System::modify_position(SurfaceComponent &surface, const ModifyPositionRequest &request) -{ - // surface.m_position = request.position; - - // auto &[display, window, _] = surface.m_native_data; - // const auto &[x, y] = request.position; - // - // // get baseline serial number for X requests generated from XResizeWindow - // auto serial = NextRequest(display); - // XMoveWindow(display, window, static_cast(x), static_cast(y)); - // - // // flush output queue and wait for X server to processes the request - // XSync(display, False); - // constexpr auto lifespan = std::chrono::milliseconds { 10 }; - // auto timer = time::Timer {}; - // auto event = XEvent {}; - // while (!XCheckIfEvent( - // display, - // &event, - // XEventTypeEquals, - // reinterpret_cast(&window) // NOLINT - // ) - // || event.xconfigure.serial < serial) - // { - // std::this_thread::sleep_for(std::chrono::microseconds { 100 }); - // if (timer.elapsed_time() > lifespan) - // { - // log::error("Timed out waiting for XMoveWindow's event"); - // return; - // } - // } - // // We don't need to update the component's state and handle the event in this funcion. - // // Since handle_requests is called before handle_events. - // // So we just put the event back into the queue and move on. - // XPutBackEvent(display, &event); - // XSync(display, False); - // XFlush(display); -} - -void System::modify_visiblity(SurfaceComponent &surface, const ModifyVisibilityRequest &request) -{ - // const auto &[display, window, _] = surface.get_native_data(); - // surface.m_visible = request.visible; - - // if (request.visible) - // { - // XMapWindow(display, window); - // } - // else - // { - // XUnmapWindow(display, window); - // } -} - -void System::tick(app::TickInfo tick) -{ - for (auto &[id, surface] : m_registry->view()) - { - handle_requests(surface); - handle_events(surface); - } - - const auto now = std::chrono::steady_clock::now(); - m_last_tick_result = app::TickResult { - .info = tick, - .duration = now - tick.start_time, - .end_time = now, - }; -} - -void ensure_component_sanity(const SurfaceComponent &component) -{ - auto [width, height] = component.get_resolution(); - - debug::ensure(width != 0u, "Received bad values for surface component: width({}) == 0", width); - - debug::ensure( - height != 0u, - "Received bad values for surface component: height({}) == 0", - height - ); - - debug::ensure( - width < SurfaceComponent::max_dimension, - "Received bad values for surface component: width({}) > max_dimension({})", - width, - SurfaceComponent::max_dimension - ); - - debug::ensure( - height < SurfaceComponent::max_dimension, - "Received bad values for surface component: height({}) > max_dimension({})", - height, - SurfaceComponent::max_dimension - ); - - debug::ensure( - component.get_title().size() < SurfaceComponent::max_title_length, - "Received bad values for surface component: title.size({}) > max_title_length({})", - component.get_title().size(), - SurfaceComponent::max_title_length - ); -} - -auto CALLBACK native_window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) -> LRESULT -{ - switch (uMsg) - { - case WM_DESTROY: - { - PostQuitMessage(0); - return 0; - } - } - - return DefWindowProcA(hwnd, uMsg, wParam, lParam); -} - -} // namespace lt::surface diff --git a/modules/surface/system.cppm b/modules/surface/system.cppm index 27d928b..e6c57d0 100644 --- a/modules/surface/system.cppm +++ b/modules/surface/system.cppm @@ -106,7 +106,6 @@ namespace lt::surface { void handle_shell_ping(void *data, xdg_wm_base *shell, std::uint32_t serial) { std::ignore = data; - xdg_wm_base_pong(shell, serial); } const auto shell_listener = xdg_wm_base_listener { @@ -264,6 +263,532 @@ void System::tick(app::TickInfo tick) #endif #ifdef LIGHT_PLATFORM_WINDOWS + +module; + #include +module surface.system; +import surface.constants; +import debug.assertions; +import memory.reference; +import surface.requests; +import surface.events; +import logger; +import ecs.registry; +import ecs.entity; +import time; +import std; + +namespace lt::surface { + +template +struct overloads: Ts... +{ + using Ts::operator()...; +}; + +void ensure_component_sanity(const SurfaceComponent &component); + +auto CALLBACK native_window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) -> LRESULT; + +System::System(memory::Ref registry): m_registry(std::move(registry)) +{ + debug::ensure(m_registry, "Failed to initialize surface system: null registry"); + + debug::ensure( + m_registry->view().get_size() == 0, + "Failed to initialize surface system: registry has surface component(s)" + ); + + m_registry->connect_on_destruct( + [this](ecs::Registry ®istry, ecs::EntityId entity) { + on_surface_destruct(registry, entity); + } + ); + + auto window_class = WNDCLASS { + .lpfnWndProc = native_window_proc, + .hInstance = GetModuleHandle(nullptr), + .lpszClassName = constants::class_name, + }; + RegisterClass(&window_class); +} + +System::~System() +{ + if (!m_registry) + { + return; + } + + try + { + // TODO(Light): make registry.remove not invalidate iterators + auto entities_to_remove = std::vector {}; + for (auto &[entity, surface] : m_registry->view()) + { + entities_to_remove.emplace_back(entity); + } + + for (auto entity : entities_to_remove) + { + m_registry->remove(entity); + } + + m_registry->disconnect_on_construct(); + m_registry->disconnect_on_destruct(); + } + catch (const std::exception &exp) + { + log::error("Uncaught exception in surface::~System:"); + log::error("\twhat: {}", exp.what()); + } +} + +void System::on_register() +{ +} + +void System::on_unregister() +{ +} + +void System::create_surface_component(ecs::EntityId entity, SurfaceComponent::CreateInfo info) +try +{ + auto &component = m_registry->add(entity, info); + auto &surface = m_registry->get(entity); + const auto &resolution = surface.get_resolution(); + const auto &position = surface.get_position(); + ensure_component_sanity(surface); + + surface.m_native_data.window = CreateWindowEx( + 0, + constants::class_name, + info.title.data(), + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + nullptr, + nullptr, + GetModuleHandle(nullptr), + nullptr + ); + debug::ensure(surface.m_native_data.window, "Failed to create Windows surface component"); + + ShowWindow(surface.m_native_data.window, SW_NORMAL); + + // TODO(Light): refactor "environment" into standalone module + // NOLINTNEXTLINE(concurrency-mt-unsafe) + // auto *display_env = std::getenv("DISPLAY"); + // debug::ensure(display_env != nullptr, "DISPLAY env var not found!"); + // + // auto *display = XOpenDisplay(display_env); + // debug::ensure(display, "Failed to open XDisplay with DISPLAY: {}", display_env); + // + // auto root_window = XDefaultRootWindow(display); + // + // auto border_width = 0; + // auto depth = std::int32_t { CopyFromParent }; + // auto window_class = CopyFromParent; + // auto *visual = (Visual *)CopyFromParent; + // + // auto attribute_value_mask = CWBackPixel | CWEventMask; + // auto attributes = XSetWindowAttributes { + // .background_pixel = 0xffafe9af, + // .event_mask = all_events_mask, + // }; + // + // typedef struct Hints + // { + // unsigned long flags; + // unsigned long functions; + // unsigned long decorations; + // long inputMode; + // unsigned long status; + // } Hints; + // + // auto main_window = XCreateWindow( + // display, + // root_window, + // position.x, + // position.y, + // resolution.x, + // resolution.y, + // border_width, + // depth, + // window_class, + // visual, + // attribute_value_mask, + // &attributes + // ); + // surface.m_native_data.display = display; + // surface.m_native_data.window = main_window; + // + // surface.m_native_data.wm_delete_message = XInternAtom(display, "WM_DELETE_WINDOW", False); + // XSetWMProtocols(display, main_window, &surface.m_native_data.wm_delete_message, 1); + // + // // code to remove decoration + // auto hints = std::array { 2, 0, 0, 0, 0 }; + // const auto motif_hints = XInternAtom(display, "_MOTIF_WM_HINTS", False); + // + // XChangeProperty( + // display, + // surface.m_native_data.window, + // motif_hints, + // motif_hints, + // 32, + // PropModeReplace, + // hints.data(), + // 5 + // ); + // + // XMapWindow(display, main_window); + // XStoreName(display, main_window, surface.m_title.c_str()); + // XFlush(display); + // + // if (!surface.is_visible()) + // { + // XUnmapWindow(display, main_window); + // } +} +catch (const std::exception &exp) +{ + log::error("Exception thrown when on_constructing surface component"); + log::error("\tentity: {}", std::uint32_t { entity }); + log::error("\twhat: {}", exp.what()); + m_registry->remove(entity); +} + +void System::on_surface_destruct(ecs::Registry ®istry, ecs::EntityId entity) +{ + auto *window = registry.get(entity).get_native_data().window; + if (!window) + { + log::warn("Surface component destroyed with null window handle"); + return; + } + + DestroyWindow(window); +} + +void System::handle_events(SurfaceComponent &surface) +{ + auto &queue = surface.m_event_queue; + queue.clear(); + + auto message = MSG {}; + while (PeekMessage(&message, 0, {}, {}, PM_REMOVE)) + { + switch (message.message) {} + + log::debug("Window message type: {}", std::uint32_t { message.message }); + } + + // auto event = XEvent {}; + // auto &[display, window, wm_delete_message] = surface.m_native_data; + // + // XFlush(display); + // while (XEventsQueued(display, QueuedAlready) != 0) + // { + // XNextEvent(surface.m_native_data.display, &event); + // + // switch (event.type) + // { + // case KeyPress: + // { + // queue.emplace_back( + // static_cast(XLookupKeysym(&event.xkey, 0)) + // ); + // break; + // } + // case KeyRelease: + // { + // queue.emplace_back( + // static_cast(XLookupKeysym(&event.xkey, 0)) + // ); + // break; + // } + // case ButtonPress: + // { + // queue.emplace_back(static_cast(event.xbutton.button)); + // break; + // } + // case ButtonRelease: + // { + // queue.emplace_back(static_cast(event.xbutton.button)); + // break; + // } + // case FocusIn: + // { + // queue.emplace_back({}); + // break; + // } + // case FocusOut: + // { + // queue.emplace_back({}); + // break; + // } + // case ClientMessage: + // { + // if (event.xclient.data.l[0] == wm_delete_message) + // { + // queue.emplace_back({}); + // } + // + // break; + // } + // case MotionNotify: + // { + // queue.emplace_back(MouseMovedEvent { + // static_cast(event.xmotion.x), + // static_cast(event.xmotion.y), + // }); + // break; + // } + // case ConfigureNotify: + // { + // const auto [prev_width, prev_height] = surface.get_resolution(); + // const auto new_width = event.xconfigure.width; + // const auto new_height = event.xconfigure.height; + // if (prev_width != new_width || prev_height != new_height) + // { + // surface.m_resolution.x = new_width; + // surface.m_resolution.y = new_height; + // queue.emplace_back(ResizedEvent { + // static_cast(new_width), + // static_cast(new_height), + // }); + // } + // + // const auto [prev_x, prev_y] = surface.get_position(); + // const auto new_x = event.xconfigure.x; + // const auto new_y = event.xconfigure.y; + // if (prev_x != new_x || prev_y != new_y) + // { + // surface.m_position.x = new_x; + // surface.m_position.y = new_y; + // queue.emplace_back(MovedEvent { + // new_x, + // new_y, + // }); + // } + // break; + // } + // + // default: break; /* pass */ + // } + // } +} + +void System::handle_requests(SurfaceComponent &surface) +{ + const auto visitor = overloads { + [&](const ModifyTitleRequest &request) { modify_title(surface, request); }, + [&](const ModifyResolutionRequest &request) { modify_resolution(surface, request); }, + [&](const ModifyPositionRequest &request) { modify_position(surface, request); }, + [&](const ModifyVisibilityRequest &request) { modify_visiblity(surface, request); }, + [&](const auto &) { log::error("Unknown surface request"); }, + }; + + for (const auto &request : surface.peek_requests()) + { + std::visit(visitor, request); + } + + surface.m_requests.clear(); +} + +void System::modify_title(SurfaceComponent &surface, const ModifyTitleRequest &request) +{ + surface.m_title = request.title; + + // const auto &[display, window, _] = surface.get_native_data(); + // XStoreName(display, window, request.title.c_str()); +} + +void System::modify_resolution(SurfaceComponent &surface, const ModifyResolutionRequest &request) +{ + // surface.m_resolution = request.resolution; + + // auto &[display, window, _] = surface.m_native_data; + // const auto &[width, height] = request.resolution; + // // XResizeWindow(display, window, width, height); + // + // // get baseline serial number for X requests generated from XResizeWindow + // auto serial = NextRequest(display); + // + // // request a new window size from the X server + // XResizeWindow( + // display, + // window, + // static_cast(width), + // static_cast(height) + // ); + // + // // flush output queue and wait for X server to processes the request + // XSync(display, False); + // // The documentation for XResizeWindow includes this important note: + // // + // // If the override-redirect flag of the window is False and some + // // other client has selected SubstructureRedirectMask on the parent, + // // the X server generates a ConfigureRequest event, and no further + // // processing is performed. + // // + // // What this means, essentially, is that if this window is a top-level + // // window, then it's the window manager (the "other client") that is + // // responsible for changing this window's size. So when we call + // // XResizeWindow() on a top-level window, then instead of resizing + // // the window immediately, the X server informs the window manager, + // // and then the window manager sets our new size (usually it will be + // // the size we asked for). We receive a ConfigureNotify event when + // // our new size has been set. + // constexpr auto lifespan = std::chrono::milliseconds { 10 }; + // auto timer = time::Timer {}; + // auto event = XEvent {}; + // while (!XCheckIfEvent( + // display, + // &event, + // XEventTypeEquals, + // reinterpret_cast(&window) // NOLINT + // ) + // || event.xconfigure.serial < serial) + // { + // std::this_thread::sleep_for(std::chrono::microseconds { 100 }); + // if (timer.elapsed_time() > lifespan) + // { + // log::error("Timed out waiting for XResizeWindow's event"); + // return; + // } + // } + // // We don't need to update the component's state and handle the event in this funcion. + // // Since handle_requests is called before handle_events. + // // So we just put the event back into the queue and move on. + // XPutBackEvent(display, &event); + // XSync(display, False); + // XFlush(display); +} + +void System::modify_position(SurfaceComponent &surface, const ModifyPositionRequest &request) +{ + // surface.m_position = request.position; + + // auto &[display, window, _] = surface.m_native_data; + // const auto &[x, y] = request.position; + // + // // get baseline serial number for X requests generated from XResizeWindow + // auto serial = NextRequest(display); + // XMoveWindow(display, window, static_cast(x), static_cast(y)); + // + // // flush output queue and wait for X server to processes the request + // XSync(display, False); + // constexpr auto lifespan = std::chrono::milliseconds { 10 }; + // auto timer = time::Timer {}; + // auto event = XEvent {}; + // while (!XCheckIfEvent( + // display, + // &event, + // XEventTypeEquals, + // reinterpret_cast(&window) // NOLINT + // ) + // || event.xconfigure.serial < serial) + // { + // std::this_thread::sleep_for(std::chrono::microseconds { 100 }); + // if (timer.elapsed_time() > lifespan) + // { + // log::error("Timed out waiting for XMoveWindow's event"); + // return; + // } + // } + // // We don't need to update the component's state and handle the event in this funcion. + // // Since handle_requests is called before handle_events. + // // So we just put the event back into the queue and move on. + // XPutBackEvent(display, &event); + // XSync(display, False); + // XFlush(display); +} + +void System::modify_visiblity(SurfaceComponent &surface, const ModifyVisibilityRequest &request) +{ + // const auto &[display, window, _] = surface.get_native_data(); + // surface.m_visible = request.visible; + + // if (request.visible) + // { + // XMapWindow(display, window); + // } + // else + // { + // XUnmapWindow(display, window); + // } +} + +void System::tick(app::TickInfo tick) +{ + for (auto &[id, surface] : m_registry->view()) + { + handle_requests(surface); + handle_events(surface); + } + + const auto now = std::chrono::steady_clock::now(); + m_last_tick_result = app::TickResult { + .info = tick, + .duration = now - tick.start_time, + .end_time = now, + }; +} + +void ensure_component_sanity(const SurfaceComponent &component) +{ + auto [width, height] = component.get_resolution(); + + debug::ensure(width != 0u, "Received bad values for surface component: width({}) == 0", width); + + debug::ensure( + height != 0u, + "Received bad values for surface component: height({}) == 0", + height + ); + + debug::ensure( + width < SurfaceComponent::max_dimension, + "Received bad values for surface component: width({}) > max_dimension({})", + width, + SurfaceComponent::max_dimension + ); + + debug::ensure( + height < SurfaceComponent::max_dimension, + "Received bad values for surface component: height({}) > max_dimension({})", + height, + SurfaceComponent::max_dimension + ); + + debug::ensure( + component.get_title().size() < SurfaceComponent::max_title_length, + "Received bad values for surface component: title.size({}) > max_title_length({})", + component.get_title().size(), + SurfaceComponent::max_title_length + ); +} + +auto CALLBACK native_window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) -> LRESULT +{ + switch (uMsg) + { + case WM_DESTROY: + { + PostQuitMessage(0); + return 0; + } + } + + return DefWindowProcA(hwnd, uMsg, wParam, lParam); +} + +} // namespace lt::surface #endif } // namespace lt::surface diff --git a/modules/surface/system.test.cpp b/modules/surface/system.test.cpp index 4a6b5eb..386af95 100644 --- a/modules/surface/system.test.cpp +++ b/modules/surface/system.test.cpp @@ -1,290 +1,284 @@ -// Suite raii = "raii"_suite = [] { -// Case { "happy path won't throw" } = [] { -// auto fixture = Fixture {}; -// ignore = System { fixture.registry() }; -// }; -// -// import test.test; -// import test.expects; -// import surface.system; -// import surface.events; -// import surface.requests; -// import ecs.registry; -// import memory.scope; -// import memory.reference; -// import logger; -// import math.vec2; -// import app.system; -// import std; -// -// using ::lt::surface::SurfaceComponent; -// using ::lt::surface::System; -// using ::lt::test::Case; -// using ::lt::test::expect_eq; -// using ::lt::test::expect_ne; -// using ::lt::test::expect_not_nullptr; -// using ::lt::test::expect_throw; -// using ::lt::test::Suite; -// using ::std::ignore; -// using ::lt::test::operator""_suite; -// -// [[nodiscard]] auto tick_info() -> lt::app::TickInfo -// { -// return { -// .delta_time = std::chrono::milliseconds { 16 }, -// .budget = std::chrono::milliseconds { 10 }, -// .start_time = std::chrono::steady_clock::now(), -// }; -// } -// -// constexpr auto title = "TestWindow"; -// constexpr auto width = 800u; -// constexpr auto height = 600u; -// constexpr auto vsync = true; -// constexpr auto visible = false; -// -// template -// struct overloads: Ts... -// { -// using Ts::operator()...; -// }; -// -// class Fixture -// { -// public: -// [[nodiscard]] auto registry() -> lt::memory::Ref -// { -// return m_registry; -// } -// -// auto create_component( -// SurfaceComponent::CreateInfo info = SurfaceComponent::CreateInfo { -// .title = title, -// .resolution = { width, height }, -// .vsync = vsync, -// .visible = visible, -// } -// ) -> std::optional -// { -// auto entity = m_registry->create_entity(); -// m_system.create_surface_component(entity, info); -// -// return &m_registry->get(entity); -// } -// -// void check_values(SurfaceComponent *component) -// { -// #ifdef LIGHT_PLATFORM_LINUX -// expect_not_nullptr(component->get_native_data().display); -// expect_ne(component->get_native_data().window, 0); -// #endif -// -// expect_eq(component->get_resolution().x, width); -// expect_eq(component->get_resolution().y, height); -// expect_eq(component->get_title(), title); -// expect_eq(component->is_vsync(), vsync); -// expect_eq(component->is_visible(), visible); -// } -// -// private: -// lt::memory::Ref m_registry = lt::memory::create_ref(); -// -// System m_system { m_registry }; -// }; -// -// -// Suite raii = "raii"_suite = [] { -// Case { "happy path won't throw" } = [] { -// auto fixture = Fixture {}; -// ignore = System { fixture.registry() }; -// }; -// -// Case { "many won't freeze/throw" } = [] { -// auto fixture = Fixture {}; -// for (auto idx : std::views::iota(0, 250)) -// { -// ignore = System { fixture.registry() }; -// } -// }; -// -// Case { "unhappy path throws" } = [] { -// expect_throw([] { ignore = System { {} }; }); -// }; -// -// Case { "post construct has correct state" } = [] { -// auto fixture = Fixture {}; -// auto system = System { fixture.registry() }; -// expect_eq(fixture.registry()->view().get_size(), 0); -// }; -// -// Case { "post destruct has correct state" } = [] { -// auto fixture = Fixture {}; -// auto system = lt::memory::create_scope(fixture.registry()); -// -// fixture.create_component(); -// expect_eq(fixture.registry()->view().get_size(), 1); -// -// system.reset(); -// expect_eq(fixture.registry()->view().get_size(), 0); -// }; -// }; -// -// Suite system_events = "system_events"_suite = [] { -// Case { "on_register won't throw" } = [] { -// auto fixture = Fixture {}; -// auto system = System { fixture.registry() }; -// -// system.on_register(); -// expect_eq(fixture.registry()->view().get_size(), 0); -// }; -// -// Case { "on_unregister won't throw" } = [] { -// auto fixture = Fixture {}; -// auto system = System { fixture.registry() }; -// -// system.on_register(); -// system.on_unregister(); -// expect_eq(fixture.registry()->view().get_size(), 0); -// }; -// }; -// -// Suite registry_events = "registry_events"_suite = [] { -// Case { "on_construct initializes component" } = [] { -// auto fixture = Fixture {}; -// -// const auto &component = fixture.create_component(); -// expect_eq(fixture.registry()->view().get_size(), 1); -// fixture.check_values(*component); -// }; -// -// Case { "unhappy on_construct throws" } = [] { -// auto fixture = Fixture {}; -// auto system = System { fixture.registry() }; -// -// expect_throw([&] { fixture.create_component({ .resolution = { width, 0 } }); }); -// -// expect_throw([&] { fixture.create_component({ .resolution = { 0, height } }); }); -// -// expect_throw([&] { -// fixture.create_component( -// { .title = "", .resolution = { SurfaceComponent::max_dimension + 1, height } } -// ); -// }); -// -// expect_throw([&] { -// fixture.create_component( -// { .title = "", .resolution = { width, SurfaceComponent::max_dimension + 1 } } -// ); -// }); -// -// auto big_str = std::string {}; -// big_str.resize(SurfaceComponent::max_title_length + 1); -// expect_throw([&] { -// fixture.create_component({ .title = big_str, .resolution = { width, height } }); -// }); -// }; -// -// Case { "unhappy on_construct removes component" } = [] { -// auto fixture = Fixture {}; -// auto system = System { fixture.registry() }; -// -// expect_throw([&] { fixture.create_component({ .resolution = { width, 0 } }); }); -// expect_eq(fixture.registry()->view().get_size(), 0); -// }; -// -// Case { "on_destrroy cleans up component" } = [] { -// auto fixture = Fixture {}; -// auto system = lt::memory::create_scope(fixture.registry()); -// -// const auto &component = fixture.create_component(); -// expect_eq(fixture.registry()->view().get_size(), 1); -// fixture.check_values(*component); -// -// system.reset(); -// expect_eq(fixture.registry()->view().get_size(), 0); -// }; -// }; -// -// Suite tick = "tick"_suite = [] { -// Case { "ticking on empty registry won't throw" } = [] { -// auto fixture = Fixture {}; -// System { fixture.registry() }.tick(tick_info()); -// }; -// -// Case { "ticking on non-empty registry won't throw" } = [] { -// auto fixture = Fixture {}; -// auto system = System { fixture.registry() }; -// -// fixture.create_component(); -// system.tick(tick_info()); -// }; -// }; -// -// Suite tick_handles_events = "tick_handles_events"_suite = [] { -// Case { "ticking clears previous tick's events" } = [] { -// auto fixture = Fixture {}; -// auto system = System { fixture.registry() }; -// auto &surface = **fixture.create_component(); -// -// // flush window-creation events -// system.tick(tick_info()); -// expect_eq(surface.peek_events().size(), 0); -// -// surface.push_event(lt::surface::MovedEvent({}, {})); -// expect_eq(surface.peek_events().size(), 1); -// -// surface.push_event(lt::surface::ButtonPressedEvent({})); -// expect_eq(surface.peek_events().size(), 2); -// -// system.tick(tick_info()); -// expect_eq(surface.peek_events().size(), 0); -// }; -// }; -// -// Suite tick_handles_requests = "tick_handles_requests"_suite = [] { -// Case { "ticking clears requests" } = [] { -// auto fixture = Fixture {}; -// auto system = System { fixture.registry() }; -// auto &surface = **fixture.create_component(); -// -// constexpr auto title = "ABC"; -// constexpr auto position = lt::math::ivec2 { 50, 50 }; -// constexpr auto resolution = lt::math::uvec2 { 50, 50 }; -// -// expect_eq(surface.peek_requests().size(), 0); -// -// surface.push_request(lt::surface::ModifyVisibilityRequest(true)); -// expect_eq(surface.peek_requests().size(), 1); -// system.tick(tick_info()); -// expect_eq(surface.peek_requests().size(), 0); -// -// surface.push_request(lt::surface::ModifyTitleRequest(title)); -// expect_eq(surface.peek_requests().size(), 1); -// -// surface.push_request(lt::surface::ModifyResolutionRequest(resolution)); -// surface.push_request(lt::surface::ModifyPositionRequest(position)); -// expect_eq(surface.peek_requests().size(), 1 + 2); -// -// surface.push_request(lt::surface::ModifyVisibilityRequest(false)); -// surface.push_request(lt::surface::ModifyVisibilityRequest(true)); -// surface.push_request(lt::surface::ModifyVisibilityRequest(false)); -// expect_eq(surface.peek_requests().size(), 1 + 2 + 3); -// -// system.tick(tick_info()); -// expect_eq(surface.peek_requests().size(), 0); -// -// expect_eq(surface.get_title(), title); -// expect_eq(surface.get_position(), position); -// expect_eq(surface.get_resolution(), resolution); -// -// lt::log::debug("EVENT COUNT: {}", surface.peek_events().size()); -// for (const auto &event : surface.peek_events()) -// { -// const auto visitor = overloads { -// [&](auto event) { lt::log::debug("event: {}", event.to_string()); }, -// }; -// -// std::visit(visitor, event); -// } -// }; -// }; +import test.test; +import test.expects; +import surface.system; +import surface.events; +import surface.requests; +import ecs.registry; +import memory.scope; +import memory.reference; +import logger; +import math.vec2; +import app.system; +import std; + +using ::lt::surface::SurfaceComponent; +using ::lt::surface::System; +using ::lt::test::Case; +using ::lt::test::expect_eq; +using ::lt::test::expect_ne; +using ::lt::test::expect_not_nullptr; +using ::lt::test::expect_throw; +using ::lt::test::Suite; +using ::std::ignore; +using ::lt::test::operator""_suite; + +[[nodiscard]] auto tick_info() -> lt::app::TickInfo +{ + return { + .delta_time = std::chrono::milliseconds { 16 }, + .budget = std::chrono::milliseconds { 10 }, + .start_time = std::chrono::steady_clock::now(), + }; +} + +constexpr auto title = "TestWindow"; +constexpr auto width = 800u; +constexpr auto height = 600u; +constexpr auto vsync = true; +constexpr auto visible = false; + +template +struct overloads: Ts... +{ + using Ts::operator()...; +}; + +class Fixture +{ +public: + [[nodiscard]] auto registry() -> lt::memory::Ref + { + return m_registry; + } + + auto create_component( + SurfaceComponent::CreateInfo info = SurfaceComponent::CreateInfo { + .title = title, + .resolution = { width, height }, + .vsync = vsync, + .visible = visible, + } + ) -> std::optional + { + auto entity = m_registry->create_entity(); + m_system.create_surface_component(entity, info); + + return &m_registry->get(entity); + } + + void check_values(SurfaceComponent *component) + { +#ifdef LIGHT_PLATFORM_LINUX + expect_not_nullptr(component->get_native_data().display); + expect_not_nullptr(component->get_native_data().surface); +#endif + + expect_eq(component->get_resolution().x, width); + expect_eq(component->get_resolution().y, height); + expect_eq(component->get_title(), title); + expect_eq(component->is_vsync(), vsync); + expect_eq(component->is_visible(), visible); + } + +private: + lt::memory::Ref m_registry = lt::memory::create_ref(); + + System m_system { m_registry }; +}; + + +Suite raii = "raii"_suite = [] { + Case { "happy path won't throw" } = [] { + auto fixture = Fixture {}; + ignore = System { fixture.registry() }; + }; + + Case { "many won't freeze/throw" } = [] { + auto fixture = Fixture {}; + for (auto idx : std::views::iota(0, 250)) + { + ignore = System { fixture.registry() }; + } + }; + + Case { "unhappy path throws" } = [] { + expect_throw([] { ignore = System { {} }; }); + }; + + Case { "post construct has correct state" } = [] { + auto fixture = Fixture {}; + auto system = System { fixture.registry() }; + expect_eq(fixture.registry()->view().get_size(), 0); + }; + + Case { "post destruct has correct state" } = [] { + auto fixture = Fixture {}; + auto system = lt::memory::create_scope(fixture.registry()); + + fixture.create_component(); + expect_eq(fixture.registry()->view().get_size(), 1); + + system.reset(); + expect_eq(fixture.registry()->view().get_size(), 0); + }; +}; + +Suite system_events = "system_events"_suite = [] { + Case { "on_register won't throw" } = [] { + auto fixture = Fixture {}; + auto system = System { fixture.registry() }; + + system.on_register(); + expect_eq(fixture.registry()->view().get_size(), 0); + }; + + Case { "on_unregister won't throw" } = [] { + auto fixture = Fixture {}; + auto system = System { fixture.registry() }; + + system.on_register(); + system.on_unregister(); + expect_eq(fixture.registry()->view().get_size(), 0); + }; +}; + +Suite registry_events = "registry_events"_suite = [] { + Case { "on_construct initializes component" } = [] { + auto fixture = Fixture {}; + + const auto &component = fixture.create_component(); + expect_eq(fixture.registry()->view().get_size(), 1); + fixture.check_values(*component); + }; + + Case { "unhappy on_construct throws" } = [] { + auto fixture = Fixture {}; + auto system = System { fixture.registry() }; + + expect_throw([&] { fixture.create_component({ .resolution = { width, 0 } }); }); + + expect_throw([&] { fixture.create_component({ .resolution = { 0, height } }); }); + + expect_throw([&] { + fixture.create_component( + { .title = "", .resolution = { SurfaceComponent::max_dimension + 1, height } } + ); + }); + + expect_throw([&] { + fixture.create_component( + { .title = "", .resolution = { width, SurfaceComponent::max_dimension + 1 } } + ); + }); + + auto big_str = std::string {}; + big_str.resize(SurfaceComponent::max_title_length + 1); + expect_throw([&] { + fixture.create_component({ .title = big_str, .resolution = { width, height } }); + }); + }; + + Case { "unhappy on_construct removes component" } = [] { + auto fixture = Fixture {}; + auto system = System { fixture.registry() }; + + expect_throw([&] { fixture.create_component({ .resolution = { width, 0 } }); }); + expect_eq(fixture.registry()->view().get_size(), 0); + }; + + Case { "on_destrroy cleans up component" } = [] { + auto fixture = Fixture {}; + auto system = lt::memory::create_scope(fixture.registry()); + + const auto &component = fixture.create_component(); + expect_eq(fixture.registry()->view().get_size(), 1); + fixture.check_values(*component); + + system.reset(); + expect_eq(fixture.registry()->view().get_size(), 0); + }; +}; + +Suite tick = "tick"_suite = [] { + Case { "ticking on empty registry won't throw" } = [] { + auto fixture = Fixture {}; + System { fixture.registry() }.tick(tick_info()); + }; + + Case { "ticking on non-empty registry won't throw" } = [] { + auto fixture = Fixture {}; + auto system = System { fixture.registry() }; + + fixture.create_component(); + system.tick(tick_info()); + }; +}; + +Suite tick_handles_events = "tick_handles_events"_suite = [] { + Case { "ticking clears previous tick's events" } = [] { + auto fixture = Fixture {}; + auto system = System { fixture.registry() }; + auto &surface = **fixture.create_component(); + + // flush window-creation events + system.tick(tick_info()); + expect_eq(surface.peek_events().size(), 0); + + surface.push_event(lt::surface::MovedEvent({}, {})); + expect_eq(surface.peek_events().size(), 1); + + surface.push_event(lt::surface::ButtonPressedEvent({})); + expect_eq(surface.peek_events().size(), 2); + + system.tick(tick_info()); + expect_eq(surface.peek_events().size(), 0); + }; +}; + +Suite tick_handles_requests = "tick_handles_requests"_suite = [] { + Case { "ticking clears requests" } = [] { + auto fixture = Fixture {}; + auto system = System { fixture.registry() }; + auto &surface = **fixture.create_component(); + + constexpr auto title = "ABC"; + constexpr auto position = lt::math::ivec2 { 50, 50 }; + constexpr auto resolution = lt::math::uvec2 { 50, 50 }; + + expect_eq(surface.peek_requests().size(), 0); + + surface.push_request(lt::surface::ModifyVisibilityRequest(true)); + expect_eq(surface.peek_requests().size(), 1); + system.tick(tick_info()); + expect_eq(surface.peek_requests().size(), 0); + + surface.push_request(lt::surface::ModifyTitleRequest(title)); + expect_eq(surface.peek_requests().size(), 1); + + surface.push_request(lt::surface::ModifyResolutionRequest(resolution)); + surface.push_request(lt::surface::ModifyPositionRequest(position)); + expect_eq(surface.peek_requests().size(), 1 + 2); + + surface.push_request(lt::surface::ModifyVisibilityRequest(false)); + surface.push_request(lt::surface::ModifyVisibilityRequest(true)); + surface.push_request(lt::surface::ModifyVisibilityRequest(false)); + expect_eq(surface.peek_requests().size(), 1 + 2 + 3); + + system.tick(tick_info()); + expect_eq(surface.peek_requests().size(), 0); + + expect_eq(surface.get_title(), title); + expect_eq(surface.get_position(), position); + expect_eq(surface.get_resolution(), resolution); + + lt::log::debug("EVENT COUNT: {}", surface.peek_events().size()); + for (const auto &event : surface.peek_events()) + { + const auto visitor = overloads { + [&](auto event) { lt::log::debug("event: {}", event.to_string()); }, + }; + + std::visit(visitor, event); + } + }; +};