raven 2.0

This commit is contained in:
hippoz 2023-04-13 00:27:08 +03:00
parent d5212ea574
commit 699091f9e1
Signed by: hippoz
GPG key ID: 56C4E02A85F2FBED
69 changed files with 1941 additions and 3222 deletions

9
.gitignore vendored
View file

@ -1,7 +1,2 @@
build/ compile_flags.txt
builddir/ build/
buildclang/
releaseclang/
.cache/
compile_commands.json

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2021 hippoz Copyright (c) 2023 hippoz
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

39
Makefile Normal file
View file

@ -0,0 +1,39 @@
CC?=cc
CFLAGS_LIBS:=`pkg-config --cflags --libs xcb cairo pangocairo` -lm
CFLAGS:=$(CFLAGS) -pipe -Wall -Wextra -Wshadow -std=c99 -pedantic $(CFLAGS_LIBS)
BUILD=build
OBJ=$(BUILD)
BINDIR=$(BUILD)
SRC=src
BIN=$(BINDIR)/main
SRCS=$(wildcard $(SRC)/*.c)
OBJS=$(patsubst $(SRC)/%.c, $(OBJ)/%.o, $(SRCS))
DEPS=$(OBJS:%.o=%.d)
all: CFLAGS+=-fsanitize=address -Og -ggdb
all: $(BUILD) $(BIN)
release: CFLAGS+=-O2 -flto=auto -DNDEBUG
release: clean $(BUILD) $(BIN)
$(BIN): $(OBJS)
$(CC) $(CFLAGS) $(OBJS) -o $@
-include $(DEPS)
$(OBJ)/%.o: $(SRC)/%.c
$(CC) $(CFLAGS) -MMD -c $< -o $@
$(BUILD):
mkdir -p $(BUILD)
clean:
$(RM) $(BUILD)/*
compile_flags.txt:
echo "$(CFLAGS)" | tr " " "\n" > compile_flags.txt
.PHONY: all release clean

View file

@ -1,39 +1,10 @@
# Raven # raven
Raven is a simple user interface library. raven is a highly experimental UI library focused on simplicity.
# Dependencies ## quick start
- meson *(make)*
- ninja *(make)* *(for meson)*
- cairo
- cairomm
- pango
- pangocairo
- xlib
- librsvg
# Installing
If you wish install libraven on your GNU/Linux system, run the script in `pkg/install.sh`. Before running the script, please read its contents to make sure its actions are compatible with your system setup.
If you use Arch Linux or a derivative, you can use the package inside of `pkg/makepkg`:
```console
$ make
$ ./build/main
``` ```
$ cd pkg/makepkg
$ makepkg -si
```
Please note that the Arch Linux package will build Raven from the remote git repository, not your local source tree.
# Hacking
If you wish to develop Raven or look at the test program, set up a Meson build directory and compile Raven:
```
$ meson builddir
$ cd builddir
$ meson compile
```
You should see `ravenapp` (the test program) and `libraven.so` (the Raven library) in the build directory.

View file

@ -1,96 +0,0 @@
project(
'raven',
'cpp',
default_options : ['cpp_std=c++17']
)
project_description = 'The Raven user interface library'
add_project_arguments('-O3', language : 'cpp')
cairomm_dep = dependency('cairomm-1.0')
pangocairo_dep = dependency('pangocairo')
xlib_dep = dependency('x11')
librsvg_dep = dependency('librsvg-2.0')
raven_dependencies = [
cairomm_dep,
xlib_dep,
pangocairo_dep,
librsvg_dep
]
headers = include_directories('src')
raven_source_files = [
'./src/SvgUtil.cpp',
'./src/Box.cpp',
'./src/Styles.cpp',
'./src/Painter.cpp',
'./src/Application.cpp',
'./src/Window.cpp',
'./src/Widget.cpp',
'./src/SvgWidget.cpp',
'./src/ScrollContainer.cpp',
'./src/Button.cpp',
'./src/DocumentLayout.cpp',
'./src/BoxLayout.cpp',
'./src/ListLayout.cpp',
'./src/Label.cpp',
'./src/ListView.cpp',
'./src/TextInput.cpp',
]
raven_header_files = [
'./src/SvgUtil.hpp',
'./src/Logging.hpp',
'./src/Box.hpp',
'./src/BoxLayout.hpp',
'./src/Button.hpp',
'./src/DocumentLayout.hpp',
'./src/Events.hpp',
'./src/Forward.hpp',
'./src/GenericStyle.hpp',
'./src/Label.hpp',
'./src/Layout.hpp',
'./src/Painter.hpp',
'./src/Point.hpp',
'./src/RGB.hpp',
'./src/ScrollContainer.hpp',
'./src/ListLayout.hpp',
'./src/Styles.hpp',
'./src/Widget.hpp',
'./src/SvgWidget.hpp',
'./src/Application.hpp',
'./src/Window.hpp',
'./src/TextInput.hpp',
]
raven_lib = library(
meson.project_name(),
raven_source_files,
dependencies : raven_dependencies,
install : true
)
raven_dep = declare_dependency(
include_directories : headers,
link_with : raven_lib
)
set_variable('raven_dep', raven_dep)
install_headers(raven_header_files, subdir : meson.project_name())
pkg_mod = import('pkgconfig')
pkg_mod.generate(
name : meson.project_name(),
filebase : meson.project_name(),
description : project_description,
subdirs : meson.project_name(),
libraries : [raven_dependencies, raven_lib]
)
executable(
'ravenapp',
'./src/main.cpp',
dependencies : [raven_dependencies, raven_dep]
)

View file

@ -1,13 +0,0 @@
#!/bin/sh
if ! command -v arch-meson &> /dev/null
then
echo "arch-meson not found, using meson"
meson build
else
echo "found arch-meson"
arch-meson . build
fi
meson compile -C build
meson install -C build --destdir "$DEST"

View file

@ -1,25 +0,0 @@
pkgname=libraven
pkgver=r85.5663552
pkgrel=1
pkgdesc='The Raven user interface library'
url='https://git.hippoz.xyz/hippoz/raven'
source=("git+https://git.hippoz.xyz/hippoz/raven")
arch=('i686' 'pentium4' 'x86_64' 'arm' 'armv7h' 'armv6h' 'aarch64')
license=('MIT')
depends=('pango' 'cairo' 'libx11')
makedepends=('meson')
sha256sums=(SKIP)
pkgver() {
cd raven
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
arch-meson raven build
meson compile -C build
}
package() {
meson install -C build --destdir "$pkgdir"
}

View file

@ -1,87 +0,0 @@
#include "Application.hpp"
#include <poll.h>
#include <unistd.h>
namespace Raven {
bool Application::wake() {
if (!m_wakeup_pipe_available) {
return false;
}
uint32_t buf = 0;
write(m_wakeup_pipe_write_end, &buf, 1);
return true;
}
void Application::add_window(std::shared_ptr<Window> window) {
m_windows.push_back(window);
update_fds();
}
void Application::update_fds() {
if (m_should_create_wakeup_pipe) {
m_should_create_wakeup_pipe = false;
int pipe_fds[2];
if (pipe(pipe_fds) != 0) {
// TODO: handle this?
return;
}
m_wakeup_pipe_read_end = pipe_fds[0];
m_wakeup_pipe_write_end = pipe_fds[1];
m_wakeup_pipe_available = true;
}
for (int i = 0; i < RAVEN_APPLICATION_MAX_WINDOWS; i++) {
m_fds[i].fd = -1;
m_fds[i].events = 0;
m_fds[i].revents = 0;
}
for (size_t i = 0; i < m_windows.size(); i++) {
m_fds[i].fd = m_windows[i]->file_descriptor();
m_fds[i].events = POLL_IN;
}
if (m_wakeup_pipe_available) {
int wakeup_pipe_index = m_windows.size();
m_fds[wakeup_pipe_index].fd = m_wakeup_pipe_read_end;
m_fds[wakeup_pipe_index].events = POLL_IN;
m_fds[wakeup_pipe_index].revents = 0;
}
}
int Application::turn() {
if (m_fds_need_update) {
update_fds();
m_fds_need_update = false;
}
if (poll(m_fds, m_windows.size() + 1, -1) > 0) {
for (size_t i = 0; i < m_windows.size(); i++) {
m_windows[i]->run(false);
}
while (!m_microtasks.empty()) {
auto callback = m_microtasks.front();
callback();
m_microtasks.pop();
}
} else {
return 1;
}
return 0;
}
int Application::run() {
// Make sure any pending events are dealt with before starting the loop
for (size_t i = 0; i < m_windows.size(); i++) {
m_windows[i]->run(false);
}
for (;;) {
int ret = turn();
if (ret != 0) {
return ret;
}
}
return 0;
}
}

View file

@ -1,43 +0,0 @@
#pragma once
#include <memory>
#include <vector>
#include <poll.h>
#include "Window.hpp"
namespace Raven {
#define RAVEN_APPLICATION_MAX_WINDOWS 1023
class Application {
public:
Application() {}
Application(bool should_create_wakeup_pipe)
: m_should_create_wakeup_pipe(should_create_wakeup_pipe) {}
void add_window(std::shared_ptr<Window>);
bool wake();
void queue_microtask(std::function<void()> callback) { m_microtasks.push(callback); }
int turn();
int run();
template<typename T, class... Args>
std::shared_ptr<T> add_window(Args&&... args) {
std::shared_ptr<T> child = std::make_shared<T>(std::forward<Args>(args)...);
add_window(child);
return child;
}
private:
void update_fds();
std::vector<std::shared_ptr<Window>> m_windows;
struct pollfd m_fds[RAVEN_APPLICATION_MAX_WINDOWS];
bool m_fds_need_update { true };
bool m_should_create_wakeup_pipe { false };
bool m_wakeup_pipe_available { false };
int m_wakeup_pipe_read_end { -1 };
int m_wakeup_pipe_write_end { -1 };
std::queue<std::function<void()>> m_microtasks;
};
}

View file

@ -1,130 +0,0 @@
#include "Box.hpp"
#include "Point.hpp"
#include <iostream>
namespace Raven {
Box Box::offset(double x, double y) {
return Box{
m_x + x,
m_y + y,
m_width,
m_height
};
}
bool Box::contains(double x, double y) const {
return x >= m_x && m_x + m_width >= x && y >= m_y && m_y + m_height >= y;
}
bool Box::contains(const Point &point) const {
return point.x() >= m_x && m_x + m_width >= point.x() && point.y() >= m_y && m_y + m_height >= point.y();
}
bool Box::contains(const Box &other) const {
double ax1 = m_x;
double ax2 = m_x + m_width;
double ay1 = m_y;
double ay2 = m_y + m_height;
double bx1 = other.x();
double bx2 = other.x() + other.width();
double by1 = other.y();
double by2 = other.y() + other.height();
// https://stackoverflow.com/a/306332
bool boxes_overlap = (ax1 < bx2 && ax2 > bx1 && ay1 < by2 && ay2 > by1);
return boxes_overlap;
}
void Box::set_width(double width) {
if (m_max_width != -1 && width > m_max_width) {
m_width = m_max_width;
} else if (m_min_width != -1 && width < m_min_width) {
m_width = m_min_width;
} else {
m_width = width;
}
}
void Box::set_height(double height) {
if (m_max_height != -1 && height > m_max_height) {
m_height = m_max_height;
} else if (m_min_height != -1 && height < m_min_height) {
m_height = m_min_height;
} else {
m_height = height;
}
}
void Box::update() {
set_width(m_width);
set_height(m_height);
}
Box Box::max_geometry() {
auto max_width = m_max_width == -1 ? m_width : m_max_width;
auto max_height = m_max_height == -1 ? m_height : m_max_height;
return Box{ m_x, m_y, max_width, max_height };
}
Box Box::min_geometry() {
auto min_width = m_min_width == -1 ? m_width : m_min_width;
auto min_height = m_min_height == -1 ? m_height : m_min_height;
return Box{ m_x, m_y, min_width, min_height };
}
double Box::clamp_for_dimension(Direction direction, double value) {
if (direction == Direction::Vertical) {
if (m_max_height != -1 && value > m_max_height) {
return m_max_height;
} else if (m_min_height != -1 && value < m_min_height) {
return m_min_height;
} else {
return value;
}
} else {
if (m_max_width != -1 && value > m_max_width) {
return m_max_width;
} else if (m_min_width != -1 && value < m_min_width) {
return m_min_width;
} else {
return value;
}
}
}
void Box::set_dimension(Direction direction, double value) {
if (direction == Direction::Horizontal) {
set_width(value);
} else {
set_height(value);
}
}
Box Box::united(const Box &other) {
if (is_null()) {
return other;
}
if (other.is_null()) {
return *this;
}
Box rect;
rect.set_left(std::min(left(), other.left()));
rect.set_top(std::min(top(), other.top()));
rect.set_right(std::max(right(), other.right()));
rect.set_bottom(std::max(bottom(), other.bottom()));
return rect;
}
bool Box::is_null() const {
return !(m_x || m_y || m_width || m_height);
}
std::string Box::debug() {
return "Box(" + std::to_string(m_x) + ", " + std::to_string(m_y) + ", " + std::to_string(m_width) + ", " + std::to_string(m_height) + ")";
}
}

View file

@ -1,92 +0,0 @@
#pragma once
#include "Point.hpp"
#include <string>
namespace Raven {
enum class Direction {
Horizontal,
Vertical
};
class Box {
private:
double m_x {0};
double m_y {0};
double m_width {0};
double m_height {0};
double m_max_width {-1};
double m_max_height {-1};
double m_min_width {-1};
double m_min_height {-1};
public:
Box() {}
Box(double x, double y, double width, double height)
: m_x(x)
, m_y(y)
, m_width(width)
, m_height(height)
{
}
Box max_geometry();
Box min_geometry();
double x() { return m_x; }
double y() { return m_y; }
double width() { return m_width; }
double height() { return m_height; }
double min_width() { return m_min_width; }
double min_height() { return m_min_height; }
double max_width() { return m_max_width; }
double max_height() { return m_max_height; }
double x() const { return m_x; }
double y() const { return m_y; }
double width() const { return m_width; }
double height() const { return m_height; }
double min_width() const { return m_min_width; }
double min_height() const { return m_min_height; }
double max_width() const { return m_max_width; }
double max_height() const { return m_max_height; }
double top() const { return y(); }
double bottom() const { return y() + height() - 1; }
double left() const { return x(); }
double right() const { return x() + width() - 1; }
bool is_null() const;
double dimension_at(Direction direction) { return direction == Direction::Horizontal ? width() : height(); }
double max_dimension_at(Direction direction) { return direction == Direction::Horizontal ? max_width() : max_height(); }
double min_dimension_at(Direction direction) { return direction == Direction::Horizontal ? min_width() : min_height(); }
double clamp_for_dimension(Direction direction, double value);
void set_x(double x) { m_x = x; }
void set_y(double y) { m_y = y; }
void set_width(double width);
void set_height(double height);
void set_top(double top) { set_y(top); }
void set_bottom(double bottom) { set_height(bottom - y() + 1); }
void set_right(double right) { set_width(right - x() + 1); }
void set_left(double left) { set_x(left); }
void set_dimension(Direction direction, double value);
void set_max_width(double max_width) { m_max_width = max_width; set_width(m_width); }
void set_max_height(double max_height) { m_max_height = max_height; set_height(m_height); }
void set_min_width(double min_width) { m_min_width = min_width; set_width(m_width); }
void set_min_height(double min_height) { m_min_height = min_height; set_height(m_height); }
void update();
bool contains(double x, double y) const;
bool contains(const Point &point) const;
bool contains(const Box &other) const;
Box offset(double x, double y);
Box united(const Box &other);
std::string debug();
};
}

View file

@ -1,147 +0,0 @@
#include "BoxLayout.hpp"
#include "Box.hpp"
#include "Widget.hpp"
#include "Events.hpp"
#include <cmath>
#include <vector>
#include <algorithm>
namespace Raven {
bool BoxLayout::run() {
if (!m_target) {
return false;
}
double total_space = m_target->rect().dimension_at(m_direction) - 2 * m_margin;
double free_space = total_space;
double maximum_secondary_dimension = 0.0;
Direction secondary_direction = m_direction == Direction::Horizontal ? Direction::Vertical : Direction::Horizontal;
Point current_position { m_margin, m_margin };
int unslotted_widgets = 0;
std::vector<Slot> working_slots(m_slots);
for (unsigned int i = 0; i < m_target->children().size(); i++) {
auto child = m_target->children()[i];
// dirty hack for DocumentLayout
if (child->layout() && child->layout()->depends_on_width() && m_direction == Direction::Vertical && !m_justify_secondary_dimension) {
child->rect().set_width(m_target->rect().width() - 2 * m_margin);
}
if (child->layout() && child->layout()->dynamically_sizes_target()) {
auto event = RelayoutSubtreeEvent();
child->dispatch_event(event);
}
auto child_secondary_dimension = child->rect().dimension_at(secondary_direction);
if (child_secondary_dimension > maximum_secondary_dimension) {
maximum_secondary_dimension = child_secondary_dimension;
}
if (i >= working_slots.size()) {
// widgets which are outside the pre-defined slot range
bool widget_has_fixed_size = child->rect().max_dimension_at(m_direction) != -1 || child->rect().min_dimension_at(m_direction) != -1;
bool widget_grows = child->grows();
bool is_widget_unslotted = !widget_has_fixed_size && widget_grows;
if (is_widget_unslotted) {
// we can consider non-growing widgets with no size perference "unslotted". these widgets will be evenly spaced amongst themselves.
unslotted_widgets++;
working_slots.push_back(Slot{ 0, 0, SlotType::Auto });
} else {
// if the widget has a size preference, we will use its dimensions instead
child->rect().update();
working_slots.push_back(Slot { 0, child->rect().dimension_at(m_direction), SlotType::Pixel });
free_space -= child->rect().dimension_at(m_direction) + m_spacing;
}
} else {
// widgets which have a pre-defined slot
// if the slot is fixed, we can already clamp it and subtract from free_space
if (working_slots[i].type == SlotType::Pixel) {
working_slots[i].pixel = child->rect().clamp_for_dimension(m_direction, working_slots[i].pixel);
free_space -= working_slots[i].pixel + m_spacing;
}
}
}
maximum_secondary_dimension = m_target->rect().clamp_for_dimension(secondary_direction, maximum_secondary_dimension);
// compute all percentages for slots
for (unsigned int i = 0; i < m_target->children().size(); i++) {
Slot *slot = &working_slots[i];
auto child = m_target->children()[i];
if (slot->type == SlotType::Percent) {
slot->pixel = ((free_space / 100.0) * slot->percent);
slot->pixel = child->rect().clamp_for_dimension(m_direction, slot->pixel);
free_space -= slot->pixel + m_spacing;
}
}
double spacing_targets = (unslotted_widgets - 1);
if (spacing_targets < 0)
spacing_targets = 0;
free_space -= spacing_targets * m_spacing;
double space_per_unslotted_widget = free_space / unslotted_widgets;
for (unsigned int i = 0; i < m_target->children().size(); i++) {
auto child = m_target->children()[i];
Slot slot = working_slots[i];
if (slot.type == SlotType::Auto) {
slot.pixel = space_per_unslotted_widget;
}
// make sure pixel values are aligned to the pixel grid
slot.pixel = std::floor(slot.pixel);
// position all children according to the slots
child->rect().set_x(current_position.x());
child->rect().set_y(current_position.y());
if (m_direction == Direction::Horizontal) {
child->rect().set_width(slot.pixel);
child->rect().set_height(m_justify_secondary_dimension ? maximum_secondary_dimension - 2 * m_margin : m_target->rect().max_geometry().height() - 2 * m_margin);
current_position.add(slot.pixel + m_spacing, 0);
} else {
child->rect().set_width(m_justify_secondary_dimension ? maximum_secondary_dimension - 2 * m_margin : m_target->rect().max_geometry().width() - 2 * m_margin);
child->rect().set_height(slot.pixel);
current_position.add(0, slot.pixel + m_spacing);
}
}
if (m_justify_secondary_dimension) {
m_target->rect().set_dimension(secondary_direction, maximum_secondary_dimension);
}
for (unsigned int i = 0; i < m_target->children().size(); i++) {
auto child = m_target->children()[i];
auto event = RelayoutSubtreeEvent();
child->dispatch_event(event);
}
return true;
};
void BoxLayout::slot_percent(double percent) {
m_slots.push_back(Slot { percent, 0, SlotType::Percent });
}
void BoxLayout::slot_pixel(double pixel) {
m_slots.push_back(Slot { 0, pixel, SlotType::Pixel });
}
void BoxLayout::slot_pixel(double pixel, double times) {
for (int i = 0; i < times; i++) {
slot_pixel(pixel);
}
}
}

View file

@ -1,53 +0,0 @@
#pragma once
#include "Layout.hpp"
#include "Widget.hpp"
#include "Box.hpp"
#include <memory>
#include <vector>
namespace Raven {
class BoxLayout : public Layout {
public:
enum class SlotType {
No,
Percent,
Pixel,
Auto
};
struct Slot {
double percent, pixel;
SlotType type;
};
public:
BoxLayout(Direction direction)
: Layout()
, m_direction(direction) {}
~BoxLayout() {}
bool run() override;
void slot_percent(double percent);
void slot_pixel(double pixel);
void slot_pixel(double pixel, double times);
void set_margin(double margin) { m_margin = margin; }
double &margin() { return m_margin; }
void set_spacing(double spacing) { m_spacing = spacing; }
double &spacing() { return m_spacing; }
bool justify_secondary_dimension() { return m_justify_secondary_dimension; }
void set_justify_secondary_dimension(bool justify_secondary_dimension) { m_justify_secondary_dimension = justify_secondary_dimension; }
bool dynamically_sizes_target() override { return m_justify_secondary_dimension; }
private:
double m_margin { 0.0 };
double m_spacing { 0.0 };
bool m_justify_secondary_dimension { false };
std::vector<Slot> m_slots;
Direction m_direction { Direction::Horizontal };
};
}

View file

@ -1,42 +0,0 @@
#include <iostream>
#include "Button.hpp"
#include "Box.hpp"
#include "Window.hpp"
#include "Painter.hpp"
#include "Styles.hpp"
#include "pango/pango-layout.h"
#include <pango/pangocairo.h>
namespace Raven {
void Button::set_text(std::string text) {
m_text = text;
if (!fit_text(text, m_padding)) {
repaint();
}
}
void Button::on_init() {
if (m_button_type == ButtonType::Flat) {
set_style_pure(&flat_button_style);
} else if (m_button_type == ButtonType::Accent) {
set_style_pure(&accent_button_style);
}
set_did_init(true);
if (!fit_text(m_text, m_padding)) {
reflow();
}
}
void Button::on_paint() {
auto painter = window()->painter();
painter.source_rgb(style()->foreground());
// todo: do we use the max geometry like in Label?
painter.text(rect(), m_text, PaintTextAlign::Center, PANGO_ELLIPSIZE_END, style()->font_description());
painter.fill();
}
}

View file

@ -1,38 +0,0 @@
#pragma once
#include <string>
#include <functional>
#include "Widget.hpp"
#include "RGB.hpp"
#include "pango/pango-font.h"
#include "Window.hpp"
namespace Raven {
class Button : public Widget {
public:
enum ButtonType {
Flat,
Accent
};
public:
Button(std::string text, ButtonType button_type = ButtonType::Flat)
: Widget(WidgetType::Button)
, m_text(text)
, m_button_type(button_type) {}
void set_text(std::string text);
std::string &text() { return m_text; }
double padding() { return m_padding; }
void set_padding(double padding) { m_padding = padding; reflow(); }
protected:
void on_paint();
void on_init();
private:
std::string m_text;
ButtonType m_button_type;
double m_padding { 8 };
};
}

View file

@ -1,61 +0,0 @@
#include "DocumentLayout.hpp"
#include "Point.hpp"
#include "Widget.hpp"
#include "Events.hpp"
#include <iostream>
namespace Raven {
bool DocumentLayout::run() {
if (!m_target)
return false;
Point current_position { m_margin, m_margin };
double largest_height_so_far = -1.0;
double desired_height = m_margin * 2;
auto& children = m_target->children();
for (auto child : children) {
if (child->absolute())
continue;
if (child->layout() && child->layout()->dynamically_sizes_target()) {
auto event = RelayoutSubtreeEvent();
child->dispatch_event(event);
}
if (child->rect().height() > largest_height_so_far) {
largest_height_so_far = child->rect().height();
}
bool new_row_because_of_control_widget = (child->control_type() == ControlWidgetType::NewRow);
bool new_row_because_of_justification = (current_position.x() + child->rect().width() + m_margin) >= m_target->rect().max_geometry().width();
bool should_do_new_row = new_row_because_of_control_widget || new_row_because_of_justification;
if (should_do_new_row) {
current_position.add(0, largest_height_so_far + m_margin);
desired_height += largest_height_so_far + m_margin;
current_position.set_x(m_margin);
}
child->rect().set_x(current_position.x());
child->rect().set_y(current_position.y());
if (!new_row_because_of_control_widget) {
current_position.add(child->rect().width() + m_margin, 0);
}
}
for (auto &child : m_target->children()) {
auto event = RelayoutSubtreeEvent();
child->dispatch_event(event);
}
/* account for the first row */
desired_height += largest_height_so_far;
m_target->rect().set_height(desired_height);
return true;
}
}

