Compare commits

...

67 Commits

Author SHA1 Message Date
a770b67b08
wind: Show memory usage in debug output 2023-08-21 14:06:58 +02:00
e0fb90d97e
wind: Handle ftruncate() and mmap() errors properly 2023-08-21 14:06:58 +02:00
ce9d0f5856
wind: Fix client references being out-of-date in windows when disconnecting other clients
Classic "keeping a pointer to an element inside a vector after the vector is updated" bug, ah yes.
2023-08-21 14:06:58 +02:00
13a0d4860d
taskbar: Wait for terminated child windows 2023-08-21 14:06:57 +02:00
59915a5cd6
wind: Add debug keybind 2023-08-21 14:06:57 +02:00
f3fbfcf8fb
wind+libos+libui: Handle interrupted reads properly 2023-08-21 14:06:57 +02:00
f2e0c87dfc
base: Actually add the start icon to source control 2023-08-21 14:06:57 +02:00
ff1135c4b6
libui: Add Buttons 2023-08-21 14:06:57 +02:00
29d9620540
libui: Handle other mouse events 2023-08-21 14:06:57 +02:00
265c22ddaa
libui: Add aligned items using Containers, ImageWidget 2023-08-21 14:06:56 +02:00
75fb5dd98e
libui: Add VerticalLayout 2023-08-21 14:06:56 +02:00
64b93f6664
wind+libui+taskbar: Add GetScreenRect IPC, non-decorated windows, taskbar 2023-08-21 14:06:56 +02:00
105cd99fc9
libui: Actually fill window backgrounds with the correct color 2023-08-21 14:06:56 +02:00
c54d9d6bf8
libui: Add basic widget and layout system =D 2023-08-21 14:06:56 +02:00
23edc6e12a
ui+wind: Send mouse move events through IPC 2023-08-21 14:06:56 +02:00
f1d10af5b2
wind+libui: Add protocol for window close requests 2023-08-21 14:06:55 +02:00
89c11cdffe
libos+libui+wind: Use uppercase for static struct IDs to avoid confusion with fields 2023-08-21 14:06:55 +02:00
6cdc9ea40a
libui+gclient: Add basic OOP wrappers around the IPC protocol 2023-08-21 14:06:55 +02:00
ae4b383a16
wind+gclient: Add SetWindowTitle and support shm buffers 2023-08-21 14:06:55 +02:00
031b57b97a
gclient: Create two example windows 2023-08-21 14:06:55 +02:00
e37b2e7b07
wind: Handle CreateWindow IPC messages 2023-08-21 14:06:55 +02:00
78fb55112e
libui: Add CreateWindow IPC message definitions 2023-08-21 14:06:54 +02:00
311ca222d3
libos: Add basic IPC message framework 2023-08-21 14:06:54 +02:00
9d10e16760
kernel: Fix poll syscall 2023-08-21 14:06:54 +02:00
6e78cdc356
wind: Monitor data on client connections 2023-08-21 14:06:54 +02:00
20488d8413
kernel: Add POLLHUP and store it when a polled socket's peer disconnects 2023-08-21 14:06:54 +02:00
c84e4d661e
libui: Add copyright/author text 2023-08-21 14:06:54 +02:00
62b8455d52
libos: Add copyright/author comments to LocalServer and LocalClient 2023-08-21 14:06:53 +02:00
71598efb45
wind: Use init --user and pledge() 2023-08-21 14:06:53 +02:00
f60d11fc42
Update .gitignore 2023-08-21 14:06:53 +02:00
e188a61499
libos: Remove some shared pointers and change them to owned/live on the stack 2023-08-21 14:06:53 +02:00
34f54a2a16
wind: Spawn a new client process after startup
Also, create the socket after dropping privileges.
2023-08-21 14:06:52 +02:00
66a40d0951
apps: Add gclient 2023-08-21 14:06:52 +02:00
ae02a3edb0
libos: Add os::LocalClient 2023-08-21 14:06:52 +02:00
6316156f83
libui: Change 'into' to 'onto' 2023-08-21 14:06:52 +02:00
d75bce3b73
libui: Document ui::Font 2023-08-21 14:06:52 +02:00
265db4b081
libui+wind: Move some static variables inside functions 2023-08-21 14:06:52 +02:00
7fddffdbff
wind: Generate random windows on keypresses 2023-08-21 14:06:51 +02:00
4e9aea19ab
wind: Make sure windows have a minimum size to fit the titlebar 2023-08-21 14:06:51 +02:00
70e2d627bc
libui: Properly cut off the last drawn character if necessary 2023-08-21 14:06:51 +02:00
9db54a4d83
libui: Add Rect::contains(Rect) 2023-08-21 14:06:51 +02:00
3c3e5ece3d
libui: Render font characters properly with no spacing, matching the width calculations 2023-08-21 14:06:51 +02:00
707516276d
wind: Render an actual TGA mouse cursor 2023-08-21 14:06:51 +02:00
44206b1305
wind: Add a close button to windows using a TGA icon 2023-08-21 14:06:51 +02:00
357aefed54
libui: Add support for TGA image loading 2023-08-21 14:06:50 +02:00
94de39ca3d
libui: Add an interface to fill a Canvas with an array of pixels 2023-08-21 14:06:50 +02:00
382e2d7492
wind: Add window titlebars using ui::Font 2023-08-21 14:06:50 +02:00
aff08a2812
libui: Add PSF font loading and rendering 2023-08-21 14:06:50 +02:00
ea1c3a28ae
libui: Add Color::GRAY 2023-08-21 14:06:50 +02:00
1f2c8da478
libui: Rename Rect::absolute to normalized and add a new absolute function 2023-08-21 14:06:50 +02:00
9ac5b73f01
libluna: Add assignment operators to Buffer 2023-08-21 14:06:50 +02:00
8eca96de64
wind: Reorder drag sequence 2023-08-21 14:06:49 +02:00
94b1d47f1b
libui: Add Rect::relative 2023-08-21 14:06:49 +02:00
818909266d
libui: Remove redundant statement 2023-08-21 14:06:49 +02:00
954fdf43f1
libui: Add getters for separate color values 2023-08-21 14:06:49 +02:00
fd330452f5
libui: Remove unnecessary stuff 2023-08-21 14:06:49 +02:00
d3d9cc9c4f
base: Remove startup items not necessary for GUI startup 2023-08-21 14:06:48 +02:00
6696fd82a1
libui+wind: (Draggable) windows 2023-08-21 14:06:48 +02:00
ec8875e6ea
wind: Create a local server object 2023-08-21 14:06:48 +02:00
ba7be587ae
libos: Add a new LocalServer class for local domain sockets 2023-08-21 14:06:48 +02:00
0a75d3e5d8
kernel: Support listening sockets in poll() 2023-08-21 14:06:48 +02:00
5ab0ffd683
base: Start wind on startup instead of the shell 2023-08-21 14:06:48 +02:00
59c20b04ee
wind: Add a simple display server skeleton using libui
No client functionality yet, but it's a start.
2023-08-21 14:06:47 +02:00
3a61341381
libui: Add a GUI and graphics library 2023-08-21 14:06:47 +02:00
7cca3d092a
kernel: Fix negative movement in the PS/2 mouse driver 2023-08-21 14:06:47 +02:00
30ff704342
libluna+libos: Install built libraries into the system root
This is less important for libluna, as it is built into libc, but is needed to link programs compiled inside Luna with libos.
2023-08-21 14:06:32 +02:00
6e69d37d62
tools: Fix building ports from git repository 2023-08-19 19:54:37 +02:00
72 changed files with 3541 additions and 22 deletions

9
.gitignore vendored

@ -4,7 +4,14 @@ build/
initrd/boot/moon
env-local.sh
initrd/bin/**
base/usr/**
base/usr/bin/**
base/usr/include/**
base/usr/lib/**
base/usr/share/pkgdb/**
!base/usr/share/fonts/*
!base/usr/share/icons/*
base/usr/share/**
base/usr/x86_64-luna/**
.fakeroot
kernel/config.cmake
ports/out/

@ -45,8 +45,10 @@ endif()
add_subdirectory(libluna)
add_subdirectory(libos)
add_subdirectory(libui)
add_subdirectory(libc)
add_subdirectory(kernel)
add_subdirectory(apps)
add_subdirectory(tests)
add_subdirectory(shell)
add_subdirectory(wind)

@ -48,3 +48,7 @@ luna_app(input.cpp input)
luna_app(shmem-test.cpp shmem-test)
luna_app(touch.cpp touch)
luna_app(free.cpp free)
luna_app(gclient.cpp gclient)
target_link_libraries(gclient PUBLIC ui)
luna_app(taskbar.cpp taskbar)
target_link_libraries(taskbar PUBLIC ui)

75
apps/gclient.cpp Normal file

@ -0,0 +1,75 @@
#include <ui/App.h>
#include <ui/Layout.h>
struct ColorWidget : public ui::Widget
{
public:
ColorWidget(ui::Color first, ui::Color second) : m_color(first), m_first_color(first), m_second_color(second)
{
}
Result<ui::EventResult> handle_mouse_move(ui::Point) override
{
m_color = m_second_color;
return ui::EventResult::DidHandle;
}
Result<ui::EventResult> handle_mouse_leave(ui::Point) override
{
m_color = m_first_color;
return ui::EventResult::DidHandle;
}
Result<ui::EventResult> handle_mouse_down(ui::Point, int) override
{
return ui::EventResult::DidNotHandle;
}
Result<ui::EventResult> handle_mouse_up(ui::Point, int) override
{
return ui::EventResult::DidNotHandle;
}
Result<void> draw(ui::Canvas& canvas) override
{
canvas.fill(m_color);
return {};
}
private:
ui::Color m_color;
ui::Color m_first_color;
ui::Color m_second_color;
};
Result<int> luna_main(int argc, char** argv)
{
ui::App app;
TRY(app.init(argc, argv));
auto* window = TRY(ui::Window::create(ui::Rect { 200, 200, 400, 300 }));
app.set_main_window(window);
window->set_title("Main Window");
window->set_background(ui::CYAN);
ui::HorizontalLayout layout;
window->set_main_widget(layout);
ColorWidget green(ui::GREEN, ui::WHITE);
layout.add_widget(green);
ColorWidget blue(ui::BLUE, ui::GRAY);
layout.add_widget(blue);
ui::VerticalLayout sublayout;
layout.add_widget(sublayout);
ColorWidget red(ui::RED, ui::CYAN);
sublayout.add_widget(red);
ColorWidget white(ui::WHITE, ui::GREEN);
sublayout.add_widget(white);
window->draw();
return app.run();
}

49
apps/taskbar.cpp Normal file

@ -0,0 +1,49 @@
#include <os/Process.h>
#include <signal.h>
#include <sys/wait.h>
#include <ui/App.h>
#include <ui/Button.h>
#include <ui/Container.h>
#include <ui/Image.h>
#include <ui/Layout.h>
void sigchld_handler(int)
{
wait(nullptr);
}
Result<int> luna_main(int argc, char** argv)
{
ui::App app;
TRY(app.init(argc, argv));
signal(SIGCHLD, sigchld_handler);
ui::Rect screen = app.screen_rect();
ui::Rect bar = ui::Rect { ui::Point { 0, screen.height - 50 }, screen.width, 50 };
auto window = TRY(ui::Window::create(bar, false));
app.set_main_window(window);
window->set_background(ui::GRAY);
ui::HorizontalLayout layout(ui::AdjustHeight::Yes, ui::AdjustWidth::No);
window->set_main_widget(layout);
ui::Button button({ 0, 0, 50, 50 });
layout.add_widget(button);
ui::Container container({ 0, 0, 50, 50 }, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center);
button.set_widget(container);
button.set_action([] {
StringView args[] = { "/usr/bin/gclient" };
os::Process::spawn("/usr/bin/gclient", Slice<StringView> { args, 1 }, false);
});
auto image = TRY(ui::ImageWidget::load("/usr/share/icons/32x32/start-icon.tga"));
container.set_widget(*image);
window->draw();
return app.run();
}

@ -1,4 +0,0 @@
Name=motd
Description=Show the message of the day to the user.
Command=/usr/bin/cat /etc/motd
Wait=true

@ -1,6 +0,0 @@
Name=listen
Description=Start a Unix domain socket test server.
Command=/usr/bin/socket-test
StandardOutput=/dev/uart0
StandardError=/dev/uart0
Restart=true

@ -1,4 +1,6 @@
Name=login
Description=Start the command-line login program.
Command=/usr/bin/login
Description=Start the display server.
Command=/usr/bin/wind --user=selene
StandardOutput=/dev/uart0
StandardError=/dev/uart0
Restart=true

4
base/etc/user/00-taskbar Normal file

@ -0,0 +1,4 @@
Name=taskbar
Description=Start the taskbar.
Command=/usr/bin/taskbar
Restart=true

3
base/etc/user/01-gclient Normal file

@ -0,0 +1,3 @@
Name=gclient
Description=Sample user application.
Command=/usr/bin/gclient

Binary file not shown.

After

(image error) Size: 1004 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

(image error) Size: 1.0 KiB

Binary file not shown.

After

(image error) Size: 4.0 KiB

@ -43,8 +43,8 @@ static void process_mouse_event(u8 data)
packet.buttons = 0;
u8 flags = g_mouse_packet[0];
if (flags & PS2_MOUSE_X_SIGN) packet.xdelta = -packet.xdelta;
if (flags & PS2_MOUSE_Y_SIGN) packet.ydelta = -packet.ydelta;
if (flags & PS2_MOUSE_X_SIGN) packet.xdelta = -(256 - packet.xdelta);
if (flags & PS2_MOUSE_Y_SIGN) packet.ydelta = -(256 - packet.ydelta);
if (flags & PS2_MOUSE_MIDDLE_BTN) packet.buttons |= moon::MouseButton::Middle;
if (flags & PS2_MOUSE_RIGHT_BTN) packet.buttons |= moon::MouseButton::Right;

@ -65,6 +65,12 @@ class Socket : public VFS::FileInode
m_metadata.nlinks--;
}
virtual bool can_accept_connections() const = 0;
virtual bool can_read_data() const = 0;
virtual bool peer_disconnected() const = 0;
virtual ~Socket() = default;
protected:

@ -17,6 +17,21 @@ class UnixSocket : public Socket
return (m_state == Connected || m_state == Reset) && !m_data.size();
}
bool can_read_data() const override
{
return (m_state == Connected || m_state == Reset) && m_data.size();
}
bool can_accept_connections() const override
{
return !m_listen_queue.is_empty();
}
bool peer_disconnected() const override
{
return m_state == Reset;
}
Result<usize> send(const u8*, usize, int) override;
Result<usize> recv(u8*, usize, int) const override;

@ -2,6 +2,7 @@
#include "Pledge.h"
#include "fs/VFS.h"
#include "memory/MemoryManager.h"
#include "net/Socket.h"
#include "sys/Syscall.h"
#include "thread/Scheduler.h"
#include <bits/poll.h>
@ -45,10 +46,30 @@ Result<u64> sys_poll(Registers*, SyscallArgs args)
auto& inode = inodes[i];
if (!inode) continue;
if (kfds[i].events & POLLIN && !inode->will_block_if_read())
if (kfds[i].events & POLLIN)
{
fds_with_events++;
kfds[i].revents |= POLLIN;
if (inode->type() == VFS::InodeType::Socket)
{
auto socket = (Socket*)inode.ptr();
if (socket->can_read_data() || socket->can_accept_connections())
{
fds_with_events++;
kfds[i].revents |= POLLIN;
}
if (socket->peer_disconnected())
{
fds_with_events++;
kfds[i].revents |= POLLHUP;
}
}
else
{
if (!inode->will_block_if_read())
{
fds_with_events++;
kfds[i].revents |= POLLIN;
}
}
}
}

@ -8,6 +8,7 @@
#define POLLIN (1 << 0)
#define POLLERR (1 << 1)
#define POLLNVAL (1 << 2)
#define POLLHUP (1 << 3)
typedef __u64_t nfds_t;

@ -55,3 +55,8 @@ target_compile_options(luna-freestanding PRIVATE -mno-80387 -mno-mmx -mno-sse -m
target_compile_definitions(luna-freestanding PUBLIC ARCH_X86_64)
target_compile_definitions(luna PUBLIC ARCH_X86_64)
endif()
add_custom_command(
TARGET luna
COMMAND "${CMAKE_COMMAND}" -E copy ${CMAKE_CURRENT_BINARY_DIR}/libluna.a ${LUNA_BASE}/usr/lib/libluna.a
)

@ -11,6 +11,9 @@ class Buffer
Buffer(Buffer&& other);
Buffer(const Buffer& other) = delete; // For now.
Buffer& operator=(Buffer&&);
Buffer& operator=(const Buffer&) = delete;
static Result<Buffer> create_sized(usize size);
Result<void> try_resize(usize new_size);

@ -16,7 +16,7 @@ template <typename T, usize Size> class CircularQueue
{
}
bool is_empty()
bool is_empty() const
{
return m_tail.load() == m_head.load();
}
@ -76,7 +76,7 @@ template <typename T> class DynamicCircularQueue
if (m_data) free_impl(m_data);
}
bool is_empty()
bool is_empty() const
{
return m_tail.load() == m_head.load();
}

@ -15,6 +15,16 @@ Buffer::Buffer(Buffer&& other) : m_data(other.data()), m_size(other.size())
other.m_data = nullptr;
}
Buffer& Buffer::operator=(Buffer&& other)
{
if (&other == this) return *this;
if (m_data) free_impl(m_data);
m_data = other.m_data;
m_size = other.m_size;
other.m_data = nullptr;
return *this;
}
Buffer::~Buffer()
{
if (m_data) free_impl(m_data);

@ -14,6 +14,9 @@ set(SOURCES
src/Mode.cpp
src/Prompt.cpp
src/Security.cpp
src/LocalServer.cpp
src/LocalClient.cpp
src/IPC.cpp
)
add_library(os ${SOURCES})
@ -24,3 +27,8 @@ target_include_directories(os PUBLIC ${LUNA_BASE}/usr/include)
if("${LUNA_ARCH}" MATCHES "x86_64")
target_compile_definitions(os PUBLIC ARCH_X86_64)
endif()
add_custom_command(
TARGET os
COMMAND "${CMAKE_COMMAND}" -E copy ${CMAKE_CURRENT_BINARY_DIR}/libos.a ${LUNA_BASE}/usr/lib/libos.a
)

157
libos/include/os/IPC.h Normal file

@ -0,0 +1,157 @@
/**
* @file IPC.h
* @author apio (cloudapio.eu)
* @brief Inter-process communication primitives.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <os/LocalClient.h>
#include <os/LocalServer.h>
#define IPC_ENUM_SERVER(name) __##name##_SERVER_ERROR = 0
#define IPC_ENUM_CLIENT(name) __##name##_CLIENT_ERROR = 0
/**
* @brief Called to handle IPC events (client-side).
*
* @param conn The connection object being used.
* @param id The ID of the message.
* @return Result<void> Whether the operation succeded.
*/
extern Result<void> handle_ipc_client_event(os::LocalClient& conn, u8 id);
/**
* @brief Called to handle IPC events (server-side).
*
* @param conn The connection object being used.
* @param id The ID of the message.
* @return Result<void> Whether the operation succeded.
*/
extern Result<void> handle_ipc_server_event(os::LocalServer::Client& conn, u8 id);
namespace os
{
namespace IPC
{
static constexpr usize IPC_STRING_LENGTH = 256;
#define IPC_STRING(name) char name[os::IPC::IPC_STRING_LENGTH];
#define COPY_IPC_STRING(name) \
TRY(String::from_string_view(StringView::from_fixed_size_cstring(name, os::IPC::IPC_STRING_LENGTH)))
#define SET_IPC_STRING(name, value) strlcpy(name, value, os::IPC::IPC_STRING_LENGTH)
/**
* @brief Sends an IPC message without waiting for a reply.
*
* @tparam Client The type of the client interface being used to communicate.
* @tparam T The type of the message.
* @param client The connection object being used to communicate.
* @param message The IPC message.
* @return Result<void> Whether the operation succeded.
*/
template <typename Client, typename T> Result<void> send_async(Client& client, const T& message)
{
u8 id = T::ID;
TRY(client.send_typed(id));
TRY(client.send_typed(message));
return {};
}
/**
* @brief Sends an error result to the IPC connection, indicating that an operation could not be performed.
*
* @tparam Client The type of the client interface being used to communicate.
* @param client The connection object being used to communicate.
* @param error The error code.
* @return Result<void> Whether the operation succeded.
*/
template <typename Client> Result<void> send_error(Client& client, int error)
{
u8 id = 0;
TRY(client.send_typed(id));
TRY(client.send_typed(error));
return {};
}
/**
* @brief Sends an IPC message and waits for a reply (client-only).
*
* @tparam ResponseType The type of the response.
* @tparam T The type of the message.
* @param client The connection object being used to communicate.
* @param message The IPC message.
* @param handler The function used to handle messages that do not match the reply.
* @return Result<ResponseType> An error, or the response.
*/
template <typename ResponseType, typename T>
Result<ResponseType> send_sync(os::LocalClient& client, const T& message,
decltype(handle_ipc_client_event) handler = handle_ipc_client_event)
{
u8 id = T::ID;
TRY(client.send_typed(id));
TRY(client.send_typed(message));
// We allow receiving 5 messages of different types, but if those have passed and we still don't have a
// reply, fail with ENOMSG.
int max_other_messages = 5;
while (max_other_messages)
{
u8 response_id;
auto rc = client.recv_typed(response_id);
if (rc.has_error() && (rc.error() == EAGAIN || rc.error() == EINTR)) continue;
if (response_id == 0) // Error result
{
while (1)
{
int code;
rc = client.recv_typed(code);
if (rc.has_error() && (rc.error() == EAGAIN || rc.error() == EINTR)) continue;
return err(code);
}
}
if (response_id != ResponseType::ID)
{
TRY(handler(client, response_id));
max_other_messages--;
continue;
}
while (1)
{
ResponseType response;
rc = client.recv_typed(response);
if (rc.has_error() && (rc.error() == EAGAIN || rc.error() == EINTR)) continue;
return response;
}
}
return err(ENOMSG);
}
/**
* @brief Check for new IPC messages on a connection and handle them appropriately.
*
* @param client The client connection.
* @param handler The function used to handle messages.
* @return Result<void> Whether the operation succeded.
*/
Result<void> check_for_messages(os::LocalClient& client,
decltype(handle_ipc_client_event) handler = handle_ipc_client_event);
/**
* @brief Check for new IPC messages on a connection and handle them appropriately.
*
* @param server The server connection.
* @param handler The function used to handle messages.
* @return Result<void> Whether the operation succeded.
*/
Result<void> check_for_messages(os::LocalServer::Client& server,
decltype(handle_ipc_server_event) handler = handle_ipc_server_event);
}
}

@ -0,0 +1,99 @@
/**
* @file LocalClient.h
* @author apio (cloudapio.eu)
* @brief UNIX local domain client class.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/OwnedPtr.h>
#include <luna/StringView.h>
namespace os
{
/**
* @brief A client used to connect to a local server socket.
*/
class LocalClient
{
public:
/**
* @brief Create a new client object and connect it to a local server.
*
* @param path The path of the server socket to connect to.
* @param blocking Whether the client should block if no data is available and recv() is called.
* @return Result<OwnedPtr<LocalClient>> An error, or a new client object.
*/
static Result<OwnedPtr<LocalClient>> connect(StringView path, bool blocking);
/**
* @brief Return the underlying socket file descriptor used by this object.
*
* @return int The file descriptor.
*/
int fd() const
{
return m_fd;
}
/**
* @brief Read arbitrary data from the server. The call will block if there is no data and this object has not
* been created as non-blocking.
*
* @param buf The buffer to read data into.
* @param length The maximum amount of bytes to read.
* @return Result<usize> An error, or the number of bytes read.
*/
Result<usize> recv(u8* buf, usize length);
/**
* @brief Read an object from the server. The call will block if there is no data and this object has not been
* created as non-blocking.
*
* @tparam T The type of the object.
* @param out A reference to the object to read data into.
* @return Result<void> Whether the operation succeded.
*/
template <typename T> Result<void> recv_typed(T& out)
{
TRY(recv((u8*)&out, sizeof(T)));
return {};
}
/**
* @brief Send arbitrary data to the server.
*
* @param buf The buffer to send data from.
* @param length The amount of bytes to send.
* @return Result<usize> An error, or the number of bytes actually sent.
*/
Result<usize> send(const u8* buf, usize length);
/**
* @brief Send an object to the server.
*
* @tparam T The type of the object.
* @param out A reference to the object to send data from.
* @return Result<void> Whether the operation succeded.
*/
template <typename T> Result<void> send_typed(const T& out)
{
TRY(send((const u8*)&out, sizeof(T)));
return {};
}
/**
* @brief Disconnect from the attached server.
*
* This will make any further reads on this connection return ECONNRESET, and will make this object invalid.
*/
void disconnect();
~LocalClient();
private:
int m_fd;
};
}

@ -0,0 +1,142 @@
/**
* @file LocalServer.h
* @author apio (cloudapio.eu)
* @brief UNIX local domain server class.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/OwnedPtr.h>
#include <luna/Result.h>
#include <luna/StringView.h>
namespace os
{
/**
* @brief A local domain server, used to communicate between processes on the same machine.
*/
class LocalServer
{
public:
/**
* @brief Create a new server object and bind it to a local address.
*
* @param path The path to use for the server socket.
* @param blocking Whether the server should block if no connections are available when calling accept().
* @return Result<OwnedPtr<LocalServer>> An error, or a new server object.
*/
static Result<OwnedPtr<LocalServer>> create(StringView path, bool blocking);
/**
* @brief Activate the server and start listening for connections.
*
* @param backlog The number of unaccepted connections to keep.
* @return Result<void> Whether the operation succeded.
*/
Result<void> listen(int backlog);
/**
* @brief Return the underlying socket file descriptor used by this object.
*
* @return int The file descriptor.
*/
int fd() const
{
return m_fd;
}
/**
* @brief An interface to communicate with clients connected to a local server.
*/
class Client
{
public:
/**
* @brief Read arbitrary data from the client. The call will block if there is no data and the parent server
* object has not been created as non-blocking.
*
* @param buf The buffer to read data into.
* @param length The maximum amount of bytes to read.
* @return Result<usize> An error, or the number of bytes read.
*/
Result<usize> recv(u8* buf, usize length);
/**
* @brief Read an object from the client. The call will block if there is no data and the parent server
* object has not been created as non-blocking.
*
* @tparam T The type of the object.
* @param out A reference to the object to read data into.
* @return Result<void> Whether the operation succeded.
*/
template <typename T> Result<void> recv_typed(T& out)
{
TRY(recv((u8*)&out, sizeof(T)));
return {};
}
/**
* @brief Send arbitrary data to the client.
*
* @param buf The buffer to send data from.
* @param length The amount of bytes to send.
* @return Result<usize> An error, or the number of bytes actually sent.
*/
Result<usize> send(const u8* buf, usize length);
/**
* @brief Send an object to the client.
*
* @tparam T The type of the object.
* @param out A reference to the object to send data from.
* @return Result<void> Whether the operation succeded.
*/
template <typename T> Result<void> send_typed(const T& out)
{
TRY(send((const u8*)&out, sizeof(T)));
return {};
}
/**
* @brief Disconnect from the attached client.
*
* This will make any further reads on the client return ECONNRESET, and will make this object invalid.
*/
void disconnect();
/**
* @brief Return the underlying socket file descriptor used by this object.
*
* @return int The file descriptor.
*/
int fd() const
{
return m_fd;
}
Client(Client&& other);
Client(int fd);
~Client();
private:
int m_fd;
};
/**
* @brief Accept a new incoming connection and return a handle to it. If there are no incoming connections,
* accept() either blocks until there is one (if the object was created with blocking=true), or returns EAGAIN
* (if the object was created with blocking=false).
*
* @return Result<Client> An error, or a handle to the new connection.
*/
Result<Client> accept();
~LocalServer();
private:
int m_fd;
bool m_blocking;
};
}

43
libos/src/IPC.cpp Normal file

@ -0,0 +1,43 @@
/**
* @file IPC.cpp
* @author apio (cloudapio.eu)
* @brief Inter-process communication primitives.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <os/IPC.h>
namespace os::IPC
{
Result<void> check_for_messages(os::LocalClient& client, decltype(handle_ipc_client_event) handler)
{
u8 id;
auto rc = client.recv_typed(id);
if (rc.has_error())
{
if (rc.error() == EAGAIN) return {}; // No messages, and the caller does not want us to block.
if (rc.error() == EINTR)
return {}; // Let the caller check for anything having happened because a signal handler ran.
return rc.release_error();
}
return handler(client, id);
}
Result<void> check_for_messages(os::LocalServer::Client& client, decltype(handle_ipc_server_event) handler)
{
u8 id;
auto rc = client.recv_typed(id);
if (rc.has_error())
{
if (rc.error() == EAGAIN) return {}; // No messages, and the caller does not want us to block.
if (rc.error() == EINTR)
return {}; // Let the caller check for anything having happened because a signal handler ran.
return rc.release_error();
}
return handler(client, id);
}
}

68
libos/src/LocalClient.cpp Normal file

@ -0,0 +1,68 @@
/**
* @file LocalClient.cpp
* @author apio (cloudapio.eu)
* @brief UNIX local domain client class.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <errno.h>
#include <fcntl.h>
#include <os/LocalClient.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
namespace os
{
Result<OwnedPtr<LocalClient>> LocalClient::connect(StringView path, bool blocking)
{
auto client = TRY(make_owned<LocalClient>());
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0) return err(errno);
struct sockaddr_un un;
un.sun_family = AF_UNIX;
strncpy(un.sun_path, path.chars(), sizeof(un.sun_path));
if (::connect(sockfd, (struct sockaddr*)&un, sizeof(un)) < 0)
{
close(sockfd);
return err(errno);
}
if (!blocking) { fcntl(sockfd, F_SETFL, O_NONBLOCK); }
fcntl(sockfd, F_SETFD, FD_CLOEXEC);
client->m_fd = sockfd;
return client;
}
LocalClient::~LocalClient()
{
close(m_fd);
}
Result<usize> LocalClient::recv(u8* buf, usize length)
{
ssize_t nread = read(m_fd, buf, length);
if (nread < 0) return err(errno);
return nread;
}
Result<usize> LocalClient::send(const u8* buf, usize length)
{
ssize_t nwrite = write(m_fd, buf, length);
if (nwrite < 0) return err(errno);
return nwrite;
}
void LocalClient::disconnect()
{
close(m_fd);
m_fd = -1;
}
}

101
libos/src/LocalServer.cpp Normal file

@ -0,0 +1,101 @@
/**
* @file LocalServer.cpp
* @author apio (cloudapio.eu)
* @brief UNIX local domain server class.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <errno.h>
#include <fcntl.h>
#include <os/FileSystem.h>
#include <os/LocalServer.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
namespace os
{
Result<OwnedPtr<LocalServer>> LocalServer::create(StringView path, bool blocking)
{
auto server = TRY(make_owned<LocalServer>());
(void)os::FileSystem::remove(path); // We explicitly ignore any error here, either it doesn't exist (which is
// fine), or it cannot be removed, which will make bind() fail later.
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0) return err(errno);
struct sockaddr_un un;
un.sun_family = AF_UNIX;
strncpy(un.sun_path, path.chars(), sizeof(un.sun_path));
if (bind(sockfd, (struct sockaddr*)&un, sizeof(un)) < 0)
{
close(sockfd);
return err(errno);
}
if (!blocking) { fcntl(sockfd, F_SETFL, O_NONBLOCK); }
server->m_blocking = blocking;
fcntl(sockfd, F_SETFD, FD_CLOEXEC);
server->m_fd = sockfd;
return server;
}
Result<void> LocalServer::listen(int backlog)
{
if (::listen(m_fd, backlog) < 0) return err(errno);
return {};
}
Result<LocalServer::Client> LocalServer::accept()
{
int fd = ::accept(m_fd, nullptr, nullptr);
if (fd < 0) return err(errno);
if (!m_blocking) fcntl(fd, F_SETFL, O_NONBLOCK);
return Client { fd };
}
LocalServer::~LocalServer()
{
close(m_fd);
}
LocalServer::Client::Client(Client&& other) : m_fd(other.m_fd)
{
other.m_fd = -1;
}
LocalServer::Client::Client(int fd) : m_fd(fd)
{
}
LocalServer::Client::~Client()
{
if (m_fd >= 0) close(m_fd);
}
Result<usize> LocalServer::Client::recv(u8* buf, usize length)
{
ssize_t nread = read(m_fd, buf, length);
if (nread < 0) return err(errno);
return nread;
}
Result<usize> LocalServer::Client::send(const u8* buf, usize length)
{
ssize_t nwrite = write(m_fd, buf, length);
if (nwrite < 0) return err(errno);
return nwrite;
}
void LocalServer::Client::disconnect()
{
close(m_fd);
m_fd = -1;
}
}

25
libui/CMakeLists.txt Normal file

@ -0,0 +1,25 @@
# The UI and graphics library for Luna.
file(GLOB HEADERS include/ui/*.h)
set(SOURCES
${HEADERS}
include/ui/ipc/Server.h
include/ui/ipc/Client.h
src/Canvas.cpp
src/Rect.cpp
src/Font.cpp
src/Image.cpp
src/App.cpp
src/Window.cpp
src/Layout.cpp
src/Alignment.cpp
src/Container.cpp
src/Button.cpp
)
add_library(ui ${SOURCES})
target_compile_options(ui PRIVATE ${COMMON_FLAGS} -fno-threadsafe-statics)
target_include_directories(ui PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include/)
target_include_directories(ui PUBLIC ${LUNA_BASE}/usr/include)
target_link_libraries(ui PUBLIC os)

@ -0,0 +1,30 @@
/**
* @file Alignment.h
* @author apio (cloudapio.eu)
* @brief UI component alignment.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <ui/Rect.h>
namespace ui
{
enum class VerticalAlignment
{
Top,
Center,
Bottom
};
enum class HorizontalAlignment
{
Left,
Center,
Right
};
Rect align(Rect container, Rect contained, VerticalAlignment valign, HorizontalAlignment halign);
}

65
libui/include/ui/App.h Normal file

@ -0,0 +1,65 @@
/**
* @file App.h
* @author apio (cloudapio.eu)
* @brief UI application event loop.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/HashMap.h>
#include <os/LocalClient.h>
#include <ui/Window.h>
namespace ui
{
class App
{
public:
App();
~App();
Result<void> init(int, char**);
Result<int> run();
Rect screen_rect();
os::LocalClient& client()
{
return *m_client;
}
void set_should_close(bool b)
{
m_should_close = b;
}
void set_main_window(Window* window)
{
check(!m_main_window);
m_main_window = window;
}
Window* main_window()
{
return m_main_window;
}
Result<void> register_window(OwnedPtr<Window>&& window, Badge<Window>);
void unregister_window(Window* window, Badge<Window>);
Result<void> handle_ipc_event(u8 id);
static App& the();
private:
static App* s_app;
OwnedPtr<os::LocalClient> m_client;
Window* m_main_window { nullptr };
HashMap<int, OwnedPtr<Window>> m_windows;
bool m_should_close { false };
Window* find_window(int id);
};
}

35
libui/include/ui/Button.h Normal file

@ -0,0 +1,35 @@
/**
* @file Button.h
* @author apio (cloudapio.eu)
* @brief A clickable component that triggers an action when pressed.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <ui/Widget.h>
namespace ui
{
class Button : public Widget
{
public:
Button(Rect rect);
void set_widget(Widget& widget);
void set_action(void (*action)(void));
Result<EventResult> handle_mouse_move(Point position) override;
Result<EventResult> handle_mouse_leave(Point position) override;
Result<EventResult> handle_mouse_down(Point position, int buttons) override;
Result<EventResult> handle_mouse_up(Point position, int buttons) override;
Result<void> draw(Canvas& canvas) override;
private:
bool m_hovered { false };
bool m_clicked { false };
Widget* m_child;
void (*m_action)(void);
};
}

73
libui/include/ui/Canvas.h Normal file

@ -0,0 +1,73 @@
/**
* @file Canvas.h
* @author apio (cloudapio.eu)
* @brief Drawable surfaces.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/Result.h>
#include <luna/Types.h>
#include <ui/Color.h>
#include <ui/Point.h>
#include <ui/Rect.h>
namespace ui
{
/**
* @brief A drawable surface.
*/
struct Canvas
{
int width;
int height;
int stride;
u8* ptr;
/**
* @brief Create a new Canvas object.
*
* @param ptr The memory to use for the canvas. It must be of at least width * height * 4 bytes of length.
* @param width The width of the canvas.
* @param height The height of the canvas.
* @return Canvas The new Canvas object.
*/
static Canvas create(u8* ptr, int width, int height);
/**
* @brief Return a new Canvas that represents a subsection of the current one.
*
* @param rect The dimensions of the new canvas. If these exceed the bounds of the current canvas, they will be
* clamped.
* @return Canvas The new Canvas object.
*/
Canvas subcanvas(Rect rect);
/**
* @brief Return the dimensions of the current canvas.
*
* @return Rect This canvas's dimensions, as a Rect object.
*/
Rect rect()
{
return Rect { .pos = { 0, 0 }, .width = width, .height = height };
}
/**
* @brief Fill the entire canvas with one color.
*
* @param color The color to use.
*/
void fill(Color color);
/**
* @brief Fill the canvas with pixels.
*
* @param pixels The array of pixels (must be at least width*height).
* @param stride The number of pixels to skip to go to the next line.
*/
void fill(u32* pixels, int stride);
};
};

113
libui/include/ui/Color.h Normal file

@ -0,0 +1,113 @@
/**
* @file Color.h
* @author apio (cloudapio.eu)
* @brief RGBA colors.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/Types.h>
namespace ui
{
/**
* @brief A 32-bit ARGB color.
*/
struct Color
{
union {
u32 raw;
u8 colors[4];
};
/**
* @brief Return the blue value of this color.
*
* @return constexpr u8 The blue value.
*/
constexpr u8 red() const
{
return colors[2];
}
/**
* @brief Return the green value of this color.
*
* @return constexpr u8 The green value.
*/
constexpr u8 green() const
{
return colors[1];
}
/**
* @brief Return the blue value of this color.
*
* @return constexpr u8 The blue value.
*/
constexpr u8 blue() const
{
return colors[0];
}
/**
* @brief Return the alpha value of this color.
*
* @return constexpr u8 The alpha value.
*/
constexpr u8 alpha() const
{
return colors[3];
}
/**
* @brief Construct a new color from a 32-bit ARGB integer.
*
* @param raw The integer representing the color.
* @return constexpr Color The new color.
*/
static constexpr Color from_u32(u32 raw)
{
return Color { .raw = raw };
}
/**
* @brief Construct a new color from its separate RGBA values (from 0 to 255).
*
* @param red The red value.
* @param green The green value.
* @param blue The blue value.
* @param alpha The alpha value.
* @return constexpr Color The new color.
*/
static constexpr Color from_rgba(u8 red, u8 green, u8 blue, u8 alpha)
{
return Color { .colors = { blue, green, red, alpha } };
}
/**
* @brief Construct a new color from its separate RGB values (from 0 to 255).
*
* @param red The red value.
* @param green The green value.
* @param blue The blue value.
* @return constexpr Color The new color.
*/
static constexpr Color from_rgb(u8 red, u8 green, u8 blue)
{
return from_rgba(red, green, blue, 0xff);
}
};
static constexpr Color WHITE = Color::from_rgb(0xff, 0xff, 0xff);
static constexpr Color BLACK = Color::from_rgb(0x00, 0x00, 0x00);
static constexpr Color GRAY = Color::from_rgb(0x80, 0x80, 0x80);
static constexpr Color BLUE = Color::from_rgb(0x00, 0x00, 0xff);
static constexpr Color GREEN = Color::from_rgb(0x00, 0xff, 0x00);
static constexpr Color RED = Color::from_rgb(0xff, 0x00, 0x00);
static constexpr Color CYAN = Color::from_rgb(0x00, 0xff, 0xff);
};

@ -0,0 +1,34 @@
/**
* @file Container.h
* @author apio (cloudapio.eu)
* @brief A container widget to pad and align objects inside it.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <ui/Alignment.h>
#include <ui/Widget.h>
namespace ui
{
class Container : public Widget
{
public:
Container(Rect rect, VerticalAlignment valign, HorizontalAlignment halign);
void set_widget(Widget& widget);
Result<EventResult> handle_mouse_move(Point position) override;
Result<EventResult> handle_mouse_leave(Point position) override;
Result<EventResult> handle_mouse_down(Point position, int buttons) override;
Result<EventResult> handle_mouse_up(Point position, int buttons) override;
Result<void> draw(Canvas& canvas) override;
private:
Widget* m_widget;
VerticalAlignment m_valign;
HorizontalAlignment m_halign;
};
}

120
libui/include/ui/Font.h Normal file

@ -0,0 +1,120 @@
/**
* @file Font.h
* @author apio (cloudapio.eu)
* @brief PSF font loading and rendering.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/Buffer.h>
#include <luna/SharedPtr.h>
#include <os/Path.h>
#include <ui/Canvas.h>
#define PSF_FONT_MAGIC 0x864ab572
namespace ui
{
/**
* @brief A class holding PSF font data, used for direct rendering of glyphs into a canvas.
*/
class Font : public Shareable
{
public:
/**
* @brief An enum used to select a font weight when loading a font.
*/
enum FontWeight
{
Regular,
Bold,
};
/**
* @brief Load a Font object from a font file.
*
* @param path The full path to the font file.
* @return Result<SharedPtr<Font>> An error, or the loaded Font object.
*/
static Result<SharedPtr<Font>> load(const os::Path& path);
/**
* @brief Load a system font by name.
*
* @param name The name of the font to load (the default system font is "Tamsyn").
* @param weight The weight of the font (regular or bold).
* @return Result<SharedPtr<Font>> An error, or the loaded Font object.
*/
static Result<SharedPtr<Font>> load_builtin(StringView name, FontWeight weight);
/**
* @brief Return a pointer to the system's default font.
*
* @return SharedPtr<Font> The default font.
*/
static SharedPtr<Font> default_font();
/**
* @brief Return a pointer to the system's default bold font.
*
* @return SharedPtr<Font> The default bold font.
*/
static SharedPtr<Font> default_bold_font();
/**
* @brief Render a single Unicode code point onto a canvas, using this font's glyphs.
*
* @param codepoint The code point to render.
* @param color The color to draw the code point in.
* @param canvas The canvas to use.
*/
void render(wchar_t codepoint, ui::Color color, ui::Canvas& canvas);
/**
* @brief Render a Unicode text string onto a canvas, using this font's glyphs.
*
* @param text The string to render (must be null-terminated).
* @param color The color to draw the code point in.
* @param canvas The canvas to use.
*/
void render(const wchar_t* text, ui::Color color, ui::Canvas& canvas);
/**
* @brief Return the width of this font's glyphs.
*
* @return int The width.
*/
int width() const
{
return m_psf_header.width;
}
/**
* @brief Return the height of this font's glyphs.
*
* @return int The height.
*/
int height() const
{
return m_psf_header.height;
}
private:
struct PSFHeader
{
u32 magic;
u32 version; // zero
u32 headersize;
u32 flags; // 0 if there's no unicode table
u32 numglyph;
u32 bytesperglyph;
int height;
int width;
};
PSFHeader m_psf_header;
Buffer m_font_data;
};
};

96
libui/include/ui/Image.h Normal file

@ -0,0 +1,96 @@
/**
* @file Image.h
* @author apio (cloudapio.eu)
* @brief TGA image loading and rendering.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/Buffer.h>
#include <luna/SharedPtr.h>
#include <os/Path.h>
#include <ui/Widget.h>
namespace ui
{
/**
* @brief An image in the TGA file format.
*/
class Image : public Shareable
{
public:
/**
* @brief Load a new TGA image from a file.
*
* @param path The path to open.
* @return Result<SharedPtr<Image>> An error, or a new Image object.
*/
static Result<SharedPtr<Image>> load(const os::Path& path);
/**
* @brief Return the array of pixels contained in the image.
*
* @return u32* The array of pixels.
*/
u32* pixels()
{
return (u32*)m_image_data.data();
}
/**
* @brief Return the width of the image.
*
* @return u16 The width.
*/
u16 width()
{
return m_tga_header.w;
}
/**
* @brief Return the height of the image.
*
* @return u16 The height.
*/
u16 height()
{
return m_tga_header.h;
}
private:
struct [[gnu::packed]] TGAHeader
{
u8 idlen;
u8 colormap;
u8 encoding;
u16 cmaporig, cmaplen;
u8 cmapent;
u16 x;
u16 y;
u16 w;
u16 h;
u8 bpp;
u8 pixeltype;
};
TGAHeader m_tga_header;
Buffer m_image_data;
};
class ImageWidget final : public Widget
{
public:
static Result<OwnedPtr<ImageWidget>> load(const os::Path& path);
Result<EventResult> handle_mouse_move(Point position) override;
Result<EventResult> handle_mouse_leave(Point position) override;
Result<EventResult> handle_mouse_down(Point position, int buttons) override;
Result<EventResult> handle_mouse_up(Point position, int buttons) override;
Result<void> draw(Canvas& canvas) override;
private:
SharedPtr<Image> m_image;
};
}

69
libui/include/ui/Layout.h Normal file

@ -0,0 +1,69 @@
/**
* @file Layout.h
* @author apio (cloudapio.eu)
* @brief Layout widgets to organize content.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/Vector.h>
#include <ui/Widget.h>
namespace ui
{
enum class AdjustHeight
{
No,
Yes
};
enum class AdjustWidth
{
No,
Yes
};
class HorizontalLayout final : public Widget
{
public:
HorizontalLayout(AdjustHeight adjust_height = AdjustHeight::Yes, AdjustWidth adjust_width = AdjustWidth::Yes);
Result<EventResult> handle_mouse_move(Point position) override;
Result<EventResult> handle_mouse_leave(Point position) override;
Result<EventResult> handle_mouse_down(Point position, int buttons) override;
Result<EventResult> handle_mouse_up(Point position, int buttons) override;
Result<void> draw(Canvas& canvas) override;
Result<void> add_widget(Widget& widget);
private:
Vector<Widget*> m_widgets;
AdjustHeight m_adjust_height;
AdjustWidth m_adjust_width;
int m_used_width;
};
class VerticalLayout final : public Widget
{
public:
VerticalLayout(AdjustHeight adjust_height = AdjustHeight::Yes, AdjustWidth adjust_width = AdjustWidth::Yes);
Result<EventResult> handle_mouse_move(Point position) override;
Result<EventResult> handle_mouse_leave(Point position) override;
Result<EventResult> handle_mouse_down(Point position, int buttons) override;
Result<EventResult> handle_mouse_up(Point position, int buttons) override;
Result<void> draw(Canvas& canvas) override;
Result<void> add_widget(Widget& widget);
private:
Vector<Widget*> m_widgets;
AdjustHeight m_adjust_height;
AdjustWidth m_adjust_width;
int m_used_height;
};
}

21
libui/include/ui/Mouse.h Normal file

@ -0,0 +1,21 @@
/**
* @file Mouse.h
* @author apio (cloudapio.eu)
* @brief Mouse buttons.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <moon/Mouse.h>
namespace ui
{
enum MouseButtons
{
LEFT = moon::Left,
MIDDLE = moon::Middle,
RIGHT = moon::Right,
};
}

22
libui/include/ui/Point.h Normal file

@ -0,0 +1,22 @@
/**
* @file Point.h
* @author apio (cloudapio.eu)
* @brief 2D space points.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
namespace ui
{
/**
* @brief A point in 2D space.
*/
struct Point
{
int x { 0 };
int y { 0 };
};
}

81
libui/include/ui/Rect.h Normal file

@ -0,0 +1,81 @@
/**
* @file Rect.h
* @author apio (cloudapio.eu)
* @brief A simple 2D rectangle representation.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <ui/Point.h>
namespace ui
{
/**
* @brief A simple rectangle.
*/
struct Rect
{
Point pos;
int width;
int height;
/**
* @brief Check if a point is contained in this rectangle.
*
* @param point The point to check.
* @return true The point is contained inside the rectangle.
* @return false The point is not contained inside the rectangle.
*/
bool contains(Point point);
/**
* @brief Check if another rectangle is contained in this one.
*
* @param point The rectangle to check.
* @return true The other rectangle is contained inside this one.
* @return false The other rectangle is not contained inside this one.
*/
bool contains(Rect rect);
/**
* @brief Normalize a point to fit inside this rectangle.
*
* @param point The original point.
* @return Point The normalized point.
*/
Point normalize(Point point);
/**
* @brief Transform an absolute position to a position relative to this rectangle.
*
* @param pos The original absolute position.
* @return Point The position relative to this rectangle.
*/
Point relative(Point pos);
/**
* @brief Transform a position relative to this rectangle to an absolute position.
*
* @param pos The original relative position.
* @return Point The absolute position.
*/
Point absolute(Point pos);
/**
* @brief Transform another rectangle relative to this one to an absolute rectangle.
*
* @param rect The original relative rectangle.
* @return Point The absolute rectangle.
*/
Rect absolute(Rect rect);
/**
* @brief Return a copy of this rectangle with no negative values (normalized to 0).
*
* @return Rect The new rectangle.
*/
Rect normalized();
};
}

68
libui/include/ui/Widget.h Normal file

@ -0,0 +1,68 @@
/**
* @file Widget.h
* @author apio (cloudapio.eu)
* @brief Abstract widget class.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/Result.h>
#include <ui/Canvas.h>
#include <ui/Point.h>
#include <ui/Rect.h>
namespace ui
{
class Window;
enum class EventResult
{
DidHandle,
DidNotHandle,
};
class Widget
{
public:
virtual Result<EventResult> handle_mouse_move(Point position);
virtual Result<EventResult> handle_mouse_down(Point position, int buttons);
virtual Result<EventResult> handle_mouse_up(Point position, int buttons);
virtual Result<EventResult> handle_mouse_leave(Point position);
virtual Result<void> draw(Canvas& canvas);
void set_window(Window* window, Rect rect, Badge<Window>)
{
m_window = window;
m_rect = rect;
}
void set_parent(Widget* parent)
{
m_parent = parent;
m_window = parent->m_window;
}
Widget* parent()
{
return m_parent;
}
Window* window()
{
return m_window;
}
Rect& rect()
{
return m_rect;
}
protected:
Widget* m_parent { nullptr };
Window* m_window;
Rect m_rect { 0, 0, 50, 50 };
};
}

66
libui/include/ui/Window.h Normal file

@ -0,0 +1,66 @@
/**
* @file Window.h
* @author apio (cloudapio.eu)
* @brief UI windows.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <luna/OwnedPtr.h>
#include <luna/StringView.h>
#include <ui/Canvas.h>
#include <ui/Mouse.h>
#include <ui/Rect.h>
#include <ui/Widget.h>
namespace ui
{
class Window
{
public:
static Result<Window*> create(Rect rect, bool decorated = true);
void set_title(StringView title);
void set_background(Color color)
{
m_background = color;
}
void set_main_widget(Widget& widget)
{
check(!m_main_widget);
widget.set_window(this, m_canvas.rect(), {});
m_main_widget = &widget;
}
Canvas& canvas()
{
return m_canvas;
}
void update();
void close();
Result<void> draw();
Result<void> handle_mouse_move(ui::Point position);
Result<void> handle_mouse_buttons(ui::Point position, int buttons);
int id() const
{
return m_id;
}
~Window();
private:
int m_id;
Canvas m_canvas;
Widget* m_main_widget { nullptr };
Color m_background { ui::BLACK };
Option<int> m_old_mouse_buttons;
};
}

@ -0,0 +1,55 @@
/**
* @file ipc/Client.h
* @author apio (cloudapio.eu)
* @brief IPC message definitions for UI messages sent to the client.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <os/IPC.h>
#include <ui/Point.h>
namespace ui
{
enum ClientMessages : u8
{
IPC_ENUM_CLIENT(ui),
CREATE_WINDOW_RESPONSE_ID,
WINDOW_CLOSE_REQUEST_ID,
MOUSE_EVENT_REQUEST_ID,
GET_SCREEN_RECT_RESPONSE_ID
};
struct CreateWindowResponse
{
static constexpr u8 ID = CREATE_WINDOW_RESPONSE_ID;
int window;
IPC_STRING(shm_path);
};
struct WindowCloseRequest
{
static constexpr u8 ID = WINDOW_CLOSE_REQUEST_ID;
int window;
};
struct MouseEventRequest
{
static constexpr u8 ID = MOUSE_EVENT_REQUEST_ID;
int window;
Point position;
int buttons;
};
struct GetScreenRectResponse
{
static constexpr u8 ID = GET_SCREEN_RECT_RESPONSE_ID;
Rect rect;
};
}

@ -0,0 +1,66 @@
/**
* @file ipc/Server.h
* @author apio (cloudapio.eu)
* @brief IPC message definitions for UI messages sent to the server.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#pragma once
#include <os/IPC.h>
#include <ui/Color.h>
#include <ui/Rect.h>
#include <ui/ipc/Client.h>
namespace ui
{
enum ServerMessages : u8
{
IPC_ENUM_SERVER(ui),
CREATE_WINDOW_ID,
SET_WINDOW_TITLE_ID,
INVALIDATE_ID,
CLOSE_WINDOW_ID,
GET_SCREEN_RECT_ID,
};
struct CreateWindowRequest
{
using ResponseType = CreateWindowResponse;
static constexpr u8 ID = CREATE_WINDOW_ID;
ui::Rect rect;
bool decorated;
};
struct SetWindowTitleRequest
{
static constexpr u8 ID = SET_WINDOW_TITLE_ID;
int window;
IPC_STRING(title);
};
struct InvalidateRequest
{
static constexpr u8 ID = INVALIDATE_ID;
int window;
};
struct CloseWindowRequest
{
static constexpr u8 ID = CLOSE_WINDOW_ID;
int window;
};
struct GetScreenRectRequest
{
using ResponseType = GetScreenRectResponse;
static constexpr u8 ID = GET_SCREEN_RECT_ID;
int _shadow; // Unused.
};
}

40
libui/src/Alignment.cpp Normal file

@ -0,0 +1,40 @@
/**
* @file Alignment.cpp
* @author apio (cloudapio.eu)
* @brief UI component alignment.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <ui/Alignment.h>
namespace ui
{
Rect align(Rect container, Rect contained, VerticalAlignment valign, HorizontalAlignment halign)
{
Rect result;
result.width = contained.width;
result.height = contained.height;
result.pos.y = container.pos.y;
result.pos.x = container.pos.x;
switch (valign)
{
case VerticalAlignment::Top: break;
case VerticalAlignment::Center: result.pos.y += (container.height - contained.height) / 2; break;
case VerticalAlignment::Bottom: result.pos.y += container.height - contained.height; break;
default: break;
}
switch (halign)
{
case HorizontalAlignment::Left: break;
case HorizontalAlignment::Center: result.pos.x += (container.width - contained.width) / 2; break;
case HorizontalAlignment::Right: result.pos.x += container.width - contained.width; break;
default: break;
}
return result;
}
}

128
libui/src/App.cpp Normal file

@ -0,0 +1,128 @@
/**
* @file App.cpp
* @author apio (cloudapio.eu)
* @brief UI application event loop.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <os/ArgumentParser.h>
#include <os/File.h>
#include <os/IPC.h>
#include <ui/App.h>
#include <ui/ipc/Client.h>
#include <ui/ipc/Server.h>
Result<void> handle_ipc_client_event(os::LocalClient&, u8 id)
{
return ui::App::the().handle_ipc_event(id);
}
namespace ui
{
App* App::s_app { nullptr };
App::App()
{
s_app = this;
}
App::~App()
{
s_app = nullptr;
}
Result<void> App::init(int argc, char** argv)
{
StringView socket_path = "/tmp/wind.sock";
os::ArgumentParser parser;
parser.add_description("A UI application."_sv);
parser.add_system_program_info(argv[0]);
parser.add_value_argument(socket_path, 's', "socket"_sv, "the path for the local IPC socket"_sv);
parser.parse(argc, argv);
m_client = TRY(os::LocalClient::connect(socket_path, true));
return {};
}
Result<int> App::run()
{
check(m_main_window);
while (!m_should_close) { TRY(os::IPC::check_for_messages(*m_client)); }
return 0;
}
App& App::the()
{
check(s_app);
return *s_app;
}
Rect App::screen_rect()
{
ui::GetScreenRectRequest request {};
auto response = os::IPC::send_sync<ui::GetScreenRectResponse>(*m_client, request).release_value();
return response.rect;
}
Result<void> App::register_window(OwnedPtr<Window>&& window, Badge<Window>)
{
int id = window->id();
check(TRY(m_windows.try_set(id, move(window))));
return {};
}
void App::unregister_window(Window* window, Badge<Window>)
{
int id = window->id();
check(m_windows.try_remove(id));
}
Window* App::find_window(int id)
{
auto* window = m_windows.try_get_ref(id);
check(window);
return window->ptr();
}
#define READ_MESSAGE(request) \
do { \
auto rc = m_client->recv_typed(request); \
if (rc.has_error()) \
{ \
if (rc.error() == EAGAIN) { continue; } \
if (rc.error() == EINTR) { continue; } \
else \
return rc.release_error(); \
} \
break; \
} while (true)
Result<void> App::handle_ipc_event(u8 id)
{
switch (id)
{
case WINDOW_CLOSE_REQUEST_ID: {
WindowCloseRequest request;
READ_MESSAGE(request);
os::eprintln("ui: Window close request from server! Shall comply.");
auto* window = find_window(request.window);
window->close();
return {};
}
case MOUSE_EVENT_REQUEST_ID: {
MouseEventRequest request;
READ_MESSAGE(request);
auto* window = find_window(request.window);
window->handle_mouse_move(request.position);
window->handle_mouse_buttons(request.position, request.buttons);
window->draw();
return {};
}
default: fail("Unexpected IPC request from server!");
}
}
}

68
libui/src/Button.cpp Normal file

@ -0,0 +1,68 @@
/**
* @file Button.cpp
* @author apio (cloudapio.eu)
* @brief A clickable component that triggers an action when pressed.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <ui/Button.h>
#include <ui/Mouse.h>
namespace ui
{
Button::Button(Rect rect)
{
m_rect = rect;
}
void Button::set_widget(Widget& widget)
{
widget.rect() = m_rect;
m_child = &widget;
widget.set_parent(this);
}
void Button::set_action(void (*action)(void))
{
m_action = action;
}
Result<EventResult> Button::handle_mouse_move(Point position)
{
m_hovered = true;
return m_child->handle_mouse_move(position);
}
Result<EventResult> Button::handle_mouse_leave(Point position)
{
m_hovered = m_clicked = false;
return m_child->handle_mouse_leave(position);
}
Result<EventResult> Button::handle_mouse_down(Point position, int buttons)
{
auto result = TRY(m_child->handle_mouse_down(position, buttons));
if (result == EventResult::DidNotHandle)
{
if (!m_clicked && (buttons == ui::MouseButtons::LEFT))
{
m_clicked = true;
m_action();
}
}
return EventResult::DidHandle;
}
Result<EventResult> Button::handle_mouse_up(Point position, int buttons)
{
if (buttons & ui::MouseButtons::LEFT) m_clicked = false;
return m_child->handle_mouse_up(position, buttons);
}
Result<void> Button::draw(Canvas& canvas)
{
return m_child->draw(canvas);
}
}

62
libui/src/Canvas.cpp Normal file

@ -0,0 +1,62 @@
/**
* @file Canvas.cpp
* @author apio (cloudapio.eu)
* @brief Drawable surfaces.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <ui/Canvas.h>
namespace ui
{
Canvas Canvas::create(u8* ptr, int width, int height)
{
return Canvas { .width = width, .height = height, .stride = width, .ptr = ptr };
}
Canvas Canvas::subcanvas(Rect rect)
{
if (rect.pos.x < 0) rect.pos.x = 0;
if (rect.pos.y < 0) rect.pos.y = 0;
if (rect.pos.x + rect.width > width) rect.width = width - rect.pos.x;
if (rect.pos.y + rect.height > height) rect.height = height - rect.pos.y;
u8* p = ptr + rect.pos.x * sizeof(Color) + (rect.pos.y * sizeof(Color) * stride);
return Canvas { .width = rect.width, .height = rect.height, .stride = stride, .ptr = p };
}
void Canvas::fill(Color color)
{
u8* p = ptr;
for (int i = 0; i < height; i++)
{
u32* colorp = (u32*)p;
for (int j = 0; j < width; j++)
{
*colorp = color.raw;
colorp++;
}
p += stride * sizeof(Color);
}
}
void Canvas::fill(u32* pixels, int _stride)
{
u8* p = ptr;
for (int i = 0; i < height; i++)
{
u32* colorp = (u32*)p;
for (int j = 0; j < width; j++)
{
u32 pix = pixels[j];
if (Color::from_u32(pix).alpha() == 0xff) *colorp = pix;
colorp++;
}
pixels += _stride;
p += stride * sizeof(Color);
}
}
}

55
libui/src/Container.cpp Normal file

@ -0,0 +1,55 @@
/**
* @file Container.cpp
* @author apio (cloudapio.eu)
* @brief A container widget to pad and align objects inside it.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <ui/Container.h>
namespace ui
{
Container::Container(Rect rect, VerticalAlignment valign, HorizontalAlignment halign)
: m_valign(valign), m_halign(halign)
{
m_rect = rect;
}
void Container::set_widget(Widget& widget)
{
m_widget = &widget;
widget.rect() = ui::align(m_rect, widget.rect(), m_valign, m_halign);
widget.set_parent(this);
}
Result<EventResult> Container::handle_mouse_move(Point position)
{
return m_widget->handle_mouse_move(position);
}
Result<EventResult> Container::handle_mouse_leave(Point position)
{
return m_widget->handle_mouse_leave(position);
}
Result<EventResult> Container::handle_mouse_down(Point position, int buttons)
{
return m_widget->handle_mouse_down(position, buttons);
}
Result<EventResult> Container::handle_mouse_up(Point position, int buttons)
{
return m_widget->handle_mouse_up(position, buttons);
}
Result<void> Container::draw(Canvas& canvas)
{
auto rect = ui::Rect { m_widget->rect().pos.x - m_rect.pos.x, m_widget->rect().pos.y - m_rect.pos.y,
m_widget->rect().width, m_widget->rect().height };
auto subcanvas = canvas.subcanvas(rect);
return m_widget->draw(subcanvas);
}
}

121
libui/src/Font.cpp Normal file

@ -0,0 +1,121 @@
/**
* @file Font.cpp
* @author apio (cloudapio.eu)
* @brief PSF font loading and rendering.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <luna/String.h>
#include <os/File.h>
#include <ui/Font.h>
constexpr static int BYTES_PER_PIXEL = (int)sizeof(ui::Color);
namespace ui
{
Result<SharedPtr<Font>> Font::load(const os::Path& path)
{
auto font = TRY(make_shared<Font>());
auto file = TRY(os::File::open(path, os::File::ReadOnly));
TRY(file->read_typed(font->m_psf_header));
if (font->m_psf_header.magic != PSF_FONT_MAGIC)
{
os::eprintln("ui::Font::load(%s) failed: font magic does not match PSF2 magic", path.name().chars());
return err(ENOTSUP);
}
if (font->m_psf_header.version != 0)
{
os::eprintln("ui::Font::load(%s) failed: font version is unsupported", path.name().chars());
return err(ENOTSUP);
}
if (font->m_psf_header.flags)
{
os::eprintln("ui::Font::load(%s) warning: font has a unicode table, which we're ignoring",
path.name().chars());
// todo(); // Font has a unicode table, oh no!
}
font->m_font_data = TRY(file->read_all()); // Read the rest of the file into the font data buffer.
return font;
}
Result<SharedPtr<Font>> Font::load_builtin(StringView name, FontWeight weight)
{
auto path = TRY(String::format("/usr/share/fonts/%s-%s.psf"_sv, name.chars(),
weight == FontWeight::Bold ? "Bold" : "Regular"));
return load(path.view());
}
SharedPtr<Font> Font::default_font()
{
static SharedPtr<ui::Font> s_default_font = {};
if (!s_default_font) s_default_font = load("/usr/share/fonts/Tamsyn-Regular.psf").release_value();
return s_default_font;
}
SharedPtr<Font> Font::default_bold_font()
{
static SharedPtr<ui::Font> s_default_bold_font = {};
if (!s_default_bold_font) s_default_bold_font = load("/usr/share/fonts/Tamsyn-Bold.psf").release_value();
return s_default_bold_font;
}
void Font::render(wchar_t codepoint, ui::Color color, ui::Canvas& canvas)
{
const wchar_t str[] = { codepoint, 0 };
render(str, color, canvas);
}
void Font::render(const wchar_t* text, ui::Color color, ui::Canvas& canvas)
{
usize len = wcslen(text);
int height = m_psf_header.height;
int width = m_psf_header.width;
int last_char_width = width;
if (canvas.width < (m_psf_header.width * static_cast<int>(len)))
{
len = (canvas.width / width) + 1;
last_char_width = canvas.width % width;
}
if (canvas.height < height) height = canvas.height;
const int bytes_per_line = (m_psf_header.width + 7) / 8;
for (usize i = 0; i < len; i++)
{
if (i + 1 == len) width = last_char_width;
wchar_t codepoint = text[i];
u8* glyph =
m_font_data.data() + (codepoint > 0 && codepoint < (wchar_t)m_psf_header.numglyph ? codepoint : 0) *
m_psf_header.bytesperglyph;
u32 offset = (u32)i * m_psf_header.width * BYTES_PER_PIXEL;
for (int y = 0; y < height; y++)
{
u32 line = offset;
int mask = 1 << (m_psf_header.width - 1);
for (int x = 0; x < width; x++)
{
if (*((u32*)glyph) & mask) *(u32*)(canvas.ptr + line) = color.raw;
mask >>= 1;
line += BYTES_PER_PIXEL;
}
glyph += bytes_per_line;
offset += canvas.stride * BYTES_PER_PIXEL;
}
}
}
}

68
libui/src/Image.cpp Normal file

@ -0,0 +1,68 @@
/**
* @file Image.cpp
* @author apio (cloudapio.eu)
* @brief TGA image loading and rendering.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <os/File.h>
#include <ui/Alignment.h>
#include <ui/Image.h>
namespace ui
{
Result<SharedPtr<Image>> Image::load(const os::Path& path)
{
auto image = TRY(make_shared<Image>());
auto file = TRY(os::File::open(path, os::File::ReadOnly));
TRY(file->read_typed(image->m_tga_header));
if (image->m_tga_header.encoding != 2) todo();
if (image->m_tga_header.bpp != 32) todo();
Buffer image_id;
TRY(file->read(image_id, image->m_tga_header.idlen));
TRY(file->read(image->m_image_data,
image->m_tga_header.w * image->m_tga_header.h * (image->m_tga_header.bpp / 8)));
return image;
}
Result<OwnedPtr<ImageWidget>> ImageWidget::load(const os::Path& path)
{
auto widget = TRY(make_owned<ImageWidget>());
widget->m_image = TRY(Image::load(path));
widget->m_rect = { 0, 0, widget->m_image->width(), widget->m_image->height() };
return widget;
}
Result<EventResult> ImageWidget::handle_mouse_move(Point)
{
return EventResult::DidNotHandle;
}
Result<EventResult> ImageWidget::handle_mouse_leave(Point)
{
return EventResult::DidNotHandle;
}
Result<EventResult> ImageWidget::handle_mouse_up(Point, int)
{
return EventResult::DidNotHandle;
}
Result<EventResult> ImageWidget::handle_mouse_down(Point, int)
{
return EventResult::DidNotHandle;
}
Result<void> ImageWidget::draw(Canvas& canvas)
{
canvas.subcanvas({ 0, 0, m_image->width(), m_image->height() }).fill(m_image->pixels(), m_image->width());
return {};
}
}

192
libui/src/Layout.cpp Normal file

@ -0,0 +1,192 @@
/**
* @file Layout.cpp
* @author apio (cloudapio.eu)
* @brief Layout widgets to organize content.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <stdlib.h>
#include <ui/Layout.h>
namespace ui
{
HorizontalLayout::HorizontalLayout(AdjustHeight adjust_height, AdjustWidth adjust_width)
: m_adjust_height(adjust_height), m_adjust_width(adjust_width)
{
}
Result<EventResult> HorizontalLayout::handle_mouse_move(Point position)
{
EventResult result = ui::EventResult::DidNotHandle;
for (auto widget : m_widgets)
{
if (widget->rect().contains(position)) result = TRY(widget->handle_mouse_move(position));
else
TRY(widget->handle_mouse_leave(position));
}
return result;
}
Result<EventResult> HorizontalLayout::handle_mouse_leave(Point position)
{
for (auto widget : m_widgets) TRY(widget->handle_mouse_leave(position));
return ui::EventResult::DidNotHandle;
}
Result<EventResult> HorizontalLayout::handle_mouse_up(Point position, int buttons)
{
for (auto widget : m_widgets)
{
if (widget->rect().contains(position)) return widget->handle_mouse_up(position, buttons);
}
return ui::EventResult::DidNotHandle;
}
Result<EventResult> HorizontalLayout::handle_mouse_down(Point position, int buttons)
{
for (auto widget : m_widgets)
{
if (widget->rect().contains(position)) return widget->handle_mouse_down(position, buttons);
}
return ui::EventResult::DidNotHandle;
}
Result<void> HorizontalLayout::draw(Canvas& canvas)
{
for (auto widget : m_widgets)
{
ui::Rect rect = { m_rect.relative(widget->rect().pos), widget->rect().width, widget->rect().height };
auto subcanvas = canvas.subcanvas(rect);
TRY(widget->draw(subcanvas));
}
return {};
}
Result<void> HorizontalLayout::add_widget(Widget& widget)
{
TRY(m_widgets.try_append(&widget));
if (m_adjust_width == AdjustWidth::No)
{
widget.rect().pos.x = m_rect.pos.x + m_used_width;
m_used_width += widget.rect().width;
}
else
{
int used_width = 0;
div_t result = div(m_rect.width, (int)m_widgets.size());
for (auto w : m_widgets)
{
w->rect().pos.x = m_rect.pos.x + used_width;
w->rect().width = result.quot;
used_width += result.quot;
}
m_widgets[m_widgets.size() - 1]->rect().width += result.rem;
}
widget.rect().pos.y = m_rect.pos.y;
if (m_adjust_height == AdjustHeight::Yes) { widget.rect().height = m_rect.height; }
widget.set_parent(this);
return {};
}
VerticalLayout::VerticalLayout(AdjustHeight adjust_height, AdjustWidth adjust_width)
: m_adjust_height(adjust_height), m_adjust_width(adjust_width)
{
}
Result<EventResult> VerticalLayout::handle_mouse_move(Point position)
{
EventResult result = ui::EventResult::DidNotHandle;
for (auto widget : m_widgets)
{
if (widget->rect().contains(position)) result = TRY(widget->handle_mouse_move(position));
else
TRY(widget->handle_mouse_leave(position));
}
return result;
}
Result<EventResult> VerticalLayout::handle_mouse_leave(Point position)
{
for (auto widget : m_widgets) TRY(widget->handle_mouse_leave(position));
return ui::EventResult::DidNotHandle;
}
Result<EventResult> VerticalLayout::handle_mouse_up(Point position, int buttons)
{
for (auto widget : m_widgets)
{
if (widget->rect().contains(position)) return widget->handle_mouse_up(position, buttons);
}
return ui::EventResult::DidNotHandle;
}
Result<EventResult> VerticalLayout::handle_mouse_down(Point position, int buttons)
{
for (auto widget : m_widgets)
{
if (widget->rect().contains(position)) return widget->handle_mouse_down(position, buttons);
}
return ui::EventResult::DidNotHandle;
}
Result<void> VerticalLayout::draw(Canvas& canvas)
{
for (auto widget : m_widgets)
{
ui::Rect rect = { m_rect.relative(widget->rect().pos), widget->rect().width, widget->rect().height };
auto subcanvas = canvas.subcanvas(rect);
TRY(widget->draw(subcanvas));
}
return {};
}
Result<void> VerticalLayout::add_widget(Widget& widget)
{
TRY(m_widgets.try_append(&widget));
if (m_adjust_height == AdjustHeight::No)
{
widget.rect().pos.y = m_rect.pos.y + m_used_height;
m_used_height += widget.rect().height;
}
else
{
int used_height = 0;
div_t result = div(m_rect.height, (int)m_widgets.size());
for (auto w : m_widgets)
{
w->rect().pos.y = m_rect.pos.y + used_height;
w->rect().height = result.quot;
used_height += result.quot;
}
m_widgets[m_widgets.size() - 1]->rect().height += result.rem;
}
widget.rect().pos.x = m_rect.pos.x;
if (m_adjust_width == AdjustWidth::Yes) { widget.rect().width = m_rect.width; }
widget.set_parent(this);
return {};
}
}

62
libui/src/Rect.cpp Normal file

@ -0,0 +1,62 @@
/**
* @file Rect.cpp
* @author apio (cloudapio.eu)
* @brief A simple 2D rectangle representation.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <ui/Rect.h>
namespace ui
{
bool Rect::contains(Point point)
{
return (point.x >= pos.x) && (point.y >= pos.y) && (point.x <= (pos.x + width)) &&
(point.y <= (pos.y + height));
}
bool Rect::contains(Rect rect)
{
if (!contains(rect.pos)) return false;
Point rel = relative(rect.pos);
if ((rel.x + rect.width) > width) return false;
if ((rel.y + rect.height) > height) return false;
return true;
}
Point Rect::normalize(Point point)
{
if (point.x < pos.x) point.x = pos.x;
if (point.y < pos.y) point.y = pos.y;
if (point.x > pos.x + width) point.x = pos.x + width;
if (point.y > pos.y + height) point.y = pos.y + height;
return point;
}
Point Rect::relative(Point point)
{
point = normalize(point);
point.x -= pos.x;
point.y -= pos.y;
return point;
}
Point Rect::absolute(Point point)
{
point.x += pos.x;
point.y += pos.y;
return point;
}
Rect Rect::absolute(Rect rect)
{
return Rect { absolute(rect.pos), rect.width, rect.height };
}
Rect Rect::normalized()
{
return Rect { ui::Point { pos.x < 0 ? 0 : pos.x, pos.y < 0 ? 0 : pos.y }, width, height };
}
};

124
libui/src/Window.cpp Normal file

@ -0,0 +1,124 @@
/**
* @file Window.cpp
* @author apio (cloudapio.eu)
* @brief UI windows.
*
* @copyright Copyright (c) 2023, the Luna authors.
*
*/
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <ui/App.h>
#include <ui/Window.h>
#include <ui/ipc/Server.h>
#include <unistd.h>
static Result<u32*> create_shm_region(const char* path, int* outfd, ui::Rect rect)
{
int fd = shm_open(path, O_RDWR, 0600);
shm_unlink(path);
if (fd < 0) return err(errno);
usize size = rect.width * rect.height * 4; // 4 bytes per pixel
void* p = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
{
shm_unlink(path);
close(fd);
return 0;
}
if (outfd) *outfd = fd;
else
close(fd);
return (u32*)p;
}
namespace ui
{
Result<Window*> Window::create(Rect rect, bool decorated)
{
auto window = TRY(make_owned<Window>());
ui::CreateWindowRequest request;
request.rect = rect;
request.decorated = decorated;
auto response = TRY(os::IPC::send_sync<ui::CreateWindowResponse>(App::the().client(), request));
u32* pixels = TRY(create_shm_region(response.shm_path, nullptr, rect));
window->m_canvas = ui::Canvas { rect.width, rect.height, rect.width, (u8*)pixels };
window->m_id = response.window;
Window* p = window.ptr();
App::the().register_window(move(window), {});
return p;
}
Window::~Window()
{
if (m_canvas.ptr) munmap(m_canvas.ptr, ((usize)m_canvas.width) * ((usize)m_canvas.height) * 4);
}
void Window::set_title(StringView title)
{
ui::SetWindowTitleRequest request;
request.window = m_id;
SET_IPC_STRING(request.title, title.chars());
os::IPC::send_async(App::the().client(), request);
}
void Window::update()
{
ui::InvalidateRequest request;
request.window = m_id;
os::IPC::send_async(App::the().client(), request);
}
void Window::close()
{
App& app = App::the();
ui::CloseWindowRequest request;
request.window = m_id;
os::IPC::send_async(app.client(), request);
if (this == app.main_window()) app.set_should_close(true);
app.unregister_window(this, {});
}
Result<void> Window::draw()
{
m_canvas.fill(m_background);
if (m_main_widget) return m_main_widget->draw(m_canvas);
update();
return {};
}
Result<void> Window::handle_mouse_move(ui::Point position)
{
if (!m_main_widget) return {};
TRY(m_main_widget->handle_mouse_move(position));
return {};
}
Result<void> Window::handle_mouse_buttons(ui::Point position, int buttons)
{
if (!m_main_widget) return {};
if (buttons) TRY(m_main_widget->handle_mouse_down(position, buttons));
if (m_old_mouse_buttons.has_value())
{
int old_buttons = m_old_mouse_buttons.value();
int diff = old_buttons & ~buttons;
if (diff) TRY(m_main_widget->handle_mouse_up(position, diff));
}
m_old_mouse_buttons = buttons;
return {};
}
}

@ -8,10 +8,12 @@ cd $LUNA_ROOT
mkdir -p $LUNA_BASE
mkdir -p $LUNA_BASE/usr/include
mkdir -p $LUNA_BASE/usr/include/luna
mkdir -p $LUNA_BASE/usr/include/ui
mkdir -p $LUNA_BASE/usr/include/os
mkdir -p $LUNA_BASE/usr/include/moon
cp --preserve=timestamps -RT libc/include/ $LUNA_BASE/usr/include
cp --preserve=timestamps -RT libluna/include/luna/ $LUNA_BASE/usr/include/luna
cp --preserve=timestamps -RT libui/include/ui/ $LUNA_BASE/usr/include/ui
cp --preserve=timestamps -RT libos/include/os/ $LUNA_BASE/usr/include/os
cp --preserve=timestamps -RT kernel/src/api/ $LUNA_BASE/usr/include/moon

@ -58,7 +58,7 @@ case $format in
tar xf $output
;;
git)
if ! [ -d $srcdir]; then
if ! [ -d $srcdir ]; then
echo "Cloning repository for $name..."
git clone $url $srcdir
else
@ -78,9 +78,11 @@ esac
if ! [ "$set_cc_variables" = "no" ]; then
export CC=x86_64-luna-gcc
export CXX=x86_64-luna-g++
export AR=x86_64-luna-ar
export PKG_CONFIG=luna-pkg-config
export CC_FOR_BUILD=gcc
export CXX_FOR_BUILD=g++
export AR_FOR_BUILD=ar
export PKG_CONFIG_FOR_BUILD=pkg-config
fi

@ -4,7 +4,7 @@ source $(dirname $0)/env.sh
cd $LUNA_ROOT
FOLDERS=(kernel libc libos libluna apps shell tests)
FOLDERS=(kernel libc libos libui libluna apps shell tests)
SOURCES=($(find ${FOLDERS[@]} -type f -name "*.cpp"))
SOURCES+=($(find ${FOLDERS[@]} -type f -name "*.h"))

19
wind/CMakeLists.txt Normal file

@ -0,0 +1,19 @@
set(SOURCES
main.cpp
Screen.h
Screen.cpp
Mouse.h
Mouse.cpp
Window.h
Window.cpp
IPC.cpp
IPC.h
Client.h
)
add_executable(wind ${SOURCES})
target_compile_options(wind PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings -fno-threadsafe-statics)
add_dependencies(wind libc)
target_include_directories(wind PRIVATE ${LUNA_BASE}/usr/include ${CMAKE_CURRENT_LIST_DIR})
target_link_libraries(wind PRIVATE os ui)
install(TARGETS wind DESTINATION ${LUNA_BASE}/usr/bin)

18
wind/Client.h Normal file

@ -0,0 +1,18 @@
#pragma once
#include "Window.h"
#include <os/LocalServer.h>
struct Client
{
os::LocalServer::Client conn;
Vector<Window*> windows;
bool rpc_in_progress { false };
u8 rpc_id { 0 };
Client(os::LocalServer::Client&& client)
#ifdef CLIENT_IMPLEMENTATION
: conn(move(client)), windows() {}
#else
;
#endif
};

221
wind/IPC.cpp Normal file

@ -0,0 +1,221 @@
#include "IPC.h"
#include "Screen.h"
#include <errno.h>
#include <luna/Alignment.h>
#include <luna/String.h>
#include <os/File.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#define TRY_OR_IPC_ERROR(expr) \
({ \
auto _expr_rc = (expr); \
if (!_expr_rc.has_value()) \
{ \
delete window; \
os::IPC::send_error(client.conn, _expr_rc.error()); \
return {}; \
} \
_expr_rc.release_value(); \
})
static Result<u32*> create_shm_region(const char* path, int* outfd, ui::Rect rect)
{
int fd = shm_open(path, O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd < 0)
{
int olderr = errno;
os::eprintln("wind: could not create shared memory region: shm_open failed (%s) - %s", path, strerror(olderr));
return err(olderr);
}
usize size = align_up<PAGE_SIZE>(rect.width * rect.height * 4); // 4 bytes per pixel
if (ftruncate(fd, size) < 0)
{
int olderr = errno;
os::eprintln("wind: could not create shared memory region: ftruncate failed (%d, %zu) - %s", fd, size,
strerror(olderr));
shm_unlink(path);
close(fd);
return err(olderr);
}
void* p = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
{
int olderr = errno;
os::eprintln("wind: could not create shared memory region: mmap failed (%zu, %d) - %s", size, fd,
strerror(olderr));
shm_unlink(path);
close(fd);
return err(olderr);
}
if (outfd) *outfd = fd;
else
close(fd);
return (u32*)p;
}
#define READ_MESSAGE(request) \
do { \
auto rc = client.conn.recv_typed(request); \
if (rc.has_error()) \
{ \
if (rc.error() == EAGAIN) \
{ \
client.rpc_in_progress = true; \
client.rpc_id = decltype(request)::ID; \
return {}; \
} \
if (rc.error() == EINTR) \
{ \
client.rpc_in_progress = true; \
client.rpc_id = decltype(request)::ID; \
return {}; \
} \
else \
return rc.release_error(); \
} \
} while (0)
#define CHECK_WINDOW_ID(request) \
do { \
if ((usize)request.window >= client.windows.size() || !client.windows[request.window]) \
{ \
os::eprintln("wind: Window id is invalid!"); \
return {}; \
} \
} while (0)
static Result<void> handle_create_window_message(Client& client)
{
ui::CreateWindowRequest request;
READ_MESSAGE(request);
request.rect = request.rect.normalized();
if (request.decorated)
{
request.rect.height +=
Window::titlebar_height(); // Make sure we provide the full contents rect that was asked for.
request.rect.pos.y -= Window::titlebar_height(); // Adjust it so the contents begin at the expected coordinates.
}
auto name = TRY(String::from_cstring("Window"));
auto* window = new (std::nothrow) Window(request.rect, move(name), request.decorated);
if (!window)
{
os::IPC::send_error(client.conn, ENOMEM);
return {};
}
auto shm_path = TRY_OR_IPC_ERROR(String::format("/wind-shm-%d-%lu"_sv, client.conn.fd(), time(NULL)));
window->pixels = TRY_OR_IPC_ERROR(create_shm_region(shm_path.chars(), nullptr, window->contents));
TRY_OR_IPC_ERROR(client.windows.try_append(window));
int id = static_cast<int>(client.windows.size() - 1);
window->client = &client;
window->id = id;
ui::CreateWindowResponse response;
response.window = id;
SET_IPC_STRING(response.shm_path, shm_path.chars());
os::IPC::send_async(client.conn, response);
return {};
}
static Result<void> handle_set_window_title_message(Client& client)
{
ui::SetWindowTitleRequest request;
READ_MESSAGE(request);
auto name = COPY_IPC_STRING(request.title);
os::println("wind: SetWindowTitle(\"%s\") for window %d", name.chars(), request.window);
CHECK_WINDOW_ID(request);
client.windows[request.window]->name = move(name);
return {};
}
static Result<void> handle_invalidate_message(Client& client)
{
ui::InvalidateRequest request;
READ_MESSAGE(request);
CHECK_WINDOW_ID(request);
client.windows[request.window]->dirty = true;
return {};
}
static Result<void> handle_close_window_message(Client& client)
{
ui::CloseWindowRequest request;
READ_MESSAGE(request);
CHECK_WINDOW_ID(request);
auto* window = client.windows[request.window];
client.windows[request.window] = nullptr;
g_windows.remove(window);
delete window;
return {};
}
static Result<void> handle_get_screen_rect_message(Client& client)
{
ui::GetScreenRectRequest request;
READ_MESSAGE(request); // Kinda pointless, but required.
ui::GetScreenRectResponse response;
response.rect = Screen::the().canvas().rect();
os::IPC::send_async(client.conn, response);
return {};
}
namespace wind
{
Result<void> handle_ipc_message(Client& client, u8 id)
{
client.rpc_in_progress = false;
switch (id)
{
case ui::CREATE_WINDOW_ID: return handle_create_window_message(client);
case ui::SET_WINDOW_TITLE_ID: return handle_set_window_title_message(client);
case ui::INVALIDATE_ID: return handle_invalidate_message(client);
case ui::CLOSE_WINDOW_ID: return handle_close_window_message(client);
case ui::GET_SCREEN_RECT_ID: return handle_get_screen_rect_message(client);
default: os::eprintln("wind: Invalid IPC message from client!"); return err(EINVAL);
}
}
Result<void> handle_ipc(Client& client)
{
if (client.rpc_in_progress) return handle_ipc_message(client, client.rpc_id);
u8 id;
auto rc = client.conn.recv_typed(id);
if (rc.has_error())
{
if (rc.error() == EAGAIN) { return {}; }
if (rc.error() == EINTR) { return {}; }
else
return rc.release_error();
}
return handle_ipc_message(client, id);
}
}

10
wind/IPC.h Normal file

@ -0,0 +1,10 @@
#pragma once
#include "Client.h"
#include <ui/ipc/Server.h>
namespace wind
{
Result<void> handle_ipc_message(Client& client, u8 id);
Result<void> handle_ipc(Client& client);
}

98
wind/Mouse.cpp Normal file

@ -0,0 +1,98 @@
#include "Mouse.h"
#include "Client.h"
#include <os/File.h>
#include <os/IPC.h>
#include <ui/Image.h>
#include <ui/ipc/Client.h>
static SharedPtr<ui::Image> g_mouse_cursor;
Mouse::Mouse(ui::Canvas& screen)
{
m_position.x = screen.width / 2;
m_position.y = screen.height / 2;
m_screen_rect = screen.rect();
g_mouse_cursor = ui::Image::load("/usr/share/cursors/default.tga").value_or({});
}
void Mouse::draw(ui::Canvas& screen)
{
if (!g_mouse_cursor) return;
auto canvas = screen.subcanvas(ui::Rect { m_position, g_mouse_cursor->width(), g_mouse_cursor->height() });
canvas.fill(g_mouse_cursor->pixels(), g_mouse_cursor->width());
}
void Mouse::update(const moon::MousePacket& packet)
{
m_position.x += packet.xdelta;
m_position.y -= packet.ydelta;
m_position = m_screen_rect.normalize(m_position);
if (m_dragging_window && !(packet.buttons & moon::MouseButton::Left))
{
os::println("Stopped drag: window at (%d,%d,%d,%d) with offset (%d,%d)", m_dragging_window->surface.pos.x,
m_dragging_window->surface.pos.y, m_dragging_window->surface.width,
m_dragging_window->surface.height, this->m_initial_drag_position.x,
this->m_initial_drag_position.y);
m_dragging_window = nullptr;
}
if (m_dragging_window)
{
m_dragging_window->surface.pos =
ui::Point { m_position.x - m_initial_drag_position.x, m_position.y - m_initial_drag_position.y };
m_dragging_window->surface = m_dragging_window->surface.normalized();
}
else if ((packet.buttons & moon::MouseButton::Left) && !m_dragging_window)
{
// Iterate from the end of the list, since windows at the beginning are stacked at the bottom and windows at the
// top are at the end.
for (Window* window = g_windows.last().value_or(nullptr); window;
window = g_windows.previous(window).value_or(nullptr))
{
if (window->surface.absolute(window->close_button).contains(m_position))
{
ui::WindowCloseRequest request;
request.window = window->id;
auto& client = *window->client;
os::IPC::send_async(client.conn, request);
break;
}
else if (window->surface.absolute(window->titlebar).contains(m_position))
{
m_dragging_window = window;
m_initial_drag_position = window->surface.relative(m_position);
os::println("Started drag: window at (%d,%d,%d,%d) with offset (%d,%d)", window->surface.pos.x,
window->surface.pos.y, window->surface.width, window->surface.height,
m_initial_drag_position.x, m_initial_drag_position.y);
window->focus();
break;
}
else if (window->surface.absolute(window->contents).contains(m_position))
{
window->focus();
break; // We don't want to continue iterating, otherwise this would take into account windows whose
// titlebar is underneath another window's contents!
}
}
}
for (Window* window = g_windows.last().value_or(nullptr); window;
window = g_windows.previous(window).value_or(nullptr))
{
auto titlebar = window->surface.absolute(window->titlebar);
auto contents = window->surface.absolute(window->contents);
if (titlebar.contains(m_position)) break;
if (contents.contains(m_position))
{
ui::MouseEventRequest request;
request.window = window->id;
request.position = contents.relative(m_position);
request.buttons = packet.buttons;
os::IPC::send_async(window->client->conn, request);
break;
}
}
}

22
wind/Mouse.h Normal file

@ -0,0 +1,22 @@
#pragma once
#include "Screen.h"
#include "Window.h"
#include <moon/Mouse.h>
#include <ui/Canvas.h>
class Mouse
{
public:
Mouse(ui::Canvas& screen);
void update(const moon::MousePacket& packet);
void draw(ui::Canvas& screen);
private:
ui::Point m_position;
ui::Rect m_screen_rect;
Window* m_dragging_window = nullptr;
ui::Point m_initial_drag_position;
};

36
wind/Screen.cpp Normal file

@ -0,0 +1,36 @@
#include "Screen.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
Screen Screen::s_the;
Result<void> Screen::open()
{
int fd = ::open("/dev/fb0", O_RDWR);
if (fd < 0) return err(errno);
int width = ioctl(fd, FB_GET_WIDTH);
int height = ioctl(fd, FB_GET_HEIGHT);
void* p = mmap(nullptr, width * height * BYTES_PER_PIXEL, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
if (p == MAP_FAILED) { return err(errno); }
Screen screen;
screen.m_canvas = ui::Canvas::create((u8*)p, width, height);
screen.m_size = width * height * BYTES_PER_PIXEL;
s_the = screen;
return {};
}
void Screen::sync()
{
msync(m_canvas.ptr, size(), MS_SYNC);
}

34
wind/Screen.h Normal file

@ -0,0 +1,34 @@
#pragma once
#include <luna/OwnedPtr.h>
#include <ui/Canvas.h>
constexpr int BYTES_PER_PIXEL = 4;
class Screen
{
public:
static Result<void> open();
ui::Canvas& canvas()
{
return m_canvas;
}
int size() const
{
return m_size;
}
static Screen& the()
{
return s_the;
}
void sync();
private:
ui::Canvas m_canvas;
int m_size;
static Screen s_the;
};

67
wind/Window.cpp Normal file

@ -0,0 +1,67 @@
#include "Window.h"
#include <luna/Utf8.h>
#include <os/File.h>
#include <sys/mman.h>
#include <ui/Font.h>
#include <ui/Image.h>
LinkedList<Window> g_windows;
void Window::draw(ui::Canvas& screen)
{
dirty = false;
auto window = screen.subcanvas(surface);
window.subcanvas(contents).fill(pixels, contents.width);
wchar_t buffer[4096];
Utf8StringDecoder decoder(name.chars());
decoder.decode(buffer, sizeof(buffer)).release_value();
auto font = ui::Font::default_font();
auto titlebar_canvas = window.subcanvas(titlebar);
titlebar_canvas.fill(ui::GRAY);
auto textarea = titlebar_canvas.subcanvas(ui::Rect { 10, 10, titlebar_canvas.width - 10, titlebar_canvas.height });
font->render(buffer, ui::BLACK, textarea);
static SharedPtr<ui::Image> g_close_icon;
if (!g_close_icon) g_close_icon = ui::Image::load("/usr/share/icons/16x16/app-close.tga").release_value();
auto close_area = window.subcanvas(close_button);
close_area.fill(g_close_icon->pixels(), g_close_icon->width());
}
void Window::focus()
{
// Bring the window to the front of the list.
g_windows.remove(this);
g_windows.append(this);
}
Window::Window(ui::Rect r, String&& n, bool d) : surface(r), name(move(n)), decorated(d)
{
auto font = ui::Font::default_font();
if (decorated && surface.width < 36) surface.width = 36;
if (decorated && surface.height < (font->height() + 20)) surface.height = font->height() + 20;
titlebar = decorated ? ui::Rect { 0, 0, surface.width, font->height() + 20 } : ui::Rect { 0, 0, 0, 0 };
close_button = decorated ? ui::Rect { surface.width - 26, 10, 16, 16 } : ui::Rect { 0, 0, 0, 0 };
contents = decorated ? ui::Rect { 0, font->height() + 20, surface.width, surface.height - (font->height() + 20) }
: ui::Rect { 0, 0, surface.width, surface.height };
g_windows.append(this);
}
int Window::titlebar_height()
{
auto font = ui::Font::default_font();
return font->height() + 20;
}
Window::~Window()
{
usize size = contents.width * contents.height * 4;
munmap(pixels, size);
}

33
wind/Window.h Normal file

@ -0,0 +1,33 @@
#pragma once
#include <luna/LinkedList.h>
#include <luna/String.h>
#include <ui/Canvas.h>
#include <ui/Color.h>
#include <ui/Rect.h>
struct Client;
struct Window : public LinkedListNode<Window>
{
ui::Rect surface;
ui::Rect titlebar;
ui::Rect close_button;
ui::Rect contents;
u32* pixels;
String name;
bool dirty { false };
Client* client;
int id;
bool decorated;
static int titlebar_height();
Window(ui::Rect, String&&, bool);
~Window();
void focus();
void draw(ui::Canvas& screen);
};
extern LinkedList<Window> g_windows;

183
wind/main.cpp Normal file

@ -0,0 +1,183 @@
#define CLIENT_IMPLEMENTATION
#include "Client.h"
#include "IPC.h"
#include "Mouse.h"
#include "Screen.h"
#include "Window.h"
#include <errno.h>
#include <moon/Keyboard.h>
#include <os/ArgumentParser.h>
#include <os/File.h>
#include <os/LocalServer.h>
#include <os/Process.h>
#include <os/Security.h>
#include <pwd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/poll.h>
#include <time.h>
#include <unistd.h>
static void debug(const Vector<OwnedPtr<Client>>& clients)
{
os::println("--- wind: DEBUG OUTPUT ---");
os::println("-- wind: Listing clients --");
for (const auto& client : clients)
{
os::println("Client with fd %d, owns %zu windows", client->conn.fd(), client->windows.size());
}
os::println("-- wind: Listing windows --");
for (const auto& window : g_windows)
{
os::println("Window of client (fd %d), id %d, %sdecorated, %sdirty (\"%s\") (%d,%d,%d,%d)",
window->client->conn.fd(), window->id, window->decorated ? "" : "not ", window->dirty ? "" : "not ",
window->name.chars(), window->surface.pos.x, window->surface.pos.y, window->surface.width,
window->surface.height);
}
os::println("-- wind: Listing processes --");
system("ps");
os::println("-- wind: Listing memory usage --");
system("free -h");
os::println("--- wind: END DEBUG OUTPUT ---");
}
Result<int> luna_main(int argc, char** argv)
{
srand((unsigned)time(NULL));
TRY(os::Security::pledge("stdio rpath wpath cpath unix proc exec tty signal id", NULL));
StringView socket_path = "/tmp/wind.sock";
StringView user;
os::ArgumentParser parser;
parser.add_description("The display server for Luna's graphical user interface."_sv);
parser.add_system_program_info("wind"_sv);
parser.add_value_argument(socket_path, 's', "socket"_sv, "the path for the local IPC socket"_sv);
parser.add_value_argument(user, 'u', "user"_sv, "the user to run as"_sv);
parser.parse(argc, argv);
if (geteuid() != 0)
{
os::eprintln("error: wind must be run as root to initialize resources, run with --user=<USERNAME> to drop "
"privileges afterwards");
return 1;
}
auto mouse = TRY(os::File::open("/dev/mouse", os::File::ReadOnly));
mouse->set_buffer(os::File::NotBuffered);
mouse->set_close_on_exec();
auto keyboard = TRY(os::File::open("/dev/kbd", os::File::ReadOnly));
keyboard->set_buffer(os::File::NotBuffered);
keyboard->set_close_on_exec();
TRY(Screen::open());
auto& screen = Screen::the();
Mouse mouse_pointer { screen.canvas() };
ioctl(STDIN_FILENO, TTYSETGFX, 1);
setpgid(0, 0);
int fd = open("/dev/null", O_RDONLY);
if (fd >= 0)
{
dup2(fd, STDIN_FILENO);
close(fd);
}
clearenv();
if (!user.is_empty())
{
auto* pwd = getpwnam(user.chars());
if (pwd)
{
setgid(pwd->pw_gid);
setuid(pwd->pw_uid);
}
}
auto server = TRY(os::LocalServer::create(socket_path, false));
TRY(server->listen(20));
StringView args[] = { "/usr/bin/init"_sv, "--user"_sv };
TRY(os::Process::spawn("/usr/bin/init"_sv, Slice<StringView> { args, 2 }, false));
ui::Color background = ui::BLACK;
Vector<OwnedPtr<Client>> clients;
Vector<struct pollfd> fds;
TRY(fds.try_append({ .fd = mouse->fd(), .events = POLLIN, .revents = 0 }));
TRY(fds.try_append({ .fd = keyboard->fd(), .events = POLLIN, .revents = 0 }));
TRY(fds.try_append({ .fd = server->fd(), .events = POLLIN, .revents = 0 }));
TRY(os::Security::pledge("stdio rpath wpath cpath unix signal proc exec", NULL));
while (1)
{
screen.canvas().fill(background);
for (auto* window : g_windows) window->draw(screen.canvas());
mouse_pointer.draw(screen.canvas());
screen.sync();
for (auto& pfd : fds) { pfd.revents = 0; }
int rc = poll(fds.data(), fds.size(), 1000);
if (!rc) continue;
if (rc < 0 && errno != EINTR) { os::println("poll: error: %s", strerror(errno)); }
if (fds[0].revents & POLLIN)
{
moon::MousePacket packet;
TRY(mouse->read_typed(packet));
mouse_pointer.update(packet);
}
if (fds[1].revents & POLLIN)
{
moon::KeyboardPacket packet;
TRY(keyboard->read_typed(packet));
os::println("%s key %d", packet.released ? "Released" : "Pressed", packet.key);
if (!packet.released && packet.key == moon::K_Tab) debug(clients);
}
for (usize i = 0; i < clients.size(); i++)
{
if (fds[i + 3].revents & POLLIN) wind::handle_ipc(*clients[i]);
if (fds[i + 3].revents & POLLHUP)
{
os::println("wind: Client %d disconnected", i);
fds.remove_at(i + 3);
auto client = clients.remove_at(i);
client->conn.disconnect();
for (auto& window : client->windows)
{
if (window)
{
g_windows.remove(window);
delete window;
}
}
}
}
if (fds[2].revents & POLLIN)
{
auto client = TRY(server->accept());
os::println("wind: New client connected!");
TRY(fds.try_append({ .fd = client.fd(), .events = POLLIN, .revents = 0 }));
OwnedPtr<Client> c = TRY(adopt_owned_if_nonnull(new Client(move(client))));
TRY(clients.try_append(move(c)));
}
}
}