498 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			498 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
#include <memory/reference.hpp>
 | 
						|
#include <surface/components.hpp>
 | 
						|
#include <surface/events/mouse.hpp>
 | 
						|
#include <surface/requests/surface.hpp>
 | 
						|
#include <surface/system.hpp>
 | 
						|
#include <time/timer.hpp>
 | 
						|
 | 
						|
//
 | 
						|
#include <X11/Xlib.h>
 | 
						|
#include <X11/Xutil.h>
 | 
						|
#include <X11/keysym.h>
 | 
						|
#include <X11/keysymdef.h>
 | 
						|
 | 
						|
namespace lt::surface {
 | 
						|
 | 
						|
template<int EventType>
 | 
						|
int XEventTypeEquals(Display *, XEvent *event, XPointer winptr)
 | 
						|
{
 | 
						|
	return (
 | 
						|
	    event->type == EventType
 | 
						|
	    && *(reinterpret_cast<Window *>(winptr)) == reinterpret_cast<XAnyEvent *>(event)->window
 | 
						|
	);
 | 
						|
}
 | 
						|
 | 
						|
template<class... Ts>
 | 
						|
struct overloads: Ts...
 | 
						|
{
 | 
						|
	using Ts::operator()...;
 | 
						|
};
 | 
						|
 | 
						|
void ensure_component_sanity(const SurfaceComponent &component);
 | 
						|
 | 
						|
constexpr auto all_events_mask = KeyPressMask |         //
 | 
						|
                                 KeyReleaseMask |       //
 | 
						|
                                 ButtonPressMask |      //
 | 
						|
                                 ButtonReleaseMask |    //
 | 
						|
                                 EnterWindowMask |      //
 | 
						|
                                 LeaveWindowMask |      //
 | 
						|
                                 PointerMotionMask |    //
 | 
						|
                                 KeymapStateMask |      //
 | 
						|
                                 ExposureMask |         //
 | 
						|
                                 VisibilityChangeMask | //
 | 
						|
                                 StructureNotifyMask |  //
 | 
						|
                                 FocusChangeMask |      //
 | 
						|
                                 ColormapChangeMask |   //
 | 
						|
                                 OwnerGrabButtonMask;
 | 
						|
 | 
						|
System::System(memory::Ref<ecs::Registry> registry): m_registry(std::move(registry))
 | 
						|
{
 | 
						|
	ensure(m_registry, "Failed to initialize surface system: null registry");
 | 
						|
 | 
						|
	ensure(
 | 
						|
	    m_registry->view<SurfaceComponent>().get_size() == 0,
 | 
						|
	    "Failed to initialize surface system: registry has surface component(s)"
 | 
						|
	);
 | 
						|
 | 
						|
	m_registry->connect_on_destruct<SurfaceComponent>(
 | 
						|
	    [this](ecs::Registry ®istry, ecs::EntityId entity) {
 | 
						|
		    on_surface_destruct(registry, entity);
 | 
						|
	    }
 | 
						|
	);
 | 
						|
}
 | 
						|
 | 
						|
System::~System()
 | 
						|
{
 | 
						|
	if (!m_registry)
 | 
						|
	{
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	try
 | 
						|
	{
 | 
						|
		// TODO(Light): make registry.remove not validate iterators
 | 
						|
		auto entities_to_remove = std::vector<ecs::EntityId> {};
 | 
						|
		for (auto &[entity, surface] : m_registry->view<SurfaceComponent>())
 | 
						|
		{
 | 
						|
			entities_to_remove.emplace_back(entity);
 | 
						|
		}
 | 
						|
 | 
						|
		for (auto entity : entities_to_remove)
 | 
						|
		{
 | 
						|
			m_registry->remove<SurfaceComponent>(entity);
 | 
						|
		}
 | 
						|
 | 
						|
		m_registry->disconnect_on_construct<SurfaceComponent>();
 | 
						|
		m_registry->disconnect_on_destruct<SurfaceComponent>();
 | 
						|
	}
 | 
						|
	catch (const std::exception &exp)
 | 
						|
	{
 | 
						|
		log_err("Uncaught exception in surface::~System:");
 | 
						|
		log_err("\twhat: {}", exp.what());
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void System::on_register()
 | 
						|
{
 | 
						|
}
 | 
						|
 | 
						|
void System::on_unregister()
 | 
						|
{
 | 
						|
}
 | 
						|
 | 
						|
auto System::create_component(ecs::EntityId entity, SurfaceComponent::CreateInfo info)
 | 
						|
    -> std::optional<SurfaceComponent *>
 | 
						|
try
 | 
						|
{
 | 
						|
	auto &component = m_registry->add<SurfaceComponent>(entity, info);
 | 
						|
	auto &surface = m_registry->get<SurfaceComponent>(entity);
 | 
						|
	const auto &resolution = surface.get_resolution();
 | 
						|
	const auto &position = surface.get_position();
 | 
						|
	ensure_component_sanity(surface);
 | 
						|
 | 
						|
	// TODO(Light): refactor "environment" into standalone module
 | 
						|
	// NOLINTNEXTLINE(concurrency-mt-unsafe)
 | 
						|
	auto *display_env = std::getenv("DISPLAY");
 | 
						|
	ensure(display_env != nullptr, "DISPLAY env var not found!");
 | 
						|
 | 
						|
	auto *display = XOpenDisplay(display_env);
 | 
						|
	ensure(display, "Failed to open XDisplay with DISPLAY: {}", display_env);
 | 
						|
 | 
						|
	auto root_window = XDefaultRootWindow(display);
 | 
						|
 | 
						|
	auto border_width = 0;
 | 
						|
	auto depth = 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<unsigned char, 5> { 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);
 | 
						|
	}
 | 
						|
 | 
						|
	return &component;
 | 
						|
}
 | 
						|
catch (const std::exception &exp)
 | 
						|
{
 | 
						|
	log_err("Exception thrown when on_constructing surface component");
 | 
						|
	log_err("\tentity: {}", entity);
 | 
						|
	log_err("\twhat: {}", exp.what());
 | 
						|
	m_registry->remove<SurfaceComponent>(entity);
 | 
						|
	return {};
 | 
						|
}
 | 
						|
 | 
						|
void System::on_surface_destruct(ecs::Registry ®istry, ecs::EntityId entity)
 | 
						|
{
 | 
						|
	const auto &[display, window, _] = registry.get<SurfaceComponent>(entity).get_native_data();
 | 
						|
	if (!display)
 | 
						|
	{
 | 
						|
		log_wrn("Surface component destroyed with null display");
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	XDestroyWindow(display, window);
 | 
						|
	XCloseDisplay(display);
 | 
						|
}
 | 
						|
 | 
						|
void System::handle_events(SurfaceComponent &surface)
 | 
						|
{
 | 
						|
	auto &queue = surface.m_event_queue;
 | 
						|
	queue.clear();
 | 
						|
 | 
						|
	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<KeyPressedEvent>(
 | 
						|
			    static_cast<uint32_t>(XLookupKeysym(&event.xkey, 0))
 | 
						|
			);
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		case KeyRelease:
 | 
						|
		{
 | 
						|
			queue.emplace_back<KeyReleasedEvent>(
 | 
						|
			    static_cast<uint32_t>(XLookupKeysym(&event.xkey, 0))
 | 
						|
			);
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		case ButtonPress:
 | 
						|
		{
 | 
						|
			queue.emplace_back<ButtonPressedEvent>(static_cast<int>(event.xbutton.button));
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		case ButtonRelease:
 | 
						|
		{
 | 
						|
			queue.emplace_back<ButtonReleasedEvent>(static_cast<int>(event.xbutton.button));
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		case FocusIn:
 | 
						|
		{
 | 
						|
			queue.emplace_back<GainFocusEvent>({});
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		case FocusOut:
 | 
						|
		{
 | 
						|
			queue.emplace_back<LostFocusEvent>({});
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		case ClientMessage:
 | 
						|
		{
 | 
						|
			if (event.xclient.data.l[0] == wm_delete_message)
 | 
						|
			{
 | 
						|
				queue.emplace_back<ClosedEvent>({});
 | 
						|
			}
 | 
						|
 | 
						|
			break;
 | 
						|
		}
 | 
						|
		case MotionNotify:
 | 
						|
		{
 | 
						|
			queue.emplace_back<MouseMovedEvent>(MouseMovedEvent {
 | 
						|
			    static_cast<float>(event.xmotion.x),
 | 
						|
			    static_cast<float>(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>(ResizedEvent {
 | 
						|
				    static_cast<uint32_t>(new_width),
 | 
						|
				    static_cast<uint32_t>(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>(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_err("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
 | 
						|
	uint64_t serial = NextRequest(display);
 | 
						|
 | 
						|
	// request a new window size from the X server
 | 
						|
	XResizeWindow(display, window, static_cast<uint32_t>(width), static_cast<uint32_t>(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 = Timer {};
 | 
						|
	auto event = XEvent {};
 | 
						|
	while (!XCheckIfEvent(
 | 
						|
	           display,
 | 
						|
	           &event,
 | 
						|
	           XEventTypeEquals<ConfigureNotify>,
 | 
						|
	           reinterpret_cast<XPointer>(&window) // NOLINT
 | 
						|
	       )
 | 
						|
	       || event.xconfigure.serial < serial)
 | 
						|
	{
 | 
						|
		std::this_thread::sleep_for(std::chrono::microseconds { 100 });
 | 
						|
		if (timer.elapsed_time() > lifespan)
 | 
						|
		{
 | 
						|
			log_err("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
 | 
						|
	uint64_t serial = NextRequest(display);
 | 
						|
	XMoveWindow(display, window, static_cast<int>(x), static_cast<int>(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 = Timer {};
 | 
						|
	auto event = XEvent {};
 | 
						|
	while (!XCheckIfEvent(
 | 
						|
	           display,
 | 
						|
	           &event,
 | 
						|
	           XEventTypeEquals<ConfigureNotify>,
 | 
						|
	           reinterpret_cast<XPointer>(&window) // NOLINT
 | 
						|
	       )
 | 
						|
	       || event.xconfigure.serial < serial)
 | 
						|
	{
 | 
						|
		std::this_thread::sleep_for(std::chrono::microseconds { 100 });
 | 
						|
		if (timer.elapsed_time() > lifespan)
 | 
						|
		{
 | 
						|
			log_err("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);
 | 
						|
}
 | 
						|
 | 
						|
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<SurfaceComponent>())
 | 
						|
	{
 | 
						|
		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();
 | 
						|
 | 
						|
	ensure(width != 0u, "Received bad values for surface component: width({}) == 0", width);
 | 
						|
 | 
						|
	ensure(height != 0u, "Received bad values for surface component: height({}) == 0", height);
 | 
						|
 | 
						|
	ensure(
 | 
						|
	    width < SurfaceComponent::max_dimension,
 | 
						|
	    "Received bad values for surface component: width({}) > max_dimension({})",
 | 
						|
	    width,
 | 
						|
	    SurfaceComponent::max_dimension
 | 
						|
	);
 | 
						|
 | 
						|
	ensure(
 | 
						|
	    height < SurfaceComponent::max_dimension,
 | 
						|
	    "Received bad values for surface component: height({}) > max_dimension({})",
 | 
						|
	    height,
 | 
						|
	    SurfaceComponent::max_dimension
 | 
						|
	);
 | 
						|
 | 
						|
	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
 | 
						|
	);
 | 
						|
}
 | 
						|
 | 
						|
} // namespace lt::surface
 | 
						|
 | 
						|
namespace lt {
 | 
						|
 | 
						|
} // namespace lt
 |