View file

@ -1,27 +0,0 @@
#pragma once
#include "Layout.hpp"
namespace Raven {
class DocumentLayout : public Layout {
private:
double m_margin { 0.0 };
public:
DocumentLayout()
: Layout() {}
DocumentLayout(double margin)
: Layout()
, m_margin(margin) {}
bool run() override;
double margin() { return m_margin; }
void set_margin(double margin) { m_margin = margin; run(); }
bool dynamically_sizes_target() override { return true; }
bool depends_on_width() override { return true; }
};
}

View file

@ -1,160 +0,0 @@
#pragma once
#include <X11/X.h>
#include <stdint.h>
#include "Point.hpp"
#include "Box.hpp"
namespace Raven {
enum class EventType {
NoneEvent,
MouseButton,
MouseMove,
RelayoutSubtree,
RepaintRect,
FocusUpdate,
ActivationUpdate,
Key
};
class Event {
public:
Event() {}
virtual EventType type() { return EventType::NoneEvent; }
virtual const char *name() { return "NoneEvent"; }
virtual ~Event() = default;
};
class KeyEvent : public Event {
public:
using RavenKeySym = KeySym;
enum class KeyStatus {
HasNone = 0,
HasKeySym,
HasKeyChars,
HasBoth
};
KeyEvent(RavenKeySym key_sym, std::string key_chars, KeyStatus status, bool control_pressed, bool shift_pressed)
: Event()
, m_key_sym(key_sym)
, m_key_chars(key_chars)
, m_status(status)
, m_control_pressed(control_pressed)
, m_shift_pressed(shift_pressed) {}
EventType type() { return EventType::Key; }
const char *name() { return "Key"; }
RavenKeySym key_sym() { return m_key_sym; }
std::string &key_chars() { return m_key_chars; }
KeyStatus status() { return m_status; }
bool control_pressed() { return m_control_pressed; }
bool shift_pressed() { return m_shift_pressed; }
private:
RavenKeySym m_key_sym {};
std::string m_key_chars {};
KeyStatus m_status { KeyStatus::HasNone };
bool m_control_pressed { false };
bool m_shift_pressed { false };
};
class MouseButtonEvent : public Event {
private:
bool m_was_left_button_pressed;
bool m_was_right_button_pressed;
bool m_did_scroll_up;
bool m_did_scroll_down;
Point m_point;
public:
MouseButtonEvent(bool was_left_button_pressed, bool was_right_button_pressed, bool did_scroll_up, bool did_scroll_down, Point point)
: m_was_left_button_pressed(was_left_button_pressed)
, m_was_right_button_pressed(was_right_button_pressed)
, m_did_scroll_up(did_scroll_up)
, m_did_scroll_down(did_scroll_down)
, m_point(point) {}
EventType type() { return EventType::MouseButton; }
const char *name() { return "MouseButton"; }
bool was_left_button_pressed() { return m_was_left_button_pressed; }
bool was_right_button_pressed() { return m_was_right_button_pressed; }
bool did_scroll_up() { return m_did_scroll_up; }
bool did_scroll_down() { return m_did_scroll_down; }
Point &point() { return m_point; }
};
class MouseMoveEvent : public Event {
private:
Point m_point;
public:
MouseMoveEvent(Point point)
: m_point(point) {}
EventType type() { return EventType::MouseMove; }
const char *name() { return "MouseMove"; }
Point &point() { return m_point; }
void set_point(Point point) { m_point = point; }
};
class RepaintRectEvent : public Event {
private:
bool m_grouping { true };
Box m_box;
public:
RepaintRectEvent(bool grouping, Box box)
: m_grouping(grouping)
, m_box(box) {}
EventType type() { return EventType::RepaintRect; }
const char *name() { return "RepaintRect"; }
bool grouping() { return m_grouping; }
Box &box() { return m_box; }
void set_grouping(bool grouping) { m_grouping = grouping; }
void set_box(Box box) { m_box = box; }
};
class RelayoutSubtreeEvent : public Event {
public:
RelayoutSubtreeEvent() {}
EventType type() { return EventType::RelayoutSubtree; }
const char *name() { return "RelayoutSubtree"; }
};
class FocusUpdateEvent : public Event {
private:
bool m_focus_status;
public:
FocusUpdateEvent(bool focus_status)
: m_focus_status(focus_status) {}
EventType type() { return EventType::FocusUpdate; }
const char *name() { return "FocusUpdate"; }
bool focus_status() { return m_focus_status; }
};
class ActivationUpdateEvent : public Event {
private:
bool m_activation_status;
public:
ActivationUpdateEvent(bool activation_status)
: m_activation_status(activation_status) {}
EventType type() { return EventType::ActivationUpdate; }
const char *name() { return "ActivationUpdate"; }
bool activation_status() { return m_activation_status; }
};
}

View file

@ -1,13 +0,0 @@
namespace Raven {
class GenericStyle;
class Box;
class Button;
class Events;
class Painter;
class Point;
class Layout;
class Widget;
class Window;
enum class EventType;
}

View file

@ -1,49 +0,0 @@
#pragma once
#include <pango/pango-font.h>
#include "RGB.hpp"
#define __RAVEN_STYLE_PROP(name, type, ...) \
private: \
type m_##name {__VA_ARGS__}; \
public: \
void set_##name(type new_prop_value) { m_##name = new_prop_value; style_prop_updated(); } \
type name() { return m_##name; } \
private:
namespace Raven {
class GenericStyle {
__RAVEN_STYLE_PROP(font_description, PangoFontDescription*, nullptr)
__RAVEN_STYLE_PROP(foreground, RGB, 0.00000, 0.00000, 0.00000)
__RAVEN_STYLE_PROP(background_norm, RGB, 1.00000, 1.00000, 1.00000)
__RAVEN_STYLE_PROP(background_focused, RGB, 1.00000, 1.00000, 1.00000)
__RAVEN_STYLE_PROP(background_active, RGB, 1.00000, 1.00000, 1.00000)
__RAVEN_STYLE_PROP(border_radius, double, 0.0)
__RAVEN_STYLE_PROP(fill_background, bool, true)
__RAVEN_STYLE_PROP(update_background, bool, false)
private:
void style_prop_updated() { }
public:
GenericStyle(
PangoFontDescription* font_description,
RGB foreground,
RGB background_norm,
RGB background_focused,
RGB background_active,
double border_radius,
bool fill_background,
bool update_background
)
: m_font_description(font_description)
, m_foreground(foreground)
, m_background_norm(background_norm)
, m_background_focused(background_focused)
, m_background_active(background_active)
, m_border_radius(border_radius)
, m_fill_background(fill_background)
, m_update_background(update_background) {}
};
}

View file

@ -1,35 +0,0 @@
#include "Label.hpp"
#include "Window.hpp"
#include "pango/pango-layout.h"
#include "Styles.hpp"
#include <iostream>
namespace Raven {
void Label::set_text(std::string text) {
m_text = text;
if (!fit_text(text)) {
repaint();
}
}
void Label::on_init() {
set_style_pure(&flat_label_style);
set_did_init(true);
if (!fit_text(m_text)) {
reflow();
}
}
void Label::on_paint() {
auto painter = window()->painter();
auto geometry = rect().max_geometry();
painter.source_rgb(style()->foreground());
painter.text(geometry, m_text, m_align, PANGO_ELLIPSIZE_END, style()->font_description());
painter.fill();
}
}

View file

@ -1,34 +0,0 @@
#pragma once
#include "Widget.hpp"
#include "Painter.hpp"
namespace Raven {
class Label : public Widget {
public:
Label(std::string text)
: Widget(WidgetType::Label)
, m_text(text) {}
Label(std::string text, PaintTextAlign align)
: Widget(WidgetType::Label)
, m_text(text)
, m_align(align) {}
~Label() {}
void set_text(std::string text);
std::string &text() { return m_text; }
PaintTextAlign &align() { return m_align; }
void set_align(PaintTextAlign align) { m_align = align; }
protected:
void on_paint();
void on_init();
private:
std::string m_text;
PaintTextAlign m_align { PaintTextAlign::Left };
};
}

View file

@ -1,24 +0,0 @@
#pragma once
#include "Forward.hpp"
namespace Raven {
class Layout {
protected:
Widget *m_target { nullptr };
public:
Layout() {}
virtual bool run() {return false;}
void bind_to(Widget *target) { m_target = target; }
Widget *target() { return m_target; }
virtual bool dynamically_sizes_target() { return false; }
virtual bool depends_on_width() { return false; }
virtual ~Layout() {}
};
}

View file

@ -1,70 +0,0 @@
#include "ListLayout.hpp"
#include "Widget.hpp"
#include "Box.hpp"
#include "Events.hpp"
namespace Raven {
bool ListLayout::run() {
if (!m_target) {
return false;
}
double current_position = m_margin;
double maximum_secondary_dimension = m_margin;
if (m_direction == Direction::Horizontal) {
for (auto &child : m_target->children()) {
if (child->layout() && child->layout()->dynamically_sizes_target()) {
auto event = RelayoutSubtreeEvent();
child->dispatch_event(event);
}
child->rect().set_x(current_position);
child->rect().set_y(m_margin);
current_position += child->rect().width() + m_spacing;
if (m_inherit_secondary_dimension) {
child->rect().set_height(m_target->rect().height() - m_margin * 2);
} else if (child->rect().height() > maximum_secondary_dimension) {
maximum_secondary_dimension = child->rect().height();
}
}
if (m_inherit_secondary_dimension) {
maximum_secondary_dimension = m_target->rect().height();
}
m_target->rect().set_width(current_position + m_margin);
m_target->rect().set_height(maximum_secondary_dimension + m_margin * 2);
} else {
for (auto &child : m_target->children()) {
if (child->layout() && child->layout()->dynamically_sizes_target()) {
auto event = RelayoutSubtreeEvent();
child->dispatch_event(event);
}
child->rect().set_y(current_position);
child->rect().set_x(m_margin);
current_position += child->rect().height() + m_spacing;
if (m_inherit_secondary_dimension) {
child->rect().set_width(m_target->rect().width() - m_margin * 2);
} else if (child->rect().width() > maximum_secondary_dimension) {
maximum_secondary_dimension = child->rect().width();
}
}
if (m_inherit_secondary_dimension) {
maximum_secondary_dimension = m_target->rect().width();
}
m_target->rect().set_width(maximum_secondary_dimension + m_margin * 2);
m_target->rect().set_height(current_position + m_margin);
}
for (auto &child : m_target->children()) {
auto event = RelayoutSubtreeEvent();
child->dispatch_event(event);
}
return true;
}
}

View file

@ -1,34 +0,0 @@
#pragma once
#include "Layout.hpp"
#include "Box.hpp"
namespace Raven {
class ListLayout : public Layout {
public:
ListLayout(Direction direction)
: Layout()
, m_direction(direction) {}
bool run() override;
double margin() { return m_margin; }
void set_margin(double margin) { m_margin = margin; run(); }
double spacing() { return m_spacing; }
void set_spacing(double spacing) { m_spacing = spacing; run(); }
bool inherit_secondary_dimension() { return m_inherit_secondary_dimension; }
void set_inherit_secondary_dimension(bool inherit_secondary_dimension) { m_inherit_secondary_dimension = inherit_secondary_dimension; }
bool dynamically_sizes_target() override { return true; }
private:
double m_margin { 0.0 };
double m_spacing { 0.0 };
bool m_inherit_secondary_dimension { false };
Direction m_direction;
};
}

View file

@ -1,102 +0,0 @@
#include "ListView.hpp"
#include "pango/pango-layout.h"
#include "Painter.hpp"
namespace Raven {
void ListView::on_init() {
set_did_init(true);
set_style_pure(&flat_listview_style);
reflow();
}
void ListView::elements_updated() {
repaint();
}
void ListView::set_active_element(unsigned int active_element) {
if (active_element < 0 || active_element >= elements.size()) {
return;
}
m_active_element = active_element;
if (on_selection) {
on_selection(active_element, elements[active_element]);
}
repaint();
}
void ListView::on_mouse_button(MouseButtonEvent &event) {
if (!rect().contains(event.point())) {
return;
}
if (event.was_left_button_pressed()) {
for (unsigned int i = 0; i < elements.size(); i++) {
auto element = elements[i];
double item_y = rect().y() + m_item_height * i;
double event_y = event.point().y() + m_scroll;
if (event_y >= item_y && event_y <= item_y + m_item_height) {
set_active_element(i);
break;
}
}
} else if (event.did_scroll_down()) {
m_scroll += m_scroll_step;
auto items_size = m_item_height * elements.size();
if (m_scroll > items_size) {
m_scroll = items_size;
}
repaint();
} else if (event.did_scroll_up()) {
m_scroll -= m_scroll_step;
if (m_scroll < 0) {
m_scroll = 0;
}
repaint();
}
}
void ListView::on_paint() {
for (unsigned int i = 0; i < elements.size(); i++) {
auto element = elements[i];
double y_pos = m_item_height * i;
double y = y_pos - m_scroll;
/* ensure the item is visible before painting */
double visible_extents_start = rect().y() + m_scroll;
double visible_extents_end = rect().y() + rect().height() + m_scroll;
if ((y_pos + m_item_height) < visible_extents_start) {
// This item is out of view, however, we cannot exit the loop here as there are candidates for painting ahead.
continue;
} else if (y_pos > visible_extents_end) {
// The item is out of view and we also don't have any further candidates for painting.
break;
}
/* paint: background */
{
if (m_active_element == i) {
painter()->source_rgb(style()->background_active());
} else if ((i % 2) == 0) {
painter()->source_rgb(style()->background_focused());
} else {
painter()->source_rgb(style()->background_norm());
}
painter()->cairo()->rectangle(0, y, item_width(), item_height());
painter()->fill();
}
/* paint: text */
{
painter()->cairo()->move_to(0, y);
painter()->source_rgb(style()->foreground());
Box item_geometry = {0, 0, item_width(), item_height() };
painter()->text(item_geometry, element, PaintTextAlign::Left, PangoEllipsizeMode::PANGO_ELLIPSIZE_END, style()->font_description());
painter()->fill();
}
}
}
}

View file

@ -1,42 +0,0 @@
#pragma once
#include "Widget.hpp"
namespace Raven {
class ListView : public Raven::Widget {
public:
ListView()
: Raven::Widget() {}
std::vector<std::string> elements;
void elements_updated();
double item_height() { return m_item_height; }
void set_item_height(double item_height) { m_item_height = item_height; reflow(); }
double scroll() { return m_scroll; }
void set_scroll(double scroll) { m_scroll = scroll; repaint(); }
double scroll_step() { return m_scroll_step; }
void set_scroll_step(double scroll_step) { m_scroll_step = scroll_step; }
double item_width() { return rect().width(); }
unsigned int active_element() { return m_active_element; }
void set_active_element(unsigned int active_element);
std::function<void(unsigned int, std::string)> on_selection { nullptr };
protected:
void on_paint() override;
void on_mouse_button(MouseButtonEvent &event) override;
void on_init() override;
private:
unsigned int m_active_element { 0 };
double m_item_height { 26.0 };
double m_scroll { 0.0 };
double m_scroll_step { 26.0 * 2 };
};
}

View file

@ -1,7 +0,0 @@
#pragma once
#include <iostream>
#define ERROR std::clog << "[ERROR] (" << __FILE__ << ":" << __LINE__ << "): "
#define INFO std::clog << "[INFO] (" << __FILE__ << ":" << __LINE__ << "): "
#define WARN std::clog << "[WARN] (" << __FILE__ << ":" << __LINE__ << "): "

View file

@ -1,122 +0,0 @@
#include "Painter.hpp"
#include "RGB.hpp"
#include "cairomm/surface.h"
#include "pango/pango-layout.h"
#include "pango/pango-types.h"
#include "pango/pangocairo.h"
#include "Box.hpp"
namespace Raven {
void Painter::rounded_rectangle(Box &geometry, double border_radius) {
if (border_radius == 0.0) {
m_cairo->rectangle(geometry.x(), geometry.y(), geometry.width(), geometry.height());
return;
}
double aspect = 1.0;
double radius = border_radius / aspect;
double degrees = M_PI / 180.0;
double x = geometry.x();
double y = geometry.y();
double w = geometry.width();
double h = geometry.height();
m_cairo->begin_new_sub_path();
m_cairo->arc(x + w - radius, y + radius, radius, -90 * degrees, 0 * degrees);
m_cairo->arc(x + w - radius, y + h - radius, radius, 0 * degrees, 90 * degrees);
m_cairo->arc(x + radius, y + h - radius, radius, 90 * degrees, 180 * degrees);
m_cairo->arc(x + radius, y + radius, radius, 180 * degrees, 270 * degrees);
m_cairo->close_path();
}
Point Painter::compute_text_size(Box &widget_geometry, std::string &text, PangoFontDescription *pango_font_description) {
PangoLayout *layout = pango_cairo_create_layout(m_cairo->cobj());
int font_width;
int font_height;
pango_layout_set_font_description(layout, pango_font_description);
if (widget_geometry.max_width() > 0)
pango_layout_set_width(layout, pango_units_from_double(widget_geometry.max_width()));
if (widget_geometry.max_height() > 0)
pango_layout_set_height(layout, pango_units_from_double(widget_geometry.max_height()));
pango_layout_set_text(layout, text.c_str(), -1);
pango_layout_get_size(layout, &font_width, &font_height);
g_object_unref(layout);
return { pango_units_to_double(font_width), pango_units_to_double(font_height) };
}
void Painter::text(Box &geometry, std::string &text, PaintTextAlign align, PangoEllipsizeMode ellipsize, PangoFontDescription *pango_font_description, int cursor_pos) {
PangoLayout *layout = pango_cairo_create_layout(m_cairo->cobj());
int font_width;
int font_height;
pango_layout_set_font_description(layout, pango_font_description);
if (geometry.width() > 0)
pango_layout_set_width(layout, pango_units_from_double(geometry.width()));
if (geometry.height() > 0)
pango_layout_set_height(layout, pango_units_from_double(geometry.height()));
pango_layout_set_ellipsize(layout, ellipsize);
pango_layout_set_text(layout, text.c_str(), -1);
pango_layout_get_pixel_size(layout, &font_width, &font_height);
double x = 0;
double y = ((geometry.height() - font_height) / 2);
if (align == PaintTextAlign::Center) {
x = ((geometry.width() - font_width) / 2);
}
if (m_cairo->has_current_point()) {
m_cairo->rel_move_to(x, y);
} else {
m_cairo->move_to(x, y);
}
pango_cairo_show_layout(m_cairo->cobj(), layout);
if (cursor_pos >= 0) {
PangoRectangle pango_cursor_rect;
pango_layout_get_cursor_pos(layout, cursor_pos, &pango_cursor_rect, NULL);
Box cursor_rect = {
static_cast<double>(pango_units_to_double(pango_cursor_rect.x)),
pango_units_to_double(pango_cursor_rect.y) + y,
1.0,
static_cast<double>(pango_units_to_double(pango_cursor_rect.height))
};
m_cairo->rectangle(cursor_rect.x(), cursor_rect.y(), cursor_rect.width(), cursor_rect.height());
}
g_object_unref(layout);
}
void Painter::source_rgb(RGB source_rgb) {
m_cairo->set_source_rgb(source_rgb.r(), source_rgb.g(), source_rgb.b());
}
void Painter::fill() {
m_cairo->fill();
}
void Painter::begin_paint_group() {
m_cairo->push_group();
}
void Painter::end_paint_group() {
m_cairo->pop_group_to_source();
m_cairo->paint();
}
void Painter::flush_paint_group() {
m_cairo->pop_group_to_source();
m_cairo->paint();
m_cairo->get_target()->flush();
}
}

View file

@ -1,40 +0,0 @@
#pragma once
#include "Box.hpp"
#include "Point.hpp"
#include "RGB.hpp"
#include "cairomm/xlib_surface.h"
#include <cairomm/cairomm.h>
#include <pango/pangocairo.h>
namespace Raven {
enum class PaintTextAlign {
Center = 0,
Left
};
class Painter {
private:
Cairo::RefPtr<Cairo::Context> m_cairo { nullptr };
public:
Painter() {}
Cairo::RefPtr<Cairo::Context> cairo() { return m_cairo; }
void set_cairo(Cairo::RefPtr<Cairo::Context> cairo) { m_cairo = cairo; }
void rounded_rectangle(Box &geometry, double border_radius);
Point compute_text_size(Box &widget_geometry, std::string &text, PangoFontDescription *pango_font_description);
void text(Box &geometry, std::string &text, PaintTextAlign align, PangoEllipsizeMode ellipsize, PangoFontDescription *pango_font_description, int cursor_pos = -1);
bool can_paint() { if (m_cairo) return true; else return false; }
void source_rgb(RGB source_rgb);
void fill();
void begin_paint_group();
void end_paint_group();
void flush_paint_group();
};
}

View file

@ -1,26 +0,0 @@
#pragma once
namespace Raven {
class Point {
private:
double m_x;
double m_y;
public:
Point(double x, double y)
: m_x(x)
, m_y(y) {}
double x() { return m_x; }
const double x() const { return m_x; }
double y() { return m_y; }
const double y() const { return m_y; }
void set_x(double x) { m_x = x; }
void set_y(double y) { m_y = y; }
void add(double x, double y) { m_x += x; m_y += y; }
void add(Point const& other) { m_x += other.x(); m_y += other.y(); }
};
}

View file

@ -1,34 +0,0 @@
#pragma once
#include <iostream>
namespace Raven {
class RGB {
private:
double m_r {0.0};
double m_g {0.0};
double m_b {0.0};
public:
RGB() {}
RGB(double r, double g, double b)
: m_r(r)
, m_g(g)
, m_b(b) {}
RGB(unsigned int hex) {
m_r = (((hex) >> (2 * 8)) & 0xFF) / 255.0;
m_g = (((hex) >> (1 * 8)) & 0xFF) / 255.0;
m_b = (((hex) >> (0 * 8)) & 0xFF) / 255.0;
}
void set_r(double r) { m_r = r; }
void set_g(double g) { m_g = g; }
void set_b(double b) { m_b = b; }
double r() { return m_r; }
double g() { return m_g; }
double b() { return m_b; }
};
}

View file

@ -1,36 +0,0 @@
#include "ScrollContainer.hpp"
#include "Styles.hpp"
#include <iostream>
#include <memory>
namespace Raven {
void ScrollContainer::on_layout() {
if (!m_target)
return;
m_target->rect().set_x(-m_scroll.x());
m_target->rect().set_y(-m_scroll.y());
// todo: doesn't work with horizontal scrolling
m_target->rect().set_min_width(rect().width());
m_target->rect().set_max_width(rect().width());
}
std::shared_ptr<Widget> ScrollContainer::make_target() {
m_target = add<Raven::Widget>();
m_target->set_style(style());
m_target->rect().update();
return m_target;
}
void ScrollContainer::on_mouse_button(MouseButtonEvent &event) {
if (event.did_scroll_down()) {
set_scroll(Point(m_scroll.x(), std::min(m_scroll.y() + m_scroll_step, m_target->rect().height())));
} else if (event.did_scroll_up()) {
set_scroll(Point(m_scroll.x(), std::max(0.0, m_scroll.y() - m_scroll_step)));
}
}
}

View file

@ -1,33 +0,0 @@
#pragma once
#include "Widget.hpp"
#include "Point.hpp"
#include <memory>
namespace Raven {
class ScrollContainer : public Widget {
public:
ScrollContainer()
: Widget() {}
void set_scroll(Point scroll) { m_scroll = scroll; reflow(); }
Point &scroll() { return m_scroll; }
std::shared_ptr<Widget> make_target();
void set_target(std::shared_ptr<Widget> target) { m_target = target; }
std::shared_ptr<Widget> target() { return m_target; }
void set_scroll_step(double scroll_step) { m_scroll_step = scroll_step; }
double scroll_step() { return m_scroll_step; }
protected:
void on_layout();
void on_mouse_button(MouseButtonEvent &event);
private:
double m_scroll_step { 25.0 };
Point m_scroll { 0, 0 };
std::shared_ptr<Widget> m_target { nullptr };
};
}

View file

@ -1,149 +0,0 @@
#include "Styles.hpp"
#include "pango/pango-font.h"
#include "RGB.hpp"
namespace Raven {
RGB unused = RGB(0);
RGB white4 = RGB(0xffffff);
RGB white3 = RGB(0xfafafa);
RGB white2 = RGB(0xebebeb);
RGB white1 = RGB(0xebebeb - 0x141414);
RGB white0 = RGB(0xebebeb - 0x212121);
RGB black0 = RGB(0x000000);
RGB black1 = RGB(0x030303);
RGB black2 = RGB(0x080808);
RGB black3 = RGB(0x151515);
RGB black4 = RGB(0x1b1b1b);
RGB black5 = RGB(0x232323);
RGB black6 = RGB(0x2d2d2d);
RGB accent0 = RGB(0x7f465f);
RGB accent1 = RGB(0x995473);
RGB accent2 = RGB(0xb16286);
GenericStyle flat_widget_style {
pango_font_description_from_string("sans-serif"),
black0,
white3,
white3,
white3,
0.0,
true,
false
};
GenericStyle raised_widget_style {
pango_font_description_from_string("sans-serif"),
black0,
white2,
white2,
white2,
6.0,
true,
false
};
GenericStyle clear_widget_style {
pango_font_description_from_string("sans-serif"),
black0,
white2,
white2,
white2,
0.0,
false,
false
};
GenericStyle flat_button_style {
pango_font_description_from_string("sans-serif"),
black0,
white3,
white2,
white1,
6.0,
true,
true
};
GenericStyle raised_button_style {
pango_font_description_from_string("sans-serif"),
black0,
white2,
white1,
white0,
6.0,
true,
true
};
GenericStyle accent_button_style {
pango_font_description_from_string("sans-serif"),
black0,
accent2,
accent1,
accent0,
6.0,
true,
true
};
GenericStyle flat_label_style {
pango_font_description_from_string("sans-serif"),
black0,
unused,
unused,
unused,
0.0,
false,
false
};
GenericStyle raised_listview_style {
pango_font_description_from_string("sans-serif"),
black0,
white2,
white0,
accent2,
6.0,
true,
false
};
GenericStyle flat_listview_style {
pango_font_description_from_string("sans-serif"),
black0,
white2,
white1,
accent2,
6.0,
true,
false
};
GenericStyle flat_textinput_style {
pango_font_description_from_string("sans-serif"),
black0,
white2,
white2,
white1,
6.0,
true,
true
};
GenericStyle raised_textinput_style {
pango_font_description_from_string("sans-serif"),
black0,
white1,
white1,
white0,
6.0,
true,
true
};
}

View file

@ -1,39 +0,0 @@
#pragma once
#include "GenericStyle.hpp"
namespace Raven {
extern RGB unused;
extern RGB white4;
extern RGB white3;
extern RGB white2;
extern RGB white1;
extern RGB white0;
extern RGB black0;
extern RGB black1;
extern RGB black2;
extern RGB black3;
extern RGB black4;
extern RGB black5;
extern RGB black6;
extern RGB accent0;
extern RGB accent1;
extern RGB accent2;
extern GenericStyle flat_widget_style;
extern GenericStyle raised_widget_style;
extern GenericStyle clear_widget_style;
extern GenericStyle flat_button_style;
extern GenericStyle raised_button_style;
extern GenericStyle accent_button_style;
extern GenericStyle flat_label_style;
extern GenericStyle flat_listview_style;
extern GenericStyle raised_listview_style;
extern GenericStyle flat_textinput_style;
extern GenericStyle raised_textinput_style;
}

View file

@ -1,35 +0,0 @@
#include "SvgUtil.hpp"
#include "librsvg/rsvg.h"
#include <iostream>
#include "cairomm/context.h"
namespace Raven {
Cairo::RefPtr<Cairo::ImageSurface> render_svg(std::string path, double x, double y, double width, double height) {
auto file = g_file_new_for_path(path.c_str());
auto handle = rsvg_handle_new_from_gfile_sync(file, RSVG_HANDLE_FLAGS_NONE, NULL, NULL);
if (!handle) {
std::cerr << "could not load svg file: " << path << std::endl;
exit(EXIT_FAILURE);
}
RsvgRectangle viewport = {
.x = x,
.y = y,
.width = width,
.height = height,
};
auto surface = Cairo::ImageSurface::create(Cairo::Format::FORMAT_ARGB32, width, height);
auto context = Cairo::Context::create(surface);
rsvg_handle_render_document(handle, context->cobj(), &viewport, NULL);
g_object_unref(file);
g_object_unref(handle);
return surface;
}
}

View file

@ -1,10 +0,0 @@
#pragma once
#include "cairomm/refptr.h"
#include "cairomm/surface.h"
namespace Raven {
Cairo::RefPtr<Cairo::ImageSurface> render_svg(std::string path, double x, double y, double width, double height);
}

View file

@ -1,70 +0,0 @@
#include "SvgWidget.hpp"
#include "cairomm/context.h"
#include "cairomm/enums.h"
#include "cairomm/surface.h"
namespace Raven {
void SvgWidget::on_init() {
set_style(&clear_widget_style);
if (m_user_defined_surface) {
set_did_init(true);
return;
}
m_file = g_file_new_for_path(m_path.c_str());
m_handle = rsvg_handle_new_from_gfile_sync(m_file, RSVG_HANDLE_FLAGS_NONE, NULL, NULL);
if (!m_handle) {
std::cerr << "could not load svg file: " << m_path << std::endl;
exit(EXIT_FAILURE);
}
set_did_init(true);
}
void SvgWidget::on_after_layout() {
if (m_user_defined_surface) {
return;
}
auto width = rect().width();
auto height = rect().height();
if (width == m_known_width && height == m_known_height) {
return;
}
m_known_width = width;
m_known_height = height;
RsvgRectangle viewport = {
.x = 0.0,
.y = 0.0,
.width = width,
.height = height,
};
m_image_surface = Cairo::ImageSurface::create(Cairo::Format::FORMAT_ARGB32, width, height);
m_image_context = Cairo::Context::create(m_image_surface);
rsvg_handle_render_document(m_handle, m_image_context->cobj(), &viewport, NULL);
}
void SvgWidget::on_paint() {
if (!m_image_surface)
return;
painter()->cairo()->set_source(m_image_surface, 0, 0);
painter()->cairo()->paint();
}
SvgWidget::~SvgWidget() {
if (m_file)
g_object_unref(m_file);
if (m_handle)
g_object_unref(m_handle);
}
}

View file

@ -1,43 +0,0 @@
#pragma once
#include <string>
#include "cairomm/context.h"
#include "cairomm/enums.h"
#include "cairomm/refptr.h"
#include "cairomm/surface.h"
#include "Widget.hpp"
#include "librsvg/rsvg.h"
namespace Raven {
class SvgWidget : public Raven::Widget {
public:
SvgWidget(std::string path)
: Raven::Widget()
, m_path(path) {}
SvgWidget(Cairo::RefPtr<Cairo::ImageSurface> image_surface)
: Raven::Widget()
, m_image_surface(image_surface)
, m_user_defined_surface(true) {}
~SvgWidget();
std::string &path() { return m_path; }
void set_path(std::string path) { m_path = path; repaint(); }
protected:
void on_init();
void on_after_layout();
void on_paint();
private:
std::string m_path;
RsvgHandle *m_handle { nullptr };
GFile *m_file { nullptr };
Cairo::RefPtr<Cairo::Context> m_image_context { nullptr };
Cairo::RefPtr<Cairo::ImageSurface> m_image_surface { nullptr };
double m_known_width { 0.0 };
double m_known_height { 0.0 };
bool m_user_defined_surface { false };
};
}

View file

@ -1,132 +0,0 @@
#include "TextInput.hpp"
#include "Events.hpp"
#include "Logging.hpp"
#include "Painter.hpp"
#include "Styles.hpp"
#include <cctype>
#include <string>
#include <locale>
namespace Raven {
void TextInput::set_text(std::string text) {
m_text = text;
m_cursor = m_text.length();
// We won't use the fit_text() function here, since reflowing is a little slower than repainting.
// It's better for text input to feel snappier, thus we will make the assumption that all text input
// widgets will not scale themselves to fit the text inside. We will rely on the parent layout or
// the user of the library explicitly sizing this widget.
repaint();
}
void TextInput::on_init() {
set_remains_active(true);
set_style_pure(&flat_textinput_style);
set_did_init(true);
if (!fit_text(m_text)) {
reflow();
}
m_cursor = m_text.length();
}
void TextInput::on_paint() {
auto painter = window()->painter();
auto geometry = rect().max_geometry();
int displayed_cursor_pos = m_cursor;
if (!is_active()) {
displayed_cursor_pos = -1;
}
painter.source_rgb(style()->foreground());
painter.text(geometry, m_text, Raven::PaintTextAlign::Left, PANGO_ELLIPSIZE_END, style()->font_description(), displayed_cursor_pos);
painter.fill();
}
void TextInput::text_did_change() {
repaint();
}
void TextInput::insert(std::string chars) {
m_text.insert(m_cursor, chars);
m_cursor++;
text_did_change();
}
void TextInput::on_key(KeyEvent &event) {
INFO << "on_key: " << event.key_chars() << std::endl;
switch (event.status()) {
case KeyEvent::KeyStatus::HasKeyChars: {
insert(event.key_chars());
break;
}
case KeyEvent::KeyStatus::HasBoth: /* fallthrough */
case KeyEvent::KeyStatus::HasKeySym: {
auto key_sym = event.key_sym();
if (event.control_pressed()) {
switch (key_sym) {
case XK_a: key_sym = XK_Home; break;
case XK_e: key_sym = XK_End; break;
case XK_f: key_sym = XK_Right; break;
case XK_b: key_sym = XK_Left; break;
default: {
return;
}
}
}
switch (key_sym) {
default: {
// FIXME
if (!iscntrl((unsigned char)*event.key_chars().c_str())) {
insert(event.key_chars());
}
break;
}
case XK_BackSpace: {
if (!m_cursor) return;
m_cursor--;
m_text.erase(m_cursor, 1);
text_did_change();
break;
}
case XK_KP_Left:
case XK_Left: {
m_cursor = std::max(m_cursor - 1, 0);
text_did_change();
break;
}
case XK_KP_Right:
case XK_Right: {
m_cursor = std::min(m_cursor + 1, static_cast<int>(m_text.length()));
text_did_change();
break;
}
case XK_Home:
case XK_KP_Home: {
m_cursor = 0;
text_did_change();
break;
}
case XK_End:
case XK_KP_End: {
m_cursor = m_text.length();
text_did_change();
break;
}
}
}
case KeyEvent::KeyStatus::HasNone:
default: {
return;
}
}
}
}

View file

@ -1,26 +0,0 @@
#pragma once
#include "Widget.hpp"
namespace Raven {
class TextInput : public Raven::Widget {
public:
TextInput()
: Raven::Widget() {}
std::string &text() { return m_text; }
void set_text(std::string text);
protected:
void on_init() override;
void on_paint() override;
void on_key(KeyEvent &event) override;
private:
void insert(std::string chars);
void text_did_change();
std::string m_text {""};
int m_cursor { 0 };
};
}

View file

@ -1,329 +0,0 @@
#include <algorithm>
#include <iostream>
#include <memory>
#include <exception>
#include "Widget.hpp"
#include "Events.hpp"
#include "Window.hpp"
#include "Layout.hpp"
namespace Raven {
Painter *Widget::painter() {
if (!window()) {
return nullptr;
}
return &window()->painter();
}
Point Widget::compute_window_relative() {
Point point = { 0, 0 };
for (Widget* parent = m_parent; parent; parent = parent->parent()) {
point.add(parent->rect().x(), parent->rect().y());
}
return point;
}
bool Widget::fit_text(std::string &text, double padding) {
if (!window())
return false;
auto size = window()->painter().compute_text_size(rect(), text, style()->font_description());
size.add(padding * 2, padding * 2);
return resize(size);
}
bool Widget::resize(double width, double height) {
if (m_rect.width() == width && m_rect.height() == height)
return false;
if (width < 0 || height < 0)
return false;
m_rect.set_width(width);
m_rect.set_height(height);
reflow();
return true;
}
bool Widget::resize_fixed(double width, double height) {
if (m_rect.width() == width && m_rect.height() == height)
return false;
if (width < 0 || height < 0)
return false;
m_rect.set_max_width(width);
m_rect.set_max_height(height);
m_rect.set_min_width(width);
m_rect.set_min_height(height);
m_rect.set_width(width);
m_rect.set_height(height);
reflow();
return true;
}
void Widget::move_to(double x, double y) {
if (m_rect.x() == x && m_rect.y() == y)
return;
m_rect.set_x(x);
m_rect.set_y(y);
reflow();
}
void Widget::set_layout(std::shared_ptr<Layout> layout) {
m_layout = layout;
m_layout->bind_to(this);
}
void Widget::set_window(Window *window) {
if (!window)
return;
m_window = window;
on_init();
}
bool Widget::add_child(std::shared_ptr<Widget> child) {
if (child->parent()) {
return false;
}
if (window())
window()->start_batch();
m_children.push_back(child);
child->set_parent(this);
// children inherit the window from the parent
// NOTE: we're no longer calling reflow() in this function, as we'll rely on the child widget to reflow
// after we call set_window() on it.
child->set_window(m_window);
if (window())
window()->end_batch();
return true;
}
void Widget::remove_child(std::shared_ptr<Widget> child) {
m_children.erase(std::remove(m_children.begin(), m_children.end(), child), m_children.end());
reflow();
}
void Widget::clear_children() {
m_children.clear();
m_children.shrink_to_fit();
reflow();
}
void Widget::repaint() {
if (m_window)
m_window->repaint(this);
}
void Widget::reflow() {
if (m_window)
m_window->reflow();
}
void Widget::handle_repaint_rect(RepaintRectEvent &event) {
auto painter = m_window->painter();
if (!m_did_init || !painter.can_paint())
return;
if (!event.box().contains(m_rect))
return; // widgets contain their children, thus we don't need to recurse further
// using a "group" in cairo reduces flickering
bool should_end_paint_group = false;
if (event.grouping()) {
painter.begin_paint_group();
should_end_paint_group = true;
}
auto cr = painter.cairo();
cr->save();
// clip to the event's invalidation rectangle. this is because widgets tend to also draw things like text,
// which sometimes goes outside the invalidation rectangle
painter.rounded_rectangle(event.box(), 0);
cr->clip();
// clip this widget. this ensures that the background fill or children won't overflow the bounds of it
painter.rounded_rectangle(m_rect, m_style->border_radius());
cr->clip();
// paint the background fill
// note that we're using the bounds of the paint event as the rectangle, this ensures that we dont draw over other widgets which won't be repainted.
if (m_style->fill_background()) {
if (m_style->update_background()) {
if (m_is_active) {
painter.source_rgb(m_style->background_active());
} else if (m_is_focused) {
painter.source_rgb(m_style->background_focused());
} else {
painter.source_rgb(m_style->background_norm());
}
} else {
painter.source_rgb(m_style->background_norm());
}
painter.rounded_rectangle(event.box(), 0.0);
cr->fill();
}
// translate to the widget's position
// the origin of the painter is now the widget's origin
// this is because the position of children is relative to the origin of their parent
cr->translate(rect().x(), rect().y());
on_paint();
// convert the paint event's origin to our coordinate space because all positions of children are relative to the origin of their parent
auto local_box = event.box().offset(-m_rect.x(), -m_rect.y());
auto local_event = RepaintRectEvent(false, local_box);
for (auto child : m_children) {
child->dispatch_event(local_event);
}
cr->restore();
if (should_end_paint_group) {
painter.end_paint_group();
}
}
void Widget::handle_relayout_subtree(RelayoutSubtreeEvent &event) {
on_layout();
m_window_relative = compute_window_relative();
if (!m_layout || !m_layout->run()) {
for (auto child : m_children) {
child->dispatch_event(event);
}
}
on_after_layout();
}
void Widget::handle_mouse_move_event(MouseMoveEvent &event) {
bool update_focus_to = true;
if (!m_rect.contains(event.point())) {
// we just became unfocused
update_focus_to = false;
}
on_mouse_move(event);
if (m_is_focused != update_focus_to) {
m_is_focused = update_focus_to;
if (m_is_focused && m_window)
m_window->set_focused_widget(this);
auto focus_update_event = FocusUpdateEvent(update_focus_to);
on_focus_update(focus_update_event);
if (m_style->update_background()) {
repaint();
}
}
if (!m_consumes_hits) {
// translate the event's point to our coordinate space because the position of all children is relative to the origin of their parent
auto local_event = MouseMoveEvent(Point(
event.point().x() - m_rect.x(),
event.point().y() - m_rect.y()
));
for (auto child : m_children) {
child->dispatch_event(local_event);
}
}
}
void Widget::handle_mouse_button_event(MouseButtonEvent &event) {
bool update_activation_to = event.was_left_button_pressed();
if (!update_activation_to && m_remains_active) {
update_activation_to = true;
}
if (m_rect.contains(event.point())) {
on_mouse_button(event);
} else {
update_activation_to = false;
}
if (m_is_active != update_activation_to || (update_activation_to && m_window->active_widget() != this)) {
m_is_active = update_activation_to;
if (m_is_active && m_window)
m_window->set_active_widget(this);
auto activation_update_event = ActivationUpdateEvent(update_activation_to);
on_activation_update(activation_update_event);
if (update_activation_to == false) {
on_click();
}
if (m_style->update_background()) {
repaint();
}
}
if (!m_consumes_hits) {
// translate the event's point to our coordinate space because the position of all children is relative to the origin of their parent
auto local_event = MouseButtonEvent(event.was_left_button_pressed(), event.was_right_button_pressed(), event.did_scroll_up(), event.did_scroll_down(), Point(
event.point().x() - m_rect.x(),
event.point().y() - m_rect.y()
));
for (auto child : m_children) {
child->dispatch_event(local_event);
}
}
}
void Widget::handle_key_event(KeyEvent &event) {
on_key(event);
}
void Widget::dispatch_event(Event &event) {
if (!m_accepts_events || !m_did_init)
return;
on_event(event);
switch (event.type()) {
case EventType::MouseMove: {
handle_mouse_move_event(reinterpret_cast<MouseMoveEvent&>(event));
break;
}
case EventType::MouseButton: {
handle_mouse_button_event(reinterpret_cast<MouseButtonEvent&>(event));
break;
}
case EventType::RepaintRect: {
handle_repaint_rect(reinterpret_cast<RepaintRectEvent&>(event));
break;
}
case EventType::RelayoutSubtree: {
handle_relayout_subtree(reinterpret_cast<RelayoutSubtreeEvent&>(event));
break;
}
case EventType::Key: {
handle_key_event(reinterpret_cast<KeyEvent&>(event));
break;
}
/* these events aren't handled here, as they won't be dispatched to us from other places */
case EventType::FocusUpdate:
case EventType::ActivationUpdate:
case EventType::NoneEvent: {
break;
}
}
}
}

View file

@ -1,162 +0,0 @@
#pragma once
#include <memory>
#include <utility>
#include <vector>
#include <string>
#include <functional>
#include "Box.hpp"
#include "Events.hpp"
#include "Forward.hpp"
#include "RGB.hpp"
#include "GenericStyle.hpp"
#include "Styles.hpp"
#include "Window.hpp"
namespace Raven {
enum class WidgetType {
Widget = 0,
Button,
Label,
Control
};
enum class ControlWidgetType {
Widget = 0,
NewRow
};
class Widget {
public:
Widget() {}
Widget(WidgetType type)
: m_type(type) {}
Widget(ControlWidgetType type)
: m_control_type(type) {
m_type = WidgetType::Control;
m_control_type = type;
m_accepts_events = false;
}
virtual ~Widget() {};
std::function<void(Event&)> on_event { [](Event&){} };
std::function<void()> on_click { [](){} };
bool fit_text(std::string &text, double padding = 0);
void move_to(double x, double y);
bool resize(double width, double height);
bool resize_fixed(double width, double height);
bool resize(Point &point) { return resize(point.x(), point.y()); }
std::vector<std::shared_ptr<Widget>> &children() { return m_children; }
bool add_child(std::shared_ptr<Widget> child);
void remove_child(std::shared_ptr<Widget> child);
void clear_children();
Box &rect() { return m_rect; }
void set_rect(Box rect) { m_rect = rect; reflow(); }
void set_rect_pure(Box rect) { m_rect = rect; }
Point &window_relative() { return m_window_relative; };
WidgetType type() { return m_type; }
ControlWidgetType control_type() { return m_control_type; }
Widget *parent() { return m_parent; }
void set_parent(Widget *parent) { m_parent = parent; }
Window *window() { return m_window; }
void set_window(Window *window);
Painter *painter();
GenericStyle *style() { return m_style; }
void set_style(GenericStyle *style) { m_style = style; reflow(); }
void set_style_pure(GenericStyle *style) { m_style = style; }
bool did_init() { return m_did_init; }
bool is_focused() { return m_is_focused; }
bool is_active() { return m_is_active; }
bool consumes_hits() { return m_consumes_hits; }
void set_consumes_hits(bool consumes_hits) { m_consumes_hits = consumes_hits; }
bool accepts_events() { return m_accepts_events; }
void set_accepts_events(bool accepts_events) { m_accepts_events = accepts_events; }
bool remains_active() { return m_remains_active; }
void set_remains_active(bool remains_active) { m_remains_active = remains_active; }
bool absolute() { return m_absolute; }
void set_absolute(bool absolute) { m_absolute = absolute; }
bool grows() { return m_grows; }
void set_grows(bool grows) { m_grows = grows; }
void set_layout(std::shared_ptr<Layout> layout);
std::shared_ptr<Layout> layout() { return m_layout; }
void dispatch_event(Event &event);
template<typename T, class... Args>
std::shared_ptr<T> set_layout(Args&&... args) {
std::shared_ptr<T> layout = std::make_shared<T>(std::forward<Args>(args)...);
set_layout(layout);
return layout;
}
template<typename T, class... Args>
std::shared_ptr<T> add(Args&&... args) {
std::shared_ptr<T> child = std::make_shared<T>(std::forward<Args>(args)...);
add_child(child);
return child;
}
protected:
WidgetType m_type { WidgetType::Widget };
virtual void on_init() { set_did_init(true); reflow(); }
virtual void on_mouse_button(MouseButtonEvent &event) {}
virtual void on_mouse_move(MouseMoveEvent &event) {}
virtual void on_focus_update(FocusUpdateEvent &event) {}
virtual void on_activation_update(ActivationUpdateEvent &event) {}
virtual void on_paint() {}
virtual void on_layout() {}
virtual void on_after_layout() {}
virtual void on_key(KeyEvent &event) {}
void set_did_init(bool did_init) { m_did_init = did_init; }
Point compute_window_relative();
void repaint();
void reflow();
private:
void handle_repaint_rect(RepaintRectEvent &event);
void handle_relayout_subtree(RelayoutSubtreeEvent &event);
void handle_mouse_move_event(MouseMoveEvent &event);
void handle_mouse_button_event(MouseButtonEvent &event);
void handle_key_event(KeyEvent &event);
Point m_window_relative { 0, 0 };
Box m_rect { 0, 0, 0, 0 };
std::vector<std::shared_ptr<Widget>> m_children;
Widget *m_parent { nullptr };
Window *m_window { nullptr };
GenericStyle *m_style { &flat_widget_style };
std::shared_ptr<Layout> m_layout { nullptr };
bool m_did_init { false };
bool m_is_focused { false };
bool m_is_active { false };
bool m_consumes_hits { false };
bool m_accepts_events { true };
bool m_absolute { false };
bool m_grows { false };
bool m_remains_active { false };
ControlWidgetType m_control_type { ControlWidgetType::Widget };
};
}

View file

@ -1,268 +0,0 @@
#include <iostream>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <cairomm/cairomm.h>
#include <cairomm/context.h>
#include <cairomm/surface.h>
#include <cairomm/xlib_surface.h>
#include "Window.hpp"
#include "Events.hpp"
#include "Point.hpp"
#include "Logging.hpp"
namespace Raven {
void Window::set_main_widget(std::shared_ptr<Widget> main_widget) {
m_main_widget = main_widget;
m_main_widget->set_window(this);
m_main_widget->set_rect(m_rect);
}
bool Window::spawn_window() {
Display *dsp = XOpenDisplay(NULL);
if (dsp == NULL) {
std::cerr << "error: XOpenDisplay failed" << "\n";
return false;
}
m_x_display = dsp;
int screen = DefaultScreen(dsp);
Drawable da = XCreateWindow(
dsp,
DefaultRootWindow(dsp),
0,
0,
m_rect.width(),
m_rect.height(),
0,
0,
CopyFromParent,
CopyFromParent,
0,
NULL
);
XSelectInput(dsp, da, ButtonPressMask | ButtonReleaseMask | KeyPressMask | PointerMotionMask | StructureNotifyMask | ExposureMask);
if ((m_xim = XOpenIM(dsp, NULL, NULL, NULL)) == NULL) {
std::cerr << "error: XOpenIM failed" << "\n";
return false;
}
m_xic = XCreateIC(m_xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, da, XNFocusWindow, da, NULL);
XMapWindow(dsp, da);
m_xlib_surface = Cairo::XlibSurface::create(
dsp,
da,
DefaultVisual(dsp, screen),
rect().width(),
rect().height()
);
auto cairo_context = Cairo::Context::create(m_xlib_surface);
m_painter.set_cairo(cairo_context);
return true;
}
int Window::file_descriptor() {
return m_x_display ? XConnectionNumber(m_x_display) : -1;
}
bool Window::dispatch_to_main_widget(Event &event) {
if (!m_main_widget)
return false;
// MouseMove events tend to spam the output
if (event.type() != EventType::MouseMove) {
INFO << "Dispatched event '" << event.name() << "' to main widget" << std::endl;
}
m_main_widget->dispatch_event(event);
return true;
}
void Window::repaint(Box geometry) {
if (m_batches) {
m_invalidated_area = m_invalidated_area.united(geometry);
return;
}
auto event = RepaintRectEvent(true, geometry);
dispatch_to_main_widget(event);
}
void Window::repaint(Widget *target) {
repaint(target->rect().offset(target->window_relative().x(), target->window_relative().y()));
}
void Window::repaint() {
repaint(m_rect);
}
void Window::relayout(Widget *target) {
m_did_relayout_during_batch = true;
if (m_batches)
return;
auto event = RelayoutSubtreeEvent();
target->dispatch_event(event);
}
void Window::relayout() {
m_did_relayout_during_batch = true;
if (m_batches)
return;
auto event = RelayoutSubtreeEvent();
dispatch_to_main_widget(event);
}
void Window::reflow() {
relayout();
repaint();
}
void Window::start_batch() {
m_batches++;
}
void Window::end_batch() {
if (m_batches < 1)
return;
// we are only interested in performing batch operations for the last batch
if (m_batches > 1) {
m_batches--;
return;
}
m_batches = 0;
if (m_did_relayout_during_batch) {
reflow();
} else if (!m_invalidated_area.is_null()) {
repaint(m_invalidated_area);
}
m_did_relayout_during_batch = false;
m_invalidated_area = {0, 0, 0, 0};
}
void Window::swallow_batches() {
m_batches = 0;
m_did_relayout_during_batch = false;
m_invalidated_area = {0, 0, 0, 0};
}
void Window::run(bool block) {
XEvent e;
swallow_batches();
for (;;) {
if (block || XPending(m_x_display))
XNextEvent(m_x_display, &e);
else
break;
if (!m_is_loop_batch_started) {
start_batch();
m_is_loop_batch_started = true;
}
switch (e.type) {
case MapNotify: {
INFO << "[X11 EVENT] MapNotify" << std::endl;
break;
}
case ConfigureNotify: {
INFO << "[X11 EVENT] ConfigureNotify" << std::endl;
if (e.xconfigure.width != m_rect.width() || e.xconfigure.height != m_rect.height()) {
m_xlib_surface->set_size(e.xconfigure.width, e.xconfigure.height);
m_rect.set_width(e.xconfigure.width);
m_rect.set_height(e.xconfigure.height);
// if we have a main widget, we are going to have to resize it as well
if (m_main_widget) {
m_main_widget->rect().set_max_height(m_rect.height());
m_main_widget->rect().set_max_width(m_rect.width());
m_main_widget->rect().set_min_height(m_rect.height());
m_main_widget->rect().set_min_width(m_rect.width());
m_main_widget->rect().set_width(m_rect.width());
m_main_widget->rect().set_height(m_rect.height());
}
}
break;
}
case Expose: {
INFO << "[X11 EVENT] Expose" << std::endl;
if (e.xexpose.count == 0) {
reflow();
}
break;
}
case ButtonRelease: {
INFO << "[X11 EVENT] ButtonRelease" << std::endl;
auto point = Point(e.xbutton.x, e.xbutton.y);
auto event = MouseButtonEvent(false, false, false, false, point);
dispatch_to_main_widget(event);
break;
}
case ButtonPress: {
INFO << "[X11 EVENT] ButtonPress" << std::endl;
auto point = Point(e.xbutton.x, e.xbutton.y);
auto event = MouseButtonEvent(e.xbutton.button == Button1, e.xbutton.button == Button2, e.xbutton.button == Button4, e.xbutton.button == Button5, point);
dispatch_to_main_widget(event);
break;
}
case MotionNotify: {
// spammy
//INFO << "[X11 EVENT] MotionNotify" << std::endl;
auto point = Point(e.xmotion.x, e.xmotion.y);
auto event = MouseMoveEvent(point);
dispatch_to_main_widget(event);
break;
}
case KeyPress: {
if (m_active_widget) {
KeySym keysym;
char chars[32];
Status status;
XmbLookupString(m_xic, &e.xkey, chars, sizeof(chars), &keysym, &status);
Raven::KeyEvent::KeyStatus key_status = Raven::KeyEvent::KeyStatus::HasNone;
switch (status) {
case XLookupChars: key_status = Raven::KeyEvent::KeyStatus::HasKeyChars; break;
case XLookupKeySym: key_status = Raven::KeyEvent::KeyStatus::HasKeySym; break;
case XLookupBoth: key_status = Raven::KeyEvent::KeyStatus::HasBoth; break;
default: key_status = Raven::KeyEvent::KeyStatus::HasNone; break;
}
auto event = KeyEvent(keysym, chars, key_status, e.xkey.state & ControlMask, e.xkey.state & ShiftMask);
m_active_widget->dispatch_event(event);
}
break;
}
default: {
break;
}
}
if (XPending(m_x_display) == 0) {
while (!m_microtasks.empty()) {
auto callback = m_microtasks.front();
callback();
m_microtasks.pop();
}
if (m_is_loop_batch_started) {
end_batch();
m_is_loop_batch_started = false;
}
}
}
}
}

View file

@ -1,78 +0,0 @@
#pragma once
#include <memory>
#include <stack>
#include "Widget.hpp"
#include "Painter.hpp"
#include "Events.hpp"
#include <X11/Xlib.h>
#include <cairomm/xlib_surface.h>
#include <functional>
#include <queue>
namespace Raven {
class Window {
public:
Window() {}
bool spawn_window();
void run(bool block);
int file_descriptor();
void start_batch();
void end_batch();
void swallow_batches();
Painter &painter() { return m_painter; }
Widget *focused_widget() { return m_focused_widget; }
void set_focused_widget(Widget *focused_widget) { m_focused_widget = focused_widget; }
Widget *active_widget() { return m_active_widget; }
void set_active_widget(Widget *active_widget) { m_active_widget = active_widget; }
std::shared_ptr<Widget> main_widget() { return m_main_widget; }
void set_main_widget(std::shared_ptr<Widget> main_widget);
void repaint();
void repaint(Box geometry);
void repaint(Widget *target);
void relayout(Widget *target);
void relayout();
void reflow();
void queue_microtask(std::function<void()> callback) { m_microtasks.push(callback); }
Box &rect() { return m_rect; }
template<typename T, class... Args>
std::shared_ptr<T> set_main_widget(Args&&... args) {
std::shared_ptr<T> widget = std::make_shared<T>(std::forward<Args>(args)...);
set_main_widget(widget);
return widget;
}
private:
Widget *m_focused_widget { nullptr };
Widget *m_active_widget { nullptr };
std::shared_ptr<Widget> m_main_widget { nullptr };
Box m_rect { 0, 0, 800, 600 };
Painter m_painter {};
Cairo::RefPtr<Cairo::XlibSurface> m_xlib_surface { nullptr };
int m_batches { 1 };
Box m_invalidated_area {0, 0, 0, 0};
bool m_did_relayout_during_batch { false };
std::queue<std::function<void()>> m_microtasks;
bool m_is_loop_batch_started { false };
Display *m_x_display { nullptr };
XIM m_xim { nullptr };
XIC m_xic { nullptr };
bool dispatch_to_main_widget(Event &event);
};
}

94
src/background-node.c Normal file
View file

@ -0,0 +1,94 @@
#include "background-node.h"
#include "window.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
int background_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{
(void)d;
(void)p;
if (!node->parent) {
return -1;
}
UIBackgroundNode *n = (UIBackgroundNode*)node;
switch (ev) {
case UI_EVENT_REPAINT: {
UIRGBA *color = &n->normal;
if (n->update_on_status) {
if (node->window->pressed == node->parent) {
color = &n->pressed;
} else if (node->window->hovered == node->parent) {
color = &n->hovered;
}
}
cairo_set_source_rgba(node->drw, color->r, color->g, color->b, color->a);
if (n->border_radius) {
double w = node->parent->rect.w;
double h = node->parent->rect.h;
double r = n->border_radius;
double deg = M_PI / 180.0;
cairo_new_sub_path(node->drw);
cairo_arc(node->drw, w - r, r, r, -90 * deg, 0 * deg);
cairo_arc(node->drw, w - r, h - r, r, 0 * deg, 90 * deg);
cairo_arc(node->drw, r, h - r, r, 90 * deg, 180 * deg);
cairo_arc(node->drw, r, r, r, 180 * deg, 270 * deg);
cairo_close_path(node->drw);
} else {
cairo_rectangle(node->drw, 0, 0, node->parent->rect.w, node->parent->rect.h);
}
cairo_fill(node->drw);
break;
}
case UI_EVENT_HOVERED: /* through */
case UI_EVENT_UNHOVERED: /* through */
case UI_EVENT_PRESSED: /* through */
case UI_EVENT_UNPRESSED: {
if (n->update_on_status) {
window_invalidate_node(node->window, node);
}
break;
}
default: {
return 0;
}
}
return 0;
}
UIBackgroundNode *state_background_node_new(UINode *parent, UIRGBA normal, UIRGBA hovered, UIRGBA pressed, double border_radius)
{
UIBackgroundNode *n = malloc(sizeof(UIBackgroundNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "background";
n->node.handle_proto = background_node_handle;
n->normal = normal;
n->hovered = hovered;
n->pressed = pressed;
n->update_on_status = true;
n->border_radius = border_radius;
node_attach(parent, (UINode*)n);
return n;
}
UIBackgroundNode *background_node_new(UINode *parent, UIRGBA normal, double border_radius)
{
UIBackgroundNode *n = malloc(sizeof(UIBackgroundNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "background";
n->node.handle_proto = background_node_handle;
n->normal = normal;
n->hovered = normal;
n->pressed = normal;
n->update_on_status = false;
n->border_radius = border_radius;
node_attach(parent, (UINode*)n);
return n;
}

18
src/background-node.h Normal file
View file

@ -0,0 +1,18 @@
#ifndef _UI__BACKGROUND_NODE_H
#define _UI__BACKGROUND_NODE_H
#include "color.h"
#include "node.h"
typedef struct UIBackgroundNode {
UINode node;
UIRGBA normal, hovered, pressed;
double border_radius;
bool update_on_status;
} UIBackgroundNode;
int background_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
UIBackgroundNode *state_background_node_new(UINode *parent, UIRGBA normal, UIRGBA hovered, UIRGBA pressed, double border_radius);
UIBackgroundNode *background_node_new(UINode *parent, UIRGBA normal, double border_radius);
#endif // _UI__BACKGROUND_NODE_H

163
src/box-layout-node.c Normal file
View file

@ -0,0 +1,163 @@
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "box-layout-node.h"
int box_layout_handle(UINode *component, enum UIEvent ev, size_t d, void *p)
{
(void)d;
(void)p;
UIBoxLayoutNode *box_layout_node = (UIBoxLayoutNode*)component;
enum UIDirection direction = box_layout_node->direction;
bool is_horizontal = direction == UI_DIRECTION_HORIZONTAL;
bool is_vertical = !is_horizontal;
double margin_top = box_layout_node->margin_top;
double margin_bottom = box_layout_node->margin_bottom;
double margin_left = box_layout_node->margin_left;
double margin_right = box_layout_node->margin_right;
double gap = box_layout_node->gap;
if (!component->parent) return 0;
UINode *node = component->parent;
double size = 0;
double maximum_secondary_position = 0;
int growing_widgets = 0;
int current_index = 0;
if (ev == UI_EVENT_RELAYOUT || ev == UI_EVENT_GET_HEIGHT || ev == UI_EVENT_GET_WIDTH) {
for (int i = 0; i < node->nodes_count; i++) {
UINode *current = node->nodes[i];
if (current->flags & UI_NODE_COMPONENT) continue;
double w = current->rect.w;
double h = current->rect.h;
if (current->width_policy == UI_SIZE_POLICY_DYNAMIC) {
node_dispatch(current, UI_EVENT_GET_WIDTH, 0, &w);
current->rect.w = w;
}
if (current->height_policy == UI_SIZE_POLICY_DYNAMIC) {
node_dispatch(current, UI_EVENT_GET_HEIGHT, 0, &h);
current->rect.h = h;
}
if (is_horizontal) {
if (h > maximum_secondary_position) {
maximum_secondary_position = h;
}
if (current->width_policy == UI_SIZE_POLICY_GROW) {
growing_widgets++;
} else {
size += w;
}
} else {
if (w > maximum_secondary_position) {
maximum_secondary_position = w;
}
if (current->height_policy == UI_SIZE_POLICY_GROW) {
growing_widgets++;
} else {
size += h;
}
}
if (current_index) {
size += gap;
}
current_index++;
}
maximum_secondary_position += is_horizontal ? margin_top + margin_bottom : margin_left + margin_right;
size += is_vertical ? margin_top + margin_bottom : margin_left + margin_right;
if (ev == UI_EVENT_GET_WIDTH) {
*(double*)p = is_horizontal ? size : maximum_secondary_position;
return 1;
}
if (ev == UI_EVENT_GET_HEIGHT) {
*(double*)p = is_vertical ? size : maximum_secondary_position;
return 1;
}
// we can distribute growing layout children only if we have a fixed sized policy in the layout primary direction
bool is_fixed_for_primary_direction = (is_horizontal && node->width_policy != UI_SIZE_POLICY_DYNAMIC) || (is_vertical && node->height_policy != UI_SIZE_POLICY_DYNAMIC);
double primary_direction = is_horizontal ? node->rect.w - margin_left - margin_right : node->rect.h - margin_top - margin_bottom;
bool distributes_growing_widgets = growing_widgets && is_fixed_for_primary_direction;
double size_per_growing_widget = 0;
// growing widgets are evenly sized among the remaining free space
if (distributes_growing_widgets) {
double total_size_for_growing_widgets = primary_direction - size;
if (total_size_for_growing_widgets > 0) {
size_per_growing_widget = floor(total_size_for_growing_widgets / growing_widgets);
} else {
distributes_growing_widgets = false;
}
}
bool is_fixed_for_secondary_direction = (is_horizontal && node->height_policy != UI_SIZE_POLICY_DYNAMIC) || (is_vertical && node->width_policy != UI_SIZE_POLICY_DYNAMIC);
double secondary_direction = is_horizontal ? node->rect.h - margin_top - margin_bottom : node->rect.w - margin_left - margin_right;
// if our layout secondary direction is fixed, we set the maximum known secondary position to it
if (is_fixed_for_secondary_direction) {
maximum_secondary_position = secondary_direction;
}
double x = margin_left;
double y = margin_top;
current_index = 0;
for (int i = 0; i < node->nodes_count; i++) {
UINode *current = node->nodes[i];
if (current->flags & UI_NODE_COMPONENT) continue;
if (is_horizontal) {
if (current->width_policy == UI_SIZE_POLICY_GROW) {
current->rect.w = distributes_growing_widgets ? size_per_growing_widget : 0;
}
if (current->height_policy == UI_SIZE_POLICY_GROW) {
current->rect.h = maximum_secondary_position;
}
if (current_index) {
x += gap;
}
current->rect.x = x;
current->rect.y = y;
x += current->rect.w;
} else {
if (current->height_policy == UI_SIZE_POLICY_GROW) {
current->rect.h = distributes_growing_widgets ? size_per_growing_widget : 0;
}
if (current->width_policy == UI_SIZE_POLICY_GROW) {
current->rect.w = maximum_secondary_position;
}
if (current_index) {
y += gap;
}
current->rect.x = x;
current->rect.y = y;
y += current->rect.h;
}
node_dispatch(current, UI_EVENT_RELAYOUT, 0, NULL);
current_index++;
}
return 1;
}
return 0;
}
UIBoxLayoutNode *box_layout_new(UINode *parent, enum UIDirection direction)
{
UIBoxLayoutNode *n = malloc(sizeof(UIBoxLayoutNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "box_layout";
n->node.handle_proto = box_layout_handle;
n->direction = direction;
n->margin_top = 0;
n->margin_left = 0;
n->margin_bottom = 0;
n->margin_right = 0;
n->gap = 0;
node_attach(parent, (UINode*)n);
return n;
}

15
src/box-layout-node.h Normal file
View file

@ -0,0 +1,15 @@
#ifndef _UI__BOX_LAYOUT_NODE_H
#define _UI__BOX_LAYOUT_NODE_H
#include "node.h"
typedef struct UIBoxLayoutNode {
UINode node;
enum UIDirection direction;
double margin_top, margin_left, margin_bottom, margin_right, gap;
} UIBoxLayoutNode;
UIBoxLayoutNode *box_layout_new(UINode *parent, enum UIDirection direction);
int box_layout_handle(UINode *component, enum UIEvent ev, size_t d, void *p);
#endif // _UI__BOX_LAYOUT_NODE_H

11
src/color.h Normal file
View file

@ -0,0 +1,11 @@
#ifndef _UI__COLOR_H
#define _UI__COLOR_H
typedef struct UIRGBA {
double r, g, b, a;
} UIRGBA;
#define UI_HEX_TO_COLOR_NORMAL(hex) ((((hex) & 0xFF0000) >> 16) / 255.0), ((((hex) & 0xFF00) >> 8) / 255.0), (((hex) & 0xFF) / 255.0)
#define UI_HEX_TO_RGBA(hex) (UIRGBA){UI_HEX_TO_COLOR_NORMAL(hex), 1.0}
#endif // _UI__COLOR_H

274
src/colors.h Normal file
View file

@ -0,0 +1,274 @@
#ifndef _UI__COLORS_H
#define _UI__COLORS_H
#include "color.h"
#define colordef(hex) {UI_HEX_TO_COLOR_NORMAL(hex), 1.0}
// Source: https://tailwindcss.com/docs/customizing-colors
const UIRGBA UISlate50 = colordef(0xf8fafc);
const UIRGBA UISlate100 = colordef(0xf1f5f9);
const UIRGBA UISlate200 = colordef(0xe2e8f0);
const UIRGBA UISlate300 = colordef(0xcbd5e1);
const UIRGBA UISlate400 = colordef(0x94a3b8);
const UIRGBA UISlate500 = colordef(0x64748b);
const UIRGBA UISlate600 = colordef(0x475569);
const UIRGBA UISlate700 = colordef(0x334155);
const UIRGBA UISlate800 = colordef(0x1e293b);
const UIRGBA UISlate900 = colordef(0x0f172a);
const UIRGBA UISlate950 = colordef(0x020617);
const UIRGBA UIGray50 = colordef(0xf9fafb);
const UIRGBA UIGray100 = colordef(0xf3f4f6);
const UIRGBA UIGray200 = colordef(0xe5e7eb);
const UIRGBA UIGray300 = colordef(0xd1d5db);
const UIRGBA UIGray400 = colordef(0x9ca3af);
const UIRGBA UIGray500 = colordef(0x6b7280);
const UIRGBA UIGray600 = colordef(0x4b5563);
const UIRGBA UIGray700 = colordef(0x374151);
const UIRGBA UIGray800 = colordef(0x1f2937);
const UIRGBA UIGray900 = colordef(0x111827);
const UIRGBA UIGray950 = colordef(0x030712);
const UIRGBA UIZinc50 = colordef(0xfafafa);
const UIRGBA UIZinc100 = colordef(0xf4f4f5);
const UIRGBA UIZinc200 = colordef(0xe4e4e7);
const UIRGBA UIZinc300 = colordef(0xd4d4d8);
const UIRGBA UIZinc400 = colordef(0xa1a1aa);
const UIRGBA UIZinc500 = colordef(0x71717a);
const UIRGBA UIZinc600 = colordef(0x52525b);
const UIRGBA UIZinc700 = colordef(0x3f3f46);
const UIRGBA UIZinc800 = colordef(0x27272a);
const UIRGBA UIZinc900 = colordef(0x18181b);
const UIRGBA UIZinc950 = colordef(0x09090b);
const UIRGBA UINeutral50 = colordef(0xfafafa);
const UIRGBA UINeutral100 = colordef(0xf5f5f5);
const UIRGBA UINeutral200 = colordef(0xe5e5e5);
const UIRGBA UINeutral300 = colordef(0xd4d4d4);
const UIRGBA UINeutral400 = colordef(0xa3a3a3);
const UIRGBA UINeutral500 = colordef(0x737373);
const UIRGBA UINeutral600 = colordef(0x525252);
const UIRGBA UINeutral700 = colordef(0x404040);
const UIRGBA UINeutral800 = colordef(0x262626);
const UIRGBA UINeutral900 = colordef(0x171717);
const UIRGBA UINeutral950 = colordef(0x0a0a0a);
const UIRGBA UIStone50 = colordef(0xfafaf9);
const UIRGBA UIStone100 = colordef(0xf5f5f4);
const UIRGBA UIStone200 = colordef(0xe7e5e4);
const UIRGBA UIStone300 = colordef(0xd6d3d1);
const UIRGBA UIStone400 = colordef(0xa8a29e);
const UIRGBA UIStone500 = colordef(0x78716c);
const UIRGBA UIStone600 = colordef(0x57534e);
const UIRGBA UIStone700 = colordef(0x44403c);
const UIRGBA UIStone800 = colordef(0x292524);
const UIRGBA UIStone900 = colordef(0x1c1917);
const UIRGBA UIStone950 = colordef(0x0c0a09);
const UIRGBA UIRed50 = colordef(0xfef2f2);
const UIRGBA UIRed100 = colordef(0xfee2e2);
const UIRGBA UIRed200 = colordef(0xfecaca);
const UIRGBA UIRed300 = colordef(0xfca5a5);
const UIRGBA UIRed400 = colordef(0xf87171);
const UIRGBA UIRed500 = colordef(0xef4444);
const UIRGBA UIRed600 = colordef(0xdc2626);
const UIRGBA UIRed700 = colordef(0xb91c1c);
const UIRGBA UIRed800 = colordef(0x991b1b);
const UIRGBA UIRed900 = colordef(0x7f1d1d);
const UIRGBA UIRed950 = colordef(0x450a0a);
const UIRGBA UIOrange50 = colordef(0xfff7ed);
const UIRGBA UIOrange100 = colordef(0xffedd5);
const UIRGBA UIOrange200 = colordef(0xfed7aa);
const UIRGBA UIOrange300 = colordef(0xfdba74);
const UIRGBA UIOrange400 = colordef(0xfb923c);
const UIRGBA UIOrange500 = colordef(0xf97316);
const UIRGBA UIOrange600 = colordef(0xea580c);
const UIRGBA UIOrange700 = colordef(0xc2410c);
const UIRGBA UIOrange800 = colordef(0x9a3412);
const UIRGBA UIOrange900 = colordef(0x7c2d12);
const UIRGBA UIOrange950 = colordef(0x431407);
const UIRGBA UIAmber50 = colordef(0xfffbeb);
const UIRGBA UIAmber100 = colordef(0xfef3c7);
const UIRGBA UIAmber200 = colordef(0xfde68a);
const UIRGBA UIAmber300 = colordef(0xfcd34d);
const UIRGBA UIAmber400 = colordef(0xfbbf24);
const UIRGBA UIAmber500 = colordef(0xf59e0b);
const UIRGBA UIAmber600 = colordef(0xd97706);
const UIRGBA UIAmber700 = colordef(0xb45309);
const UIRGBA UIAmber800 = colordef(0x92400e);
const UIRGBA UIAmber900 = colordef(0x78350f);
const UIRGBA UIAmber950 = colordef(0x451a03);
const UIRGBA UIYellow50 = colordef(0xfefce8);
const UIRGBA UIYellow100 = colordef(0xfef9c3);
const UIRGBA UIYellow200 = colordef(0xfef08a);
const UIRGBA UIYellow300 = colordef(0xfde047);
const UIRGBA UIYellow400 = colordef(0xfacc15);
const UIRGBA UIYellow500 = colordef(0xeab308);
const UIRGBA UIYellow600 = colordef(0xca8a04);
const UIRGBA UIYellow700 = colordef(0xa16207);
const UIRGBA UIYellow800 = colordef(0x854d0e);
const UIRGBA UIYellow900 = colordef(0x713f12);
const UIRGBA UIYellow950 = colordef(0x422006);
const UIRGBA UILime50 = colordef(0xf7fee7);
const UIRGBA UILime100 = colordef(0xecfccb);
const UIRGBA UILime200 = colordef(0xd9f99d);
const UIRGBA UILime300 = colordef(0xbef264);
const UIRGBA UILime400 = colordef(0xa3e635);
const UIRGBA UILime500 = colordef(0x84cc16);
const UIRGBA UILime600 = colordef(0x65a30d);
const UIRGBA UILime700 = colordef(0x4d7c0f);
const UIRGBA UILime800 = colordef(0x3f6212);
const UIRGBA UILime900 = colordef(0x365314);
const UIRGBA UILime950 = colordef(0x1a2e05);
const UIRGBA UIGreen50 = colordef(0xf0fdf4);
const UIRGBA UIGreen100 = colordef(0xdcfce7);
const UIRGBA UIGreen200 = colordef(0xbbf7d0);
const UIRGBA UIGreen300 = colordef(0x86efac);
const UIRGBA UIGreen400 = colordef(0x4ade80);
const UIRGBA UIGreen500 = colordef(0x22c55e);
const UIRGBA UIGreen600 = colordef(0x16a34a);
const UIRGBA UIGreen700 = colordef(0x15803d);
const UIRGBA UIGreen800 = colordef(0x166534);
const UIRGBA UIGreen900 = colordef(0x14532d);
const UIRGBA UIGreen950 = colordef(0x052e16);
const UIRGBA UIEmerald50 = colordef(0xecfdf5);
const UIRGBA UIEmerald100 = colordef(0xd1fae5);
const UIRGBA UIEmerald200 = colordef(0xa7f3d0);
const UIRGBA UIEmerald300 = colordef(0x6ee7b7);
const UIRGBA UIEmerald400 = colordef(0x34d399);
const UIRGBA UIEmerald500 = colordef(0x10b981);
const UIRGBA UIEmerald600 = colordef(0x059669);
const UIRGBA UIEmerald700 = colordef(0x047857);
const UIRGBA UIEmerald800 = colordef(0x065f46);
const UIRGBA UIEmerald900 = colordef(0x064e3b);
const UIRGBA UIEmerald950 = colordef(0x022c22);
const UIRGBA UITeal50 = colordef(0xf0fdfa);
const UIRGBA UITeal100 = colordef(0xccfbf1);
const UIRGBA UITeal200 = colordef(0x99f6e4);
const UIRGBA UITeal300 = colordef(0x5eead4);
const UIRGBA UITeal400 = colordef(0x2dd4bf);
const UIRGBA UITeal500 = colordef(0x14b8a6);
const UIRGBA UITeal600 = colordef(0x0d9488);
const UIRGBA UITeal700 = colordef(0x0f766e);
const UIRGBA UITeal800 = colordef(0x115e59);
const UIRGBA UITeal900 = colordef(0x134e4a);
const UIRGBA UITeal950 = colordef(0x042f2e);
const UIRGBA UICyan50 = colordef(0xecfeff);
const UIRGBA UICyan100 = colordef(0xcffafe);
const UIRGBA UICyan200 = colordef(0xa5f3fc);
const UIRGBA UICyan300 = colordef(0x67e8f9);
const UIRGBA UICyan400 = colordef(0x22d3ee);
const UIRGBA UICyan500 = colordef(0x06b6d4);
const UIRGBA UICyan600 = colordef(0x0891b2);
const UIRGBA UICyan700 = colordef(0x0e7490);
const UIRGBA UICyan800 = colordef(0x155e75);
const UIRGBA UICyan900 = colordef(0x164e63);
const UIRGBA UICyan950 = colordef(0x083344);
const UIRGBA UISky50 = colordef(0xf0f9ff);
const UIRGBA UISky100 = colordef(0xe0f2fe);
const UIRGBA UISky200 = colordef(0xbae6fd);
const UIRGBA UISky300 = colordef(0x7dd3fc);
const UIRGBA UISky400 = colordef(0x38bdf8);
const UIRGBA UISky500 = colordef(0x0ea5e9);
const UIRGBA UISky600 = colordef(0x0284c7);
const UIRGBA UISky700 = colordef(0x0369a1);
const UIRGBA UISky800 = colordef(0x075985);
const UIRGBA UISky900 = colordef(0x0c4a6e);
const UIRGBA UISky950 = colordef(0x082f49);
const UIRGBA UIBlue50 = colordef(0xeff6ff);
const UIRGBA UIBlue100 = colordef(0xdbeafe);
const UIRGBA UIBlue200 = colordef(0xbfdbfe);
const UIRGBA UIBlue300 = colordef(0x93c5fd);
const UIRGBA UIBlue400 = colordef(0x60a5fa);
const UIRGBA UIBlue500 = colordef(0x3b82f6);
const UIRGBA UIBlue600 = colordef(0x2563eb);
const UIRGBA UIBlue700 = colordef(0x1d4ed8);
const UIRGBA UIBlue800 = colordef(0x1e40af);
const UIRGBA UIBlue900 = colordef(0x1e3a8a);
const UIRGBA UIBlue950 = colordef(0x172554);
const UIRGBA UIIndigo50 = colordef(0xeef2ff);
const UIRGBA UIIndigo100 = colordef(0xe0e7ff);
const UIRGBA UIIndigo200 = colordef(0xc7d2fe);
const UIRGBA UIIndigo300 = colordef(0xa5b4fc);
const UIRGBA UIIndigo400 = colordef(0x818cf8);
const UIRGBA UIIndigo500 = colordef(0x6366f1);
const UIRGBA UIIndigo600 = colordef(0x4f46e5);
const UIRGBA UIIndigo700 = colordef(0x4338ca);
const UIRGBA UIIndigo800 = colordef(0x3730a3);
const UIRGBA UIIndigo900 = colordef(0x312e81);
const UIRGBA UIIndigo950 = colordef(0x1e1b4b);
const UIRGBA UIViolet50 = colordef(0xf5f3ff);
const UIRGBA UIViolet100 = colordef(0xede9fe);
const UIRGBA UIViolet200 = colordef(0xddd6fe);
const UIRGBA UIViolet300 = colordef(0xc4b5fd);
const UIRGBA UIViolet400 = colordef(0xa78bfa);
const UIRGBA UIViolet500 = colordef(0x8b5cf6);
const UIRGBA UIViolet600 = colordef(0x7c3aed);
const UIRGBA UIViolet700 = colordef(0x6d28d9);
const UIRGBA UIViolet800 = colordef(0x5b21b6);
const UIRGBA UIViolet900 = colordef(0x4c1d95);
const UIRGBA UIViolet950 = colordef(0x2e1065);
const UIRGBA UIPurple50 = colordef(0xfaf5ff);
const UIRGBA UIPurple100 = colordef(0xf3e8ff);
const UIRGBA UIPurple200 = colordef(0xe9d5ff);
const UIRGBA UIPurple300 = colordef(0xd8b4fe);
const UIRGBA UIPurple400 = colordef(0xc084fc);
const UIRGBA UIPurple500 = colordef(0xa855f7);
const UIRGBA UIPurple600 = colordef(0x9333ea);
const UIRGBA UIPurple700 = colordef(0x7e22ce);
const UIRGBA UIPurple800 = colordef(0x6b21a8);
const UIRGBA UIPurple900 = colordef(0x581c87);
const UIRGBA UIPurple950 = colordef(0x3b0764);
const UIRGBA UIFuchsia50 = colordef(0xfdf4ff);
const UIRGBA UIFuchsia100 = colordef(0xfae8ff);
const UIRGBA UIFuchsia200 = colordef(0xf5d0fe);
const UIRGBA UIFuchsia300 = colordef(0xf0abfc);
const UIRGBA UIFuchsia400 = colordef(0xe879f9);
const UIRGBA UIFuchsia500 = colordef(0xd946ef);
const UIRGBA UIFuchsia600 = colordef(0xc026d3);
const UIRGBA UIFuchsia700 = colordef(0xa21caf);
const UIRGBA UIFuchsia800 = colordef(0x86198f);
const UIRGBA UIFuchsia900 = colordef(0x701a75);
const UIRGBA UIFuchsia950 = colordef(0x4a044e);
const UIRGBA UIPink50 = colordef(0xfdf2f8);
const UIRGBA UIPink100 = colordef(0xfce7f3);
const UIRGBA UIPink200 = colordef(0xfbcfe8);
const UIRGBA UIPink300 = colordef(0xf9a8d4);
const UIRGBA UIPink400 = colordef(0xf472b6);
const UIRGBA UIPink500 = colordef(0xec4899);
const UIRGBA UIPink600 = colordef(0xdb2777);
const UIRGBA UIPink700 = colordef(0xbe185d);
const UIRGBA UIPink800 = colordef(0x9d174d);
const UIRGBA UIPink900 = colordef(0x831843);
const UIRGBA UIPink950 = colordef(0x500724);
const UIRGBA UIRose50 = colordef(0xfff1f2);
const UIRGBA UIRose100 = colordef(0xffe4e6);
const UIRGBA UIRose200 = colordef(0xfecdd3);
const UIRGBA UIRose300 = colordef(0xfda4af);
const UIRGBA UIRose400 = colordef(0xfb7185);
const UIRGBA UIRose500 = colordef(0xf43f5e);
const UIRGBA UIRose600 = colordef(0xe11d48);
const UIRGBA UIRose700 = colordef(0xbe123c);
const UIRGBA UIRose800 = colordef(0x9f1239);
const UIRGBA UIRose900 = colordef(0x881337);
const UIRGBA UIRose950 = colordef(0x4c0519);
#endif // _UI__COLORS_H

31
src/dispatcher-node.c Normal file
View file

@ -0,0 +1,31 @@
#include <stdlib.h>
#include "dispatcher-node.h"
int dispatcher_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{
if (!node->parent) {
return -1;
}
UIDispatcherNode *n = (UIDispatcherNode*)node;
if (ev == n->node_event_type) {
(n->handle)(node->parent, n->data, n->event_type, d, p);
}
return 0;
}
UIDispatcherNode *dispatcher_new(UINode *parent, enum UIEvent node_event_type, int event_type, void *data, int (*handle)(struct UINode *node, void *data, int event_type, size_t d, void *p))
{
UIDispatcherNode *n = malloc(sizeof(UIDispatcherNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "dispatcher";
n->node.handle_proto = dispatcher_handle;
n->node_event_type = node_event_type;
n->event_type = event_type;
n->data = data;
n->handle = handle;
node_attach(parent, (UINode*)n);
return n;
}

17
src/dispatcher-node.h Normal file
View file

@ -0,0 +1,17 @@
#ifndef _UI__DISPATCHER_NODE_H
#define _UI__DISPATCHER_NODE_H
#include "node.h"
typedef struct UIDispatcherNode {
UINode node;
enum UIEvent node_event_type;
int event_type;
void *data;
int (*handle)(struct UINode *node, void *data, int event_type, size_t d, void *p);
} UIDispatcherNode;
UIDispatcherNode *dispatcher_new(UINode *parent, enum UIEvent node_event_type, int event_type, void *data, int (*handle)(struct UINode *node, void *data, int event_type, size_t d, void *p));
int dispatcher_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
#endif // _UI__DISPATCHER_NODE_H

168
src/main.c Normal file
View file

@ -0,0 +1,168 @@
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pango/pango.h>
#include "color.h"
#include "node.h"
#include "scrollable-node.h"
#include "window.h"
#include "text-node.h"
#include "background-node.h"
#include "dispatcher-node.h"
#include "box-layout-node.h"
#include "colors.h"
#include <xcb/randr.h>
typedef struct AppState {
PangoFontDescription *font;
UITextNode *counter_text_node;
UINode *sidebar_node;
int count;
char counter_text[12];
uint64_t sidebar_animate_duration_ms;
uint64_t sidebar_animate_time_ms;
double sidebar_animate_target_w;
double sidebar_animate_start_w;
} AppState;
enum AppEvent {
APP_SCAFFOLD,
INCREMENT_COUNT,
DECREMENT_COUNT,
UPDATE_COUNT,
SIDEBAR_ANIAMTE,
SIDEBAR_BEGIN_ANIMATE
};
double lerp(double a, double b, double t)
{
return a + (b - a) * t;
}
int app_handle(struct UINode *node, void *data, int event_type, size_t d, void *p)
{
(void)d; (void)p;
AppState *state = data;
switch (event_type) {
case APP_SCAFFOLD: {
/* layout */
{
UIBoxLayoutNode *main_layout = box_layout_new(node, UI_DIRECTION_HORIZONTAL);
main_layout->gap = 6;
}
/* sidebar */
{
UINode *sidebar = node_new(node, "sidebar");
sidebar->width_policy = UI_SIZE_POLICY_STATIC;
sidebar->height_policy = UI_SIZE_POLICY_GROW;
sidebar->rect.w = 200;
dispatcher_new(sidebar, UI_EVENT_TIMER_END, SIDEBAR_ANIAMTE, state, app_handle);
background_node_new(sidebar, UIZinc800, 0);
state->sidebar_node = sidebar;
}
/* animate button */
{
UINode *button = node_new(node, "button");
state_background_node_new(button, UIPurple600, UIPurple700, UIPurple800, 6.0);
text_node_new(button, state->font, UINeutral50, "animate");
dispatcher_new(button, UI_EVENT_UNPRESSED, SIDEBAR_BEGIN_ANIMATE, state, app_handle);
}
/* increment button */
{
UINode *button = node_new(node, "button");
state_background_node_new(button, UIPurple600, UIPurple700, UIPurple800, 6.0);
text_node_new(button, state->font, UINeutral50, "add");
dispatcher_new(button, UI_EVENT_UNPRESSED, INCREMENT_COUNT, state, app_handle);
}
/* count display */
{
UINode *counter = node_new(node, "counter_display");
state->counter_text_node = text_node_new(counter, state->font, UINeutral50, "0");
}
/* decrement button */
{
UINode *button = node_new(node, "button");
state_background_node_new(button, UIPurple600, UIPurple700, UIPurple800, 6.0);
text_node_new(button, state->font, UINeutral50, "subtract");
dispatcher_new(button, UI_EVENT_UNPRESSED, DECREMENT_COUNT, state, app_handle);
}
break;
}
case SIDEBAR_BEGIN_ANIMATE: {
state->sidebar_animate_start_w = state->sidebar_node->rect.w;
state->sidebar_animate_target_w = state->sidebar_node->rect.w == 0 ? 200 : 0;
state->sidebar_animate_time_ms = 0.0;
window_sched_timer(node->window, state->sidebar_node, 16);
break;
}
case SIDEBAR_ANIAMTE: {
uint64_t dt = *(uint64_t*)p;
state->sidebar_animate_time_ms += dt;
state->sidebar_node->rect.w = lerp(state->sidebar_animate_start_w, state->sidebar_animate_target_w, (double)state->sidebar_animate_time_ms / (double)state->sidebar_animate_duration_ms);
if (state->sidebar_node->rect.w < 0) {
state->sidebar_node->rect.w = 0;
}
node_request_relayout(state->sidebar_node->parent);
if (state->sidebar_animate_time_ms >= state->sidebar_animate_duration_ms) {
state->sidebar_animate_time_ms = 0;
return 0;
}
window_sched_timer(node->window, state->sidebar_node, 10);
return 1;
}
case INCREMENT_COUNT: {
app_handle(node, data, UPDATE_COUNT, state->count + 1, p);
break;
}
case DECREMENT_COUNT: {
app_handle(node, data, UPDATE_COUNT, state->count - 1, p);
break;
}
case UPDATE_COUNT: {
state->count = d;
snprintf(state->counter_text, sizeof(state->counter_text), "%d", state->count);
state->counter_text_node->pending_text = state->counter_text;
node_request_relayout((UINode*)state->counter_text_node);
break;
}
}
return 0;
}
int main()
{
UIWindow *window = window_new(800, 600);
if (!window) {
fprintf(stderr, "err: failed to create window\n");
return EXIT_FAILURE;
}
PangoFontDescription *desc = pango_font_description_from_string("sans-serif");
UINode *root = node_new(NULL, "[root]");
window_attach_root(window, root);
background_node_new(root, UIZinc900, 0.0);
AppState state = {0};
state.font = desc;
state.sidebar_animate_duration_ms = 3000;
app_handle(root, &state, APP_SCAFFOLD, 0, NULL);
window_turn(window);
pango_font_description_free(desc);
window_free(window);
return EXIT_SUCCESS;
}

View file

@ -1,123 +0,0 @@
#include "Label.hpp"
#include "Application.hpp"
#include "ListLayout.hpp"
#include "ListView.hpp"
#include "TextInput.hpp"
#include "Window.hpp"
#include "Widget.hpp"
#include "Button.hpp"
#include "Box.hpp"
#include "Label.hpp"
#include "Layout.hpp"
#include "RGB.hpp"
#include "DocumentLayout.hpp"
#include "Events.hpp"
#include "BoxLayout.hpp"
#include "Box.hpp"
#include "ScrollContainer.hpp"
#include "Styles.hpp"
#include <iostream>
#include <memory>
#include <string>
int main() {
Raven::Application application {};
auto window = application.add_window<Raven::Window>();
auto test_window = application.add_window<Raven::Window>();
window->spawn_window();
test_window->spawn_window();
auto test_container = test_window->set_main_widget<Raven::ScrollContainer>();
auto test_widget = test_container->make_target();
auto test_layout = test_widget->set_layout<Raven::ListLayout>(Raven::Direction::Vertical);
test_layout->set_margin(69);
test_layout->set_spacing(10);
test_layout->set_inherit_secondary_dimension(true);
for (int i = 0; i < 150; i++) {
auto button = test_widget->add<Raven::Button>("Hello: " + std::to_string(i));
button->set_style(&Raven::raised_button_style);
button->on_click = [button]() {
if (button->style() == &Raven::accent_button_style) {
button->set_style(&Raven::raised_button_style);
} else {
button->set_style(&Raven::accent_button_style);
}
};
}
auto main_widget = window->set_main_widget<Raven::Widget>();
auto main_widget_layout = main_widget->set_layout<Raven::BoxLayout>(Raven::Direction::Horizontal);
main_widget_layout->set_margin(8);
main_widget_layout->set_spacing(8);
main_widget_layout->slot_pixel(250);
main_widget_layout->slot_percent(100.0);
auto list_view = main_widget->add<Raven::ListView>();
auto content = main_widget->add<Raven::Widget>();
auto content_layout = content->set_layout<Raven::BoxLayout>(Raven::Direction::Vertical);
content_layout->set_margin(6.0);
content_layout->set_spacing(6.0);
content->set_style(&Raven::raised_widget_style);
auto selected_label = content->add<Raven::Label>("No selection");
selected_label->rect().set_max_height(28.0);
auto control_row = content->add<Raven::Widget>();
control_row->set_style(&Raven::raised_widget_style);
auto control_row_layout = control_row->set_layout<Raven::BoxLayout>(Raven::Direction::Horizontal);
control_row_layout->set_spacing(6.0);
control_row_layout->set_justify_secondary_dimension(true);
auto next_button = control_row->add<Raven::Button>("Next Item", Raven::Button::Accent);
next_button->set_grows(true);
auto delete_button = control_row->add<Raven::Button>("Delete Item");
delete_button->set_style(&Raven::raised_button_style);
delete_button->set_grows(true);
auto edit_row = content->add<Raven::Widget>();
edit_row->set_style(&Raven::raised_widget_style);
auto edit_row_layout = edit_row->set_layout<Raven::BoxLayout>(Raven::Direction::Horizontal);
edit_row_layout->set_spacing(6.0);
edit_row_layout->set_justify_secondary_dimension(true);
auto edit_input = edit_row->add<Raven::TextInput>();
edit_input->set_style(&Raven::raised_textinput_style);
edit_input->set_grows(true);
auto edit_button = edit_row->add<Raven::Button>("Edit");
edit_button->set_style(&Raven::accent_button_style);
edit_button->on_click = [list_view, edit_input]() {
if (list_view->active_element() >= 0) {
list_view->elements[list_view->active_element()] = edit_input->text();
list_view->elements_updated();
}
};
delete_button->on_click = [list_view]() {
list_view->elements.erase(list_view->elements.begin() + list_view->active_element());
list_view->set_active_element(list_view->active_element() - 1);
list_view->elements_updated();
};
next_button->on_click = [list_view]() {
list_view->set_active_element(list_view->active_element() + 1);
};
list_view->on_selection = [selected_label](unsigned int index, std::string item) {
selected_label->set_text("Selected: " + item);
};
int i = 100;
while (i --> 0) {
list_view->elements.push_back("Item " + std::to_string(i));
}
list_view->elements_updated();
return application.run();
}

153
src/node.c Normal file
View file

@ -0,0 +1,153 @@
#include <stdlib.h>
#include <stdio.h>
#include "node.h"
#include "rect.h"
typedef struct UIWindow UIWindow;
int window_invalidate_node(UIWindow *window, UINode *node);
int node_dispatch(UINode *node, enum UIEvent ev, size_t d, void *p)
{
for (int i = 0; i < node->nodes_count; i++) {
if (node->nodes[i]->flags & UI_NODE_COMPONENT) {
int result = node_dispatch(node->nodes[i], ev, d, p);
if (result) return result;
}
}
if (node->handle) {
int result = (node->handle)(node, ev, d, p);
if (result) return result;
}
if (node->handle_proto) {
return (node->handle_proto)(node, ev, d, p);
}
return 0;
}
UINode *node_by_point(UINode *root, double x, double y)
{
for (int i = 0; i < root->nodes_count; i++) {
double local_x = x - root->nodes[i]->window_rel_x;
double local_y = y - root->nodes[i]->window_rel_y;
if (ui_rect_contains_point(&root->nodes[i]->rect, local_x, local_y)) {
return node_by_point(root->nodes[i], local_x, local_y);
}
}
return root;
}
void node_init(UINode *node)
{
node->rect = (UIRect){0, 0, 0, 0};
node->window_rel_x = 0;
node->window_rel_y = 0;
node->parent = NULL;
node->nodes = NULL;
node->window = NULL;
node->drw = NULL;
node->nodes_count = 0;
node->nodes_capacity = 0;
node->flags = 0;
node->width_policy = UI_SIZE_POLICY_DYNAMIC;
node->height_policy = UI_SIZE_POLICY_DYNAMIC;
node->handle = NULL;
node->handle_proto = NULL;
}
void node_free(UINode *node)
{
if (!node) return;
node_dispatch(node, UI_EVENT_DESTROY, 0, NULL);
for (int i = 0; i < node->nodes_count; i++) {
node_free(node->nodes[i]);
}
free(node->nodes);
free(node);
}
void node_repaint(UINode *node, UIRect *rect, bool do_group, double window_rel_x, double window_rel_y)
{
node->window_rel_x = window_rel_x;
node->window_rel_y = window_rel_y;
if (!ui_rect_overlap_rect(rect, &node->rect)) {
return;
}
if (do_group) {
cairo_rectangle(node->drw, rect->x, rect->y, rect->w, rect->h);
cairo_clip(node->drw);
}
cairo_save(node->drw);
cairo_translate(node->drw, node->rect.x, node->rect.y);
node_dispatch(node, UI_EVENT_REPAINT, 0, NULL);
UIRect local_rect = { rect->x - node->rect.x, rect->y - node->rect.y, rect->w, rect->h };
for (int i = 0; i < node->nodes_count; i++) {
UINode *current = node->nodes[i];
if (current->flags & UI_NODE_COMPONENT) {
cairo_save(current->drw);
node_dispatch(current, UI_EVENT_REPAINT, 0, NULL);
cairo_restore(current->drw);
} else {
node_repaint(node->nodes[i], &local_rect, false, window_rel_x + node->rect.x, window_rel_y + node->rect.y);
}
}
cairo_restore(node->drw);
}
UINode *node_attach(UINode *parent, UINode *node)
{
if (node->parent) {
return NULL;
}
if (parent) {
node->parent = parent;
node->drw = parent->drw;
node->window = parent->window;
if (parent->nodes_count >= parent->nodes_capacity) {
parent->nodes_capacity += 2;
parent->nodes = realloc(parent->nodes, parent->nodes_capacity * sizeof(**parent->nodes));
if (!parent->nodes) {
return NULL;
}
}
parent->nodes[parent->nodes_count++] = node;
}
return node;
}
UINode *node_new(UINode *parent, const char *text)
{
UINode *node = malloc(sizeof(UINode));
node_init(node);
node->text = text;
node_attach(parent, (UINode*)node);
return node;
}
void node_dump(UINode *node, int depth)
{
printf("%*s%s [%g, %g %gx%g]\n", depth * 2, "", node->text, node->rect.x, node->rect.y, node->rect.w, node->rect.h);
for (int i = 0; i < node->nodes_count; i++) {
node_dump(node->nodes[i], depth + 1);
}
}
void node_request_relayout(UINode *node)
{
UINode *current = NULL;
for (current = node; current->parent && current->width_policy != UI_SIZE_POLICY_STATIC && current->height_policy != UI_SIZE_POLICY_STATIC; current = current->parent);
if (current) {
node_dispatch(current, UI_EVENT_RELAYOUT, 0, NULL);
window_invalidate_node(current->window, current);
}
}

68
src/node.h Normal file
View file

@ -0,0 +1,68 @@
#ifndef _UI__NODE_H
#define _UI__NODE_H
#include <stdint.h>
#include <unistd.h>
#include "rect.h"
#include <cairo.h>
enum UIEvent {
UI_EVENT_REPAINT,
UI_EVENT_RELAYOUT,
UI_EVENT_GET_WIDTH,
UI_EVENT_GET_HEIGHT,
UI_EVENT_HOVERED,
UI_EVENT_UNHOVERED,
UI_EVENT_PRESSED,
UI_EVENT_UNPRESSED,
UI_EVENT_DESTROY,
UI_EVENT_TIMER_END,
UI_EVENT_BUTTON_LEFT_UPDATE, // d == 1 if pressed, d == 0 if released
UI_EVENT_BUTTON_RIGHT_UPDATE, // d == 1 if pressed, d == 0 if released
UI_EVENT_SCROLL, // p (double*) = delta
};
enum UISizePolicy {
UI_SIZE_POLICY_DYNAMIC,
UI_SIZE_POLICY_GROW,
UI_SIZE_POLICY_FIXED,
UI_SIZE_POLICY_STATIC
};
enum UIDirection {
UI_DIRECTION_HORIZONTAL,
UI_DIRECTION_VERTICAL
};
#define UI_NODE_COMPONENT (1 << 0)
typedef struct UINode {
const char *text;
UIRect rect;
double window_rel_x, window_rel_y;
struct UINode *parent;
struct UINode **nodes;
struct UIWindow *window;
cairo_t *drw;
int nodes_count, nodes_capacity;
uint32_t flags;
enum UISizePolicy width_policy;
enum UISizePolicy height_policy;
int (*handle)(struct UINode *node, enum UIEvent ev, size_t d, void *p);
int (*handle_proto)(struct UINode *node, enum UIEvent ev, size_t d, void *p);
} UINode;
int node_dispatch(UINode *node, enum UIEvent ev, size_t d, void *p);
UINode *node_by_point(UINode *root, double x, double y);
void node_init(UINode *node);
void node_free(UINode *node);
void node_repaint(UINode *node, UIRect *rect, bool do_group, double window_rel_x, double window_rel_y);
UINode *node_attach(UINode *parent, UINode *node);
UINode *node_new(UINode *parent, const char *text);
void node_dump(UINode *node, int depth);
void node_request_relayout(UINode *node);
#endif // _UI__NODE_H

158
src/prof.c Normal file
View file

@ -0,0 +1,158 @@
// TODO: document how to use the profiler module @doc
#include <assert.h>
#include <time.h>
#include <stddef.h>
#ifdef PROF
typedef struct {
const char *label;
double elapsed;
size_t size;
} Entry;
typedef struct {
struct timespec begin;
Entry *entry;
} Clock;
// TODO: several active contexts
#define CLOCK_STACK_CAP 256
Clock clock_stack[CLOCK_STACK_CAP];
size_t clock_stack_count = 0;
#define SUMMARY_CAP 1024
Entry summary[SUMMARY_CAP];
size_t summary_count = 0;
// TODO: Profiler: several passes over the same path and compute the average elapsed time
// It would be interesting to explore how to do that. If you do several passes over the
// same path without cleaning the summary you will just pile more entries to the summary.
// We need a way to "rewind" the summary_count so the pass hits the same entries again and
// computes the average time on their elapsed values.
//
// This would be also super useful when we integrate our renderer with a display window. This
// will enable us to display the average renderer performance in real time.
void begin_clock(const char *label)
{
assert(clock_stack_count < CLOCK_STACK_CAP);
assert(summary_count < SUMMARY_CAP);
Entry *e = &summary[summary_count++];
e->label = label;
e->size = 1;
e->elapsed = 0.0;
Clock *c = &clock_stack[clock_stack_count++];
if (clock_gettime(CLOCK_MONOTONIC, &c->begin) < 0) {
fprintf(stderr, "ERROR: could not get current monotonic time: %s\n",
strerror(errno));
exit(1);
}
c->entry = e;
}
void end_clock(void)
{
assert(clock_stack_count > 0);
Clock *c = &clock_stack[--clock_stack_count];
struct timespec end;
if (clock_gettime(CLOCK_MONOTONIC, &end) < 0) {
fprintf(stderr, "ERROR: could not get current monotonic time: %s\n",
strerror(errno));
exit(1);
}
c->entry->elapsed = (end.tv_sec - c->begin.tv_sec) * 1000 + (end.tv_nsec - c->begin.tv_nsec) / 1e+6;
if (clock_stack_count > 0) {
Clock *pc = &clock_stack[clock_stack_count - 1];
pc->entry->size += c->entry->size;
}
}
void render_entry(FILE *stream, ptrdiff_t root, size_t level, size_t line_width)
{
fprintf(stream, "%*s%-*s%.9lfms\n",
(int) level * 2, "",
(int) line_width - (int) level * 2, summary[root].label,
summary[root].elapsed);
size_t size = summary[root].size - 1;
ptrdiff_t child = root + 1;
while (size > 0) {
render_entry(stream, child, level + 1, line_width);
size -= summary[child].size;
child += summary[child].size;
}
}
void render_summary(FILE *stream, size_t line_width)
{
ptrdiff_t root = 0;
while ((size_t) root < summary_count) {
render_entry(stream, root, 0, line_width);
root += summary[root].size;
}
}
size_t estimate_entry_line_width(ptrdiff_t root, size_t level)
{
size_t line_width = 2 * level + strlen(summary[root].label);
size_t size = summary[root].size - 1;
ptrdiff_t child = root + 1;
while (size > 0) {
size_t entry_line_width = estimate_entry_line_width( child, level + 1);
if (entry_line_width > line_width) {
line_width = entry_line_width;
}
size -= summary[child].size;
child += summary[child].size;
}
return line_width;
}
size_t estimate_line_width(void)
{
size_t line_width = 0;
ptrdiff_t root = 0;
while ((size_t) root < summary_count) {
size_t entry_line_width = estimate_entry_line_width(root, 0);
if (entry_line_width > line_width) {
line_width = entry_line_width;
}
root += summary[root].size;
}
return line_width;
}
void clear_summary()
{
clock_stack_count = 0;
summary_count = 0;
}
void dump_summary(FILE *stream)
{
size_t line_width = estimate_line_width();
render_summary(stream, line_width + 2);
clear_summary();
}
#else
#define begin_clock(...)
#define end_clock(...)
#define dump_summary(...)
#define clear_summary(...)
#endif

39
src/rect.c Normal file
View file

@ -0,0 +1,39 @@
#include "rect.h"
#include <stdbool.h>
#define max(a,b) __extension__({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _a : _b; })
#define min(a,b) __extension__({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; })
bool ui_rect_null(UIRect *r)
{
return !(r->x || r->y || r->w || r->h);
}
bool ui_rect_contains_point(UIRect *r, double x, double y)
{
return r->x <= x && r->x + r->w >= x && r->y <= y && r->y + r->h >= y;
}
bool ui_rect_overlap_rect(UIRect *a, UIRect *b)
{
// https://stackoverflow.com/a/306332
return (a->x < b->x + b->w && a->x + a->w > b->x && a->y < b->y + b->h && a->y + a->h > b->y);
}
UIRect ui_rect_united(UIRect *a, UIRect *b)
{
if (ui_rect_null(a)) return *b;
if (ui_rect_null(b)) return *a;
return (UIRect){
.x = min(a->x, b->x),
.y = min(a->y, b->y),
.w = max(a->x + a->w, b->x + b->w),
.h = max(a->y + a->h, b->y + b->h)
};
}
bool ui_rect_equals(UIRect *a, UIRect *b)
{
return a->x == b->x && a->y == b->y && a->w == b->w && a->h == b->h;
}

16
src/rect.h Normal file
View file

@ -0,0 +1,16 @@
#ifndef _UI__RECT_H
#define _UI__RECT_H
#include <stdbool.h>
typedef struct UIRect {
double x, y, w, h;
} UIRect;
bool ui_rect_null(UIRect *r);
bool ui_rect_contains_point(UIRect *r, double x, double y);
bool ui_rect_overlap_rect(UIRect *a, UIRect *b);
UIRect ui_rect_united(UIRect *a, UIRect *b);
bool ui_rect_equals(UIRect *a, UIRect *b);
#endif // _UI__RECT_H

67
src/scrollable-node.c Normal file
View file

@ -0,0 +1,67 @@
#include "scrollable-node.h"
#include "node.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
UIScrollableNode *scrollable_new(UINode *parent, UINode *target)
{
UIScrollableNode *n = malloc(sizeof(UIScrollableNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "scrollable";
n->node.handle_proto = scrollable_handle;
n->target = target;
n->x_scroll = 0;
n->y_scroll = 0;
node_attach(parent, (UINode*)n);
return n;
}
int scrollable_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{
(void)d;
UIScrollableNode *n = (UIScrollableNode *)node;
UINode *target = n->target;
if (!n->node.parent || !target) {
return 0;
}
// TODO: horizontal scroll
switch (ev) {
case UI_EVENT_SCROLL: {
double scroll_end = target->rect.h - node->parent->rect.h;
if (scroll_end < 0) scroll_end = 0;
n->y_scroll -= *(double*)p;
if (n->y_scroll < 0) {
n->y_scroll = 0;
}
if (n->y_scroll > scroll_end) {
n->y_scroll = scroll_end;
}
node_request_relayout(n->node.parent);
break;
}
case UI_EVENT_RELAYOUT: {
double w = target->rect.w;
double h = target->rect.h;
if (target->width_policy == UI_SIZE_POLICY_DYNAMIC) {
node_dispatch(target, UI_EVENT_GET_WIDTH, 0, &w);
target->rect.w = w;
}
if (target->height_policy == UI_SIZE_POLICY_DYNAMIC) {
node_dispatch(target, UI_EVENT_GET_HEIGHT, 0, &h);
target->rect.h = h;
}
target->rect.x = -n->x_scroll;
target->rect.y = -n->y_scroll;
node_dispatch(target, UI_EVENT_RELAYOUT, 0, NULL);
break;
}
default: {}
}
return 0;
}

15
src/scrollable-node.h Normal file
View file

@ -0,0 +1,15 @@
#ifndef _UI__SCROLL_CONTAINER_NODE_H
#define _UI__SCROLL_CONTAINER_NODE_H
#include "node.h"
typedef struct UIScrollableNode {
UINode node;
UINode *target;
double x_scroll, y_scroll;
} UIScrollableNode;
UIScrollableNode *scrollable_new(UINode *parent, UINode *target);
int scrollable_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
#endif // _UI__SCROLL_CONTAINER_NODE_H

94
src/text-node.c Normal file
View file

@ -0,0 +1,94 @@
#include <pango/pango.h>
#include <pango/pangocairo.h>
#include "text-node.h"
UITextNode *text_node_new(UINode *parent, PangoFontDescription *desc, UIRGBA color, char *text)
{
UITextNode *n = malloc(sizeof(UITextNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->pending_text = text;
n->desc = desc;
n->layout = NULL;
n->color = color;
n->node.text = "text";
n->node.handle_proto = text_node_handle;
node_attach(parent, (UINode*)n);
return n;
}
int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{
(void)d;
(void)p;
if (!node->parent) {
return -1;
}
UITextNode *n = (UITextNode*)node;
if (n->pending_text) {
if (n->layout) {
g_object_unref(n->layout);
}
PangoLayout *layout = pango_cairo_create_layout(n->node.drw);
pango_layout_set_font_description(layout, n->desc);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
pango_layout_set_text(layout, n->pending_text, -1);
n->layout = layout;
n->pending_text = NULL;
}
switch (ev) {
case UI_EVENT_REPAINT: {
if (!n->layout) {
break;
}
cairo_set_source_rgba(n->node.drw, n->color.r, n->color.g, n->color.b, n->color.a);
pango_cairo_show_layout(n->node.drw, n->layout);
cairo_fill(n->node.drw);
break;
}
case UI_EVENT_GET_WIDTH: {
if (!n->layout) {
break;
}
int w;
pango_layout_get_size(n->layout, &w, NULL);
*(double*)p = pango_units_to_double(w);
return 1;
break;
}
case UI_EVENT_GET_HEIGHT: {
if (!n->layout) {
break;
}
int h;
pango_layout_get_size(n->layout, NULL, &h);
*(double*)p = pango_units_to_double(h);
return 1;
break;
}
case UI_EVENT_RELAYOUT: {
if (!n->layout) {
break;
}
pango_cairo_update_layout(n->node.drw, n->layout);
break;
}
case UI_EVENT_DESTROY: {
if (n->layout) {
g_object_unref(n->layout);
}
n->layout = NULL;
n->pending_text = NULL;
break;
}
default: {
return 0;
}
}
return 0;
}

21
src/text-node.h Normal file
View file

@ -0,0 +1,21 @@
#ifndef _UI__TEXT_NODE_H
#define _UI__TEXT_NODE_H
#include "node.h"
#include "color.h"
#include <pango/pango-layout.h>
#include <pango/pango-font.h>
#include <pango/pango-types.h>
typedef struct UITextNode {
UINode node;
PangoFontDescription *desc;
PangoLayout *layout;
char *pending_text;
UIRGBA color;
} UITextNode;
int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
UITextNode *text_node_new(UINode *parent, PangoFontDescription *desc, UIRGBA color, char *text);
#endif // _UI__TEXT_NODE_H

9
src/timeutil.c Normal file
View file

@ -0,0 +1,9 @@
#include "timeutil.h"
#include <time.h>
int64_t time_current_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + (int64_t)ts.tv_nsec / 1000000;
}

8
src/timeutil.h Normal file
View file

@ -0,0 +1,8 @@
#ifndef _UI__TIMEUTIL_H
#define _UI__TIMEUTIL_H
#include <stdint.h>
int64_t time_current_ms(void);
#endif // _UI__TIMEUTIL_H

410
src/window.c Normal file
View file

@ -0,0 +1,410 @@
#include "cairo.h"
#include "timeutil.h"
#include "window.h"
#include "node.h"
//#define PROF
#include "prof.c"
#include "time.h"
#include <stdlib.h>
#include <sys/poll.h>
#include <time.h>
#include <xcb/xcb.h>
int window_invalidate_node(UIWindow *window, UINode *node)
{
if (node->flags & UI_NODE_COMPONENT) {
if (!node->parent) {
return -1;
}
node = node->parent;
}
UIRect local_rect = (UIRect){
.x = node->rect.x + node->window_rel_x,
.y = node->rect.y + node->window_rel_y,
.w = node->rect.w,
.h = node->rect.h
};
window->invalid_region = ui_rect_united(&window->invalid_region, &local_rect);
return 0;
}
void window_free(UIWindow *window)
{
if (!window) return;
if (window->_cairo_surface) cairo_surface_destroy(window->_cairo_surface);
if (window->drw) cairo_destroy(window->drw);
if (window->_xcb_connection) xcb_disconnect(window->_xcb_connection);
node_free(window->root);
free(window);
}
UIWindow *window_new(int width, int height)
{
bool fail = false;
UIWindow *window = NULL;
window = calloc(1, sizeof(UIWindow));
if (!window) {
fail = true;
goto done;
}
window->hovered = NULL;
window->pressed = NULL;
window->root = NULL;
window->invalid_region = (UIRect){ 0, 0, 0, 0 };
window->_cairo_surface = NULL;
window->_xcb_connection = NULL;
window->_xcb_window_id = -1;
window->drw = NULL;
window->_xcb_connection = xcb_connect(NULL, NULL);
if (xcb_connection_has_error(window->_xcb_connection) != 0) {
fail = true;
goto done;
}
const xcb_setup_t *setup = xcb_get_setup(window->_xcb_connection);
xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup);
xcb_screen_t *screen = iter.data;
xcb_visualtype_t *visualtype = NULL;
for (xcb_depth_iterator_t iter_depth = xcb_screen_allowed_depths_iterator(screen); iter_depth.rem; xcb_depth_next(&iter_depth)) {
for (xcb_visualtype_iterator_t iter_visualtype = xcb_depth_visuals_iterator(iter_depth.data); iter_visualtype.rem; xcb_visualtype_next(&iter_visualtype)) {
if (iter_visualtype.data->visual_id == screen->root_visual) {
visualtype = iter_visualtype.data;
break;
}
}
if (visualtype) break;
}
if (!visualtype) {
fail = true;
goto done;
}
window->_xcb_window_id = xcb_generate_id(window->_xcb_connection);
if (window->_xcb_window_id == (unsigned)-1) {
fail = true;
goto done;
}
xcb_create_window(
window->_xcb_connection, // connection
XCB_COPY_FROM_PARENT, // depth
window->_xcb_window_id, // window id
screen->root, // parent window
0, // x
0, // y
width, // width
height, // height
0, // border_width
XCB_WINDOW_CLASS_INPUT_OUTPUT, // class
screen->root_visual, // visual
XCB_CW_EVENT_MASK, // value mask
(uint32_t[]){ XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE } // value list
);
xcb_map_window(window->_xcb_connection, window->_xcb_window_id);
if (xcb_flush(window->_xcb_connection) <= 0) {
fail = true;
goto done;
}
window->_cairo_surface = cairo_xcb_surface_create(window->_xcb_connection, window->_xcb_window_id, visualtype, width, height);
if (cairo_surface_status(window->_cairo_surface) != CAIRO_STATUS_SUCCESS) {
fail = true;
goto done;
}
window->drw = cairo_create(window->_cairo_surface);
if (cairo_status(window->drw) != CAIRO_STATUS_SUCCESS) {
fail = true;
goto done;
}
done:
if (fail) {
window_free(window);
return NULL;
}
return window;
}
bool window_flush_invalidated(UIWindow *window)
{
if (ui_rect_null(&window->invalid_region)) {
return false;
}
cairo_push_group(window->drw);
node_repaint(window->root, &window->invalid_region, true, 0, 0);
window->invalid_region = (UIRect){ 0, 0, 0, 0 };
cairo_pop_group_to_source(window->drw);
cairo_paint(window->drw);
cairo_surface_flush(window->_cairo_surface);
return true;
}
int window_attach_root(UIWindow *window, UINode *root)
{
if (!window || !root || window->root || !window->drw) {
return -1;
}
root->window = window;
root->drw = window->drw;
root->width_policy = UI_SIZE_POLICY_STATIC;
root->height_policy = UI_SIZE_POLICY_STATIC;
window->root = root;
return 0;
}
void _window_process_xcb_event(UIWindow *window, xcb_generic_event_t *event)
{
begin_clock("Process XCB event");
uint8_t event_type = event->response_type & ~0x80;
switch (event_type) {
case XCB_EXPOSE: {
window_invalidate_node(window, window->root);
break;
}
case XCB_CONFIGURE_NOTIFY: {
xcb_configure_notify_event_t *ev = (xcb_configure_notify_event_t*)event;
UIRect new_rect = { 0, 0, ev->width, ev->height };
if (!ui_rect_equals(&window->root->rect, &new_rect)) {
cairo_xcb_surface_set_size(window->_cairo_surface, ev->width, ev->height);
window->root->rect.x = 0;
window->root->rect.y = 0;
window->root->rect.w = ev->width;
window->root->rect.h = ev->height;
node_dispatch(window->root, UI_EVENT_RELAYOUT, 0, NULL);
window_invalidate_node(window, window->root);
node_dump(window->root, 0);
}
break;
}
case XCB_MOTION_NOTIFY: {
xcb_motion_notify_event_t *ev = (xcb_motion_notify_event_t*)event;
UINode *node = node_by_point(window->root, ev->event_x, ev->event_y);
if (node && node != window->hovered) {
UINode *previously_hovered = window->hovered;
window->hovered = node;
if (previously_hovered) {
node_dispatch(previously_hovered, UI_EVENT_UNHOVERED, 0, NULL);
}
node_dispatch(node, UI_EVENT_HOVERED, 0, NULL);
}
break;
}
case XCB_BUTTON_RELEASE: /* through */
case XCB_BUTTON_PRESS: {
xcb_button_press_event_t *ev = (xcb_button_press_event_t*)event;
UINode *node = node_by_point(window->root, ev->event_x, ev->event_y);
if (node) {
enum UIEvent ui_event = UI_EVENT_BUTTON_LEFT_UPDATE;
bool state = event_type == XCB_BUTTON_PRESS ? true : false;
double delta = 0.0;
switch (ev->detail) {
case XCB_BUTTON_INDEX_3: {
ui_event = UI_EVENT_BUTTON_RIGHT_UPDATE;
break;
}
case XCB_BUTTON_INDEX_4: {
// scroll up
ui_event = UI_EVENT_SCROLL;
delta = 20.0;
break;
}
case XCB_BUTTON_INDEX_5: {
// scroll down
ui_event = UI_EVENT_SCROLL;
delta = -20.0;
break;
}
}
UINode *n = node;
while (n) {
if (node_dispatch(n, ui_event, state, &delta) > 0) {
break;
}
n = n->parent;
}
if (ui_event == UI_EVENT_BUTTON_LEFT_UPDATE) {
if (state) {
UINode *previously_pressed = window->pressed;
window->pressed = node;
if (previously_pressed) {
node_dispatch(previously_pressed, UI_EVENT_UNPRESSED, 0, NULL);
}
node_dispatch(node, UI_EVENT_PRESSED, 0, NULL);
} else {
UINode *previously_pressed = window->pressed;
window->pressed = NULL;
if (previously_pressed) {
node_dispatch(previously_pressed, UI_EVENT_UNPRESSED, 0, NULL);
}
}
}
}
break;
}
default: {
break;
}
}
end_clock();
}
int window_turn(UIWindow *window)
{
if (!window || !window->root || !window->_xcb_connection || !window->_cairo_surface || !window->drw) {
return -1;
}
if (xcb_connection_has_error(window->_xcb_connection) != 0) {
return -1;
}
if (cairo_surface_status(window->_cairo_surface) != CAIRO_STATUS_SUCCESS || cairo_status(window->drw) != CAIRO_STATUS_SUCCESS) {
return -1;
}
bool has_looped_once = false;
struct pollfd fds[UI_WINDOW_MAX_POLL] = {0};
for (int i = 0; i < UI_WINDOW_MAX_POLL; i++) {
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
fds[0].fd = xcb_get_file_descriptor(window->_xcb_connection);
fds[0].events = POLLIN;
uint64_t frame_peak_ms = 0;
uint64_t frame_peak_last_measurement_ms = 0;
for (;;) {
int64_t poll_timeout;
/* compute `poll_timeout` based on active timers */
{
uint64_t now = time_current_ms();
int64_t lowest = 0;
bool has_timer = false;
for (int i = 0; i < UI_WINDOW_MAX_TIMERS; i++) {
UIWindowTimer *timer = &window->timers[i];
if (timer->present) {
has_timer = true;
int64_t distance = (timer->started_at_ms + timer->duration_ms) - now;
if (distance < lowest) {
lowest = distance;
}
}
}
if (lowest < 0) {
lowest = 0;
}
poll_timeout = has_timer ? lowest : -1;
if (!has_looped_once) {
// Makes sure that we handle any pending events before entering the loop for the first time
poll_timeout = 0;
}
}
{
if (poll(fds, UI_WINDOW_MAX_POLL, poll_timeout) < 0) {
fprintf(stderr, "err: window_turn(): poll() failed\n");
return -1;
}
}
clear_summary();
uint64_t frame_start_ms = time_current_ms();
for (int i = 0; i < UI_WINDOW_MAX_POLL; i++) {
if (fds[i].revents & POLLNVAL) {
fprintf(stderr, "err: window_turn(): poll got POLLNVAL\n");
return -1;
}
if (fds[i].revents & POLLERR) {
fprintf(stderr, "err: window_turn(): poll got POLLERR\n");
return -1;
}
if (fds[i].revents & POLLHUP) {
fprintf(stderr, "err: window_turn(): poll got POLLHUP\n");
return -1;
}
if ((i == 0 && fds[i].revents & POLLIN) || !has_looped_once) {
xcb_generic_event_t *xcb_event;
while ((xcb_event = xcb_poll_for_event(window->_xcb_connection))) {
_window_process_xcb_event(window, xcb_event);
free(xcb_event);
}
}
}
/* process finished timers */
{
for (int j = 0; j < UI_WINDOW_MAX_TIMERS; j++) {
UIWindowTimer *timer = &window->timers[j];
if (timer->present) {
uint64_t now = time_current_ms();
int64_t distance = (timer->started_at_ms + timer->duration_ms) - now;
if (distance <= 0) {
uint64_t dt = time_current_ms() - timer->started_at_ms;
UINode *target = timer->target;
timer->present = false;
timer->started_at_ms = 0;
timer->duration_ms = 0;
timer->target = NULL;
if (target) {
node_dispatch(target, UI_EVENT_TIMER_END, 0, &dt);
}
}
}
}
}
begin_clock("Flush invalidated widgets");
if (window_flush_invalidated(window)) {
begin_clock("Flush painting results to XCB");
xcb_flush(window->_xcb_connection);
end_clock();
}
end_clock();
has_looped_once = true;
uint64_t frame_end_ms = time_current_ms();
uint64_t frame_delta_ms = frame_end_ms - frame_start_ms;
if (frame_delta_ms > frame_peak_ms || frame_end_ms - frame_peak_last_measurement_ms >= 3000) {
frame_peak_last_measurement_ms = frame_end_ms;
frame_peak_ms = frame_delta_ms;
printf("peak frametime: %ldms\n", frame_peak_ms);
}
dump_summary();
}
return 0;
}
UIWindowTimer *window_sched_timer(UIWindow *window, UINode *target, uint64_t duration_ms)
{
for (int i = 0; i < UI_WINDOW_MAX_TIMERS; i++) {
if (!window->timers[i].present) {
UIWindowTimer *timer = &window->timers[i];
timer->present = true;
timer->duration_ms = duration_ms;
timer->target = target;
timer->started_at_ms = time_current_ms();
return timer;
}
}
return NULL;
}

44
src/window.h Normal file
View file

@ -0,0 +1,44 @@
#ifndef _UI__WINDOW_H
#define _UI__WINDOW_H
#include "color.h"
#include "rect.h"
#include "node.h"
#include <xcb/xcb.h>
#include <xcb/xproto.h>
#include <cairo/cairo.h>
#include <cairo/cairo-xcb.h>
#include <pango/pangocairo.h>
#include <pango/pango-layout.h>
typedef struct UIWindowTimer {
bool present;
uint64_t started_at_ms;
uint64_t duration_ms;
UINode *target; // TODO: what if the node gets deleted while the timer is still running?
} UIWindowTimer;
#define UI_WINDOW_MAX_TIMERS 4
#define UI_WINDOW_MAX_POLL (1)
typedef struct UIWindow {
struct UINode *root, *hovered, *pressed;
UIRect invalid_region;
xcb_connection_t *_xcb_connection;
xcb_window_t _xcb_window_id;
cairo_surface_t *_cairo_surface;
cairo_t *drw;
UIWindowTimer timers[UI_WINDOW_MAX_TIMERS];
} UIWindow;
int window_invalidate_node(UIWindow *window, UINode *node);
void window_free(UIWindow *window);
UIWindow *window_new(int width, int height);
bool window_flush_invalidated(UIWindow *window);
int window_attach_root(UIWindow *window, UINode *root);
int window_turn(UIWindow *window);
UIWindowTimer *window_sched_timer(UIWindow *window, UINode *target, uint64_t duration_ms);
#endif // _UI__WINDOW_H