diff options
| author | Federico Angelilli <code@fedang.net> | 2024-07-08 16:41:06 +0200 |
|---|---|---|
| committer | Federico Angelilli <code@fedang.net> | 2024-07-08 16:41:06 +0200 |
| commit | 8e392c583c7c0b68bae6204bf98e7f8e077c5bbf (patch) | |
| tree | eec9e15918194a6f4bca9ce4de479783c0ddbada | |
| parent | 5d170a634ead0119f6e5a9f63c23b2b064126f75 (diff) | |
Rewrite display code
| -rw-r--r-- | src/any_log.h | 722 | ||||
| -rw-r--r-- | src/comet.c | 29 | ||||
| -rw-r--r-- | src/display.c | 168 | ||||
| -rw-r--r-- | src/display.h | 28 | ||||
| -rw-r--r-- | src/window.c | 296 | ||||
| -rw-r--r-- | src/window.h | 31 |
6 files changed, 1274 insertions, 0 deletions
diff --git a/src/any_log.h b/src/any_log.h new file mode 100644 index 0000000..c4f143c --- /dev/null +++ b/src/any_log.h @@ -0,0 +1,722 @@ +// any_log +// +// A single-file library that provides a simple and somewhat opinionated +// interface for logging and structured logging. +// +// To use this library you should choose a suitable file to put the +// implementation and define ANY_LOG_IMPLEMENT. For example +// +// #define ANY_LOG_IMPLEMENT +// #include "any_log.h" +// +// Additionally, you can customize the library behavior by defining certain +// macros in the file where you put the implementation. You can see which are +// supported by reading the code guarded by ANY_LOG_IMPLEMENT. +// +// This library is licensed under the terms of the MIT license. +// A copy of the license is included at the end of this file. +// + +#ifndef ANY_LOG_INCLUDE +#define ANY_LOG_INCLUDE + +#include <stdio.h> + +// These values represent the decreasing urgency of a log invocation. +// +// panic: indicates a fatal error and using it will result in +// the program termination (see any_log_panic) +// +// error: indicates a (non-fatal) error +// +// warn: indicates a warning +// +// info: indicates an information (potentially useful to the user) +// +// debug: indicates debugging information +// +// trace: indicates verbose debugging information and can be completely +// disabled by defining ANY_LOG_NO_TRACE before including +// +// NOTE: The value ANY_LOG_ALL is not an actual level and it is used as +// a sentinel to indicate the last value of any_log_level_t +// +typedef enum { + ANY_LOG_PANIC, + ANY_LOG_ERROR, + ANY_LOG_WARN, + ANY_LOG_INFO, + ANY_LOG_DEBUG, + ANY_LOG_TRACE, + ANY_LOG_ALL, +} any_log_level_t; + +// The value of ANY_LOG_MODULE is used to indicate the current module. +// By default it is defined as __FILE__, so that it will coincide with the +// source file path (relative to the compiler cwd). +// +// You can customize ANY_LOG_MODULE before including the header by simply +// defining it. For example +// +// #define ANY_LOG_MODULE "my-library" +// #include "any_log.h" +// +#ifndef ANY_LOG_MODULE +#define ANY_LOG_MODULE __FILE__ +#endif + +// C99 and later define the __func__ variable +#ifndef ANY_LOG_FUNC +#define ANY_LOG_FUNC __func__ +#endif + +// log_panic is implemented with the function any_log_panic, which takes +// some extra parameters compared with the other log levels. This way we can +// include as many information as possible for identifying fatal errors. +// +// You can change the format string and exit function in the implementation +// (see ANY_LOG_EXIT, ANY_LOG_PANIC_BEFORE and ANY_LOG_PANIC_AFTER). +// +// NOTE: log_panic will always terminate the program and should be used only +// for non recoverable situations! For normal errors just use log_error +// +#define log_panic(...) any_log_panic(__FILE__, __LINE__, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__) + +// log_[level] provide normal printf style logging. +// +// The logs will be filtered according to the global log level. See any_log_level. +// +// You should invoke log_[level] with a format string and any number of +// matched arguments. For example +// +// log_error("This is an error"); +// log_debug("The X is %d (padding %d)", X, 10); +// +// log_trace and log_debug can be disabled completely (to avoid their overhead +// in release/optimized builds) by defining ANY_LOG_NO_TRACE and ANY_LOG_NO_DEBUG +// respectively. As this will work only if they are defined before every header +// include, it is recommended to define this from the compiler. +// +#define log_error(...) any_log_format(ANY_LOG_ERROR, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__) +#define log_warn(...) any_log_format(ANY_LOG_WARN, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__) +#define log_info(...) any_log_format(ANY_LOG_INFO, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__) + +#ifdef ANY_LOG_NO_DEBUG +#define log_debug(...) +#else +#define log_debug(...) any_log_format(ANY_LOG_DEBUG, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__) +#endif + +#ifdef ANY_LOG_NO_TRACE +#define log_trace(...) +#else +#define log_trace(...) any_log_format(ANY_LOG_TRACE, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__) +#endif + +// log_value_[level] provide structured logging. +// +// The logs will be filtered according to the global log level. See any_log_level. +// +// You should always pass a message string (printf style specifiers are ignored) +// and some key-value pairs. +// +// The key are simply strings. It is advised to pass only literals for security. +// +// The value can be of type int, unsigned int, pointer (void *), double and +// string (char *). +// +// The value type is specified by a type specifier at the start of the key +// string and should be like so +// +// key = (type_specifier ANY_LOG_VALUE_TYPE_SEP)? ... +// +// By default ANY_LOG_VALUE_TYPE_SEP is the character ':'. +// +// type_specifier | type | default format +// | | +// b | bool (promoted to int) | "%s", b ? "true" : "false" +// d, i | int | "%d" +// x, u | unsigned int | "%#x" +// l | long int | "%ld" +// p | void * | "%p" +// f | double | "%lf" +// s | char * (0-terminated) | "%s" +// +// g | any_log_formatter_t (function) + ANY_LOG_VALUE_GENERIC_TYPE +// +// If no type specifier is given the function will assume the type given +// by ANY_LOG_VALUE_DEFAULT_TYPE (by default string). +// +// The 'g' specifier is handled differently than the others. It needs two parameters, +// the first must be a custom formatter function (of type any_log_formatter_t) to +// format the second value of type ANY_LOG_VALUE_GENERIC_TYPE (by default void *). +// By defining ANY_LOG_NO_GENERIC you can disable this custom type specifier. +// +// Example usage of value logging +// +// log_value_info("Created graphical context", +// "d:width", width, +// "d:height", height, +// "p:window", window_handle, +// "f:scale", scale_factor_dpi, +// "b:hidden", visibility == HIDDEN, +// "g:widgets", ANY_LOG_FORMATTER(widget_format), widgets, +// "appname", "nice app"); +// +// In the implementation you can customize the format of every key-value pair +// and of the message. This is useful if you want to adhere to a structured +// logging format like JSON. For example +// +// #define ANY_LOG_IMPLEMENT +// #define ANY_LOG_VALUE_BEFORE(level, module, func, message) +// "{\"module\": \"%s\", \"function\": \"%s\", \"level\": \"%s\", \"message\": \"%s\", ", +// module, func, any_log_level_strings[level], message +// +// #define ANY_LOG_VALUE_BOOL(key, value) "\"%s\": %s", key, (value ? "true" : "false") +// #define ANY_LOG_VALUE_INT(key, value) "\"%s\": %d", key, value +// #define ANY_LOG_VALUE_HEX(key, value) "\"%s\": %u", key, value +// #define ANY_LOG_VALUE_LONG(key, value) "\"%s\": %ld", key, value +// #define ANY_LOG_VALUE_PTR(key, value) "\"%s\": \"%p\"", key, value +// #define ANY_LOG_VALUE_DOUBLE(key, value) "\"%s\": %lf", key, value +// #define ANY_LOG_VALUE_STRING(key, value) "\"%s \": \"%s\"", key, value +// #define ANY_LOG_VALUE_AFTER(level, module, func, message) "}\n" +// #define ANY_LOG_NO_GENERIC +// #include "any_log.h" +// +// As with log_trace and log_debug, log_value_trace and log_value_debug can be +// disabled by defining ANY_LOG_NO_TRACE and ANY_LOG_NO_DEBUG respectively. +// +#define log_value_error(...) any_log_value(ANY_LOG_ERROR, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__, (char *)NULL) +#define log_value_warn(...) any_log_value(ANY_LOG_WARN, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__, (char *)NULL) +#define log_value_info(...) any_log_value(ANY_LOG_INFO, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__, (char *)NULL) +#define log_value_debug(...) any_log_value(ANY_LOG_DEBUG, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__, (char *)NULL) + +#ifdef ANY_LOG_NO_DEBUG +#define log_value_debug(...) +#else +#define log_value_debug(...) any_log_value(ANY_LOG_DEBUG, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__, (char *)NULL) +#endif + +#ifdef ANY_LOG_NO_TRACE +#define log_value_trace(...) +#else +#define log_value_trace(...) any_log_value(ANY_LOG_TRACE, ANY_LOG_MODULE, ANY_LOG_FUNC, __VA_ARGS__, (char *)NULL) +#endif + +#ifndef ANY_LOG_NO_GENERIC + +#ifndef ANY_LOG_VALUE_GENERIC_TYPE +#define ANY_LOG_VALUE_GENERIC_TYPE void * +#endif + +// The type of the format functions for custom types +// +typedef void (*any_log_formatter_t)(FILE *stream, ANY_LOG_VALUE_GENERIC_TYPE value); + +#define ANY_LOG_FORMATTER(f) ((any_log_formatter_t)(f)) + +#endif + +#ifdef __GNUC__ +#define ANY_LOG_ATTRIBUTE(...) __attribute__((__VA_ARGS__)) +#else +#define ANY_LOG_ATTRIBUTE(...) +#endif + +// All log functions will output to the file stream specified by any_log_stream. +// +// You should always set this global to a valid stream (eg in main) before +// invoking any_log macros or functions! +// +extern FILE *any_log_stream; + +// All log functions will ignore the message if the level is below the +// threshold specified in any_log_level. +// +// To modify the log level you can assign a any_log_level_t to this global. +// +// By default it has value ANY_LOG_LEVEL_DEFAULT (see implementation). +// +extern any_log_level_t any_log_level; + +// This is a simple utility function that sets both any_log_level and +// any_log_stream with a single call. +// +// Call this function before any use of log_* (for example in main) to +// correctly initialize the library! +// +void any_log_init(FILE *stream, any_log_level_t level); + +// An array containing the strings corresponding to the log levels. +// +// Can be modified in the implementation by defining the macros ANY_LOG_[level]_STRING. +// +// The functions any_log_level_to_string and any_log_level_from_string are +// provided for easy conversion. +// +extern const char *any_log_level_strings[ANY_LOG_ALL]; + +ANY_LOG_ATTRIBUTE(pure) +const char *any_log_level_to_string(any_log_level_t level); + +ANY_LOG_ATTRIBUTE(pure) +any_log_level_t any_log_level_from_string(const char *string); + +// The default format macros for all logging function uses the global +// any_log_color to get the color sequence to use when printing the logs. +// +// By default this global points to any_log_colors_default, but you can +// set it to any_log_colors_disabled or to a custom array of your choice. +// +// The array you give should have length ANY_LOG_ALL + 3 and this organization +// +// from ANY_LOG_PANIC to ANY_LOG_TRACE: the colors indexed by log levels +// ANY_LOG_ALL: the color reset sequence +// ANY_LOG_ALL + 1: the color for the module +// ANY_LOG_ALL + 2: the color for the function +// +// NOTE: If you changed the default format in the implementation +// (by redefining ANY_LOG_FORMAT_*, ANY_LOG_VALUE_* and ANY_LOG_PANIC_*), +// you can safely ignore these variables and use whatever method you +// prefer to get the colors. +// +extern const char **any_log_colors; + +// This array contains the default colors. +// +// See ANY_LOG_[level]_COLOR, ANY_LOG_RESET_COLOR, ANY_LOG_MODULE_COLOR and +// ANY_LOG_FUNC_COLOR in the implementation. +// +extern const char *any_log_colors_default[ANY_LOG_ALL + 3]; + +// This array contains empty strings. +extern const char *any_log_colors_disabled[ANY_LOG_ALL + 3]; + +// NOTE: You should never call the functions below directly! +// See the above explanations on how to use logging. + +ANY_LOG_ATTRIBUTE(format(printf, 4, 5)) +ANY_LOG_ATTRIBUTE(nonnull(4)) +void any_log_format(any_log_level_t level, const char *module, + const char *func, const char *format, ...); + +ANY_LOG_ATTRIBUTE(nonnull(4)) +void any_log_value(any_log_level_t level, const char *module, + const char *func, const char *message, ...); + +ANY_LOG_ATTRIBUTE(noreturn) +ANY_LOG_ATTRIBUTE(format(printf, 5, 6)) +ANY_LOG_ATTRIBUTE(nonnull(1, 4)) +void any_log_panic(const char *file, int line, const char *module, + const char *func, const char *format, ...); + +#endif + +#ifdef ANY_LOG_IMPLEMENT + +#include <stdarg.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +// For the C standard we can't assign stdout or any other streams here, +// since they are not constant. +// +// Thus it is imperative to set this variable to a valid FILE * before using it! +// +FILE *any_log_stream = NULL; + +// The default value for any_log_level +#ifndef ANY_LOG_LEVEL_DEFAULT +#define ANY_LOG_LEVEL_DEFAULT ANY_LOG_INFO +#endif + +any_log_level_t any_log_level = ANY_LOG_LEVEL_DEFAULT; + +// Utility function to initialize the library +void any_log_init(FILE *stream, any_log_level_t level) +{ + any_log_stream = stream; + any_log_level = level; +} + +// Log level strings +#ifndef ANY_LOG_PANIC_STRING +#define ANY_LOG_PANIC_STRING "panic" +#endif +#ifndef ANY_LOG_ERROR_STRING +#define ANY_LOG_ERROR_STRING "error" +#endif +#ifndef ANY_LOG_WARN_STRING +#define ANY_LOG_WARN_STRING "warn" +#endif +#ifndef ANY_LOG_INFO_STRING +#define ANY_LOG_INFO_STRING "info" +#endif +#ifndef ANY_LOG_DEBUG_STRING +#define ANY_LOG_DEBUG_STRING "debug" +#endif +#ifndef ANY_LOG_TRACE_STRING +#define ANY_LOG_TRACE_STRING "trace" +#endif + +const char *any_log_level_strings[ANY_LOG_ALL] = { + ANY_LOG_PANIC_STRING, + ANY_LOG_ERROR_STRING, + ANY_LOG_WARN_STRING, + ANY_LOG_INFO_STRING , + ANY_LOG_DEBUG_STRING, + ANY_LOG_TRACE_STRING, +}; + +const char *any_log_level_to_string(any_log_level_t level) +{ + return level >= ANY_LOG_PANIC && level <= ANY_LOG_TRACE + ? any_log_level_strings[level] : ""; +} + +any_log_level_t any_log_level_from_string(const char *string) +{ + for (int level = ANY_LOG_PANIC; level < ANY_LOG_ALL; level++) { + if (strcmp(any_log_level_strings[level], string) == 0) + return (any_log_level_t)level; + } + + return ANY_LOG_ALL; +} + +// These colors related variables are provided just to provide a uniform +// interface for setting the colors. If you decide to change the default +// log format macros, feel free to ignore all this variables. +// +const char **any_log_colors = any_log_colors_default; + +// Log colors indexed by log level, with the addition of special colors +// for func, module and reset sequence. +// +#ifndef ANY_LOG_PANIC_COLOR +#define ANY_LOG_PANIC_COLOR "\x1b[1;91m" +#endif +#ifndef ANY_LOG_ERROR_COLOR +#define ANY_LOG_ERROR_COLOR "\x1b[31m" +#endif +#ifndef ANY_LOG_WARN_COLOR +#define ANY_LOG_WARN_COLOR "\x1b[1;33m" +#endif +#ifndef ANY_LOG_INFO_COLOR +#define ANY_LOG_INFO_COLOR "\x1b[1;96m" +#endif +#ifndef ANY_LOG_DEBUG_COLOR +#define ANY_LOG_DEBUG_COLOR "\x1b[1;37m" +#endif +#ifndef ANY_LOG_TRACE_COLOR +#define ANY_LOG_TRACE_COLOR "\x1b[1;90m" +#endif +#ifndef ANY_LOG_RESET_COLOR +#define ANY_LOG_RESET_COLOR "\x1b[0m" +#endif +#ifndef ANY_LOG_MODULE_COLOR +#define ANY_LOG_MODULE_COLOR "" +#endif +#ifndef ANY_LOG_FUNC_COLOR +#define ANY_LOG_FUNC_COLOR "\x1b[1m" +#endif + +const char *any_log_colors_default[ANY_LOG_ALL + 3] = { + ANY_LOG_PANIC_COLOR, + ANY_LOG_ERROR_COLOR, + ANY_LOG_WARN_COLOR, + ANY_LOG_INFO_COLOR , + ANY_LOG_DEBUG_COLOR, + ANY_LOG_TRACE_COLOR, + ANY_LOG_RESET_COLOR, + ANY_LOG_MODULE_COLOR, + ANY_LOG_FUNC_COLOR +}; + +const char *any_log_colors_disabled[ANY_LOG_ALL + 3] = { + "", "", "", "", "", "", "", +}; + +// Format for any_log_format (used at the start) +#ifndef ANY_LOG_FORMAT_BEFORE +#define ANY_LOG_FORMAT_BEFORE(level, module, func) \ + "[%s%s%s %s%s%s] %s%s%s: ", any_log_colors[ANY_LOG_ALL + 1], module, any_log_colors[ANY_LOG_ALL], any_log_colors[ANY_LOG_ALL + 2], \ + func, any_log_colors[ANY_LOG_ALL], any_log_colors[level], any_log_level_strings[level], any_log_colors[ANY_LOG_ALL] +#endif + +// Format for any_log_format (used at the end) +#ifndef ANY_LOG_FORMAT_AFTER +#define ANY_LOG_FORMAT_AFTER(level, module, func) "\n" +#endif + +void any_log_format(any_log_level_t level, const char *module, + const char *func, const char *format, ...) +{ + if (level > any_log_level) + return; + + fprintf(any_log_stream, ANY_LOG_FORMAT_BEFORE(level, module, func)); + + va_list args; + va_start(args, format); + vfprintf(any_log_stream, format, args); + va_end(args); + + fprintf(any_log_stream, ANY_LOG_FORMAT_AFTER(level, module, func)); + + // NOTE: Suppress compiler warning if the user customizes the format string + // and doesn't use these values in it + (void)module; + (void)func; +} + +// This is used in the parsing of the type specifier from the key +// +// NOTE: It must be a character +#ifndef ANY_LOG_VALUE_TYPE_SEP +#define ANY_LOG_VALUE_TYPE_SEP ':' +#endif + +// Format for any_log_value (used at the start) +#ifndef ANY_LOG_VALUE_BEFORE +#define ANY_LOG_VALUE_BEFORE(level, module, func, message) \ + "[%s%s%s %s%s%s] %s%s%s: %s [", any_log_colors[ANY_LOG_ALL + 1], module, any_log_colors[ANY_LOG_ALL], any_log_colors[ANY_LOG_ALL + 2], \ + func, any_log_colors[ANY_LOG_ALL], any_log_colors[level], any_log_level_strings[level], any_log_colors[ANY_LOG_ALL], message +#endif + +// Format for any_log_value (used at the end) +#ifndef ANY_LOG_VALUE_AFTER +#define ANY_LOG_VALUE_AFTER(level, module, func, message) "]\n" +#endif + +// Format for pairs with a bool value +// +// NOTE: C automatically promotes boolean types to int +#ifndef ANY_LOG_VALUE_BOOL +#define ANY_LOG_VALUE_BOOL(key, value) "%s=%s", key, (value ? "true" : "false") +#endif + +// Format for pairs with an int value +#ifndef ANY_LOG_VALUE_INT +#define ANY_LOG_VALUE_INT(key, value) "%s=%d", key, value +#endif + +// Format for pairs with an unsinged int value (hex by default) +#ifndef ANY_LOG_VALUE_HEX +#define ANY_LOG_VALUE_HEX(key, value) "%s=%#x", key, value +#endif + +// Format for pairs with a long int value +#ifndef ANY_LOG_VALUE_LONG +#define ANY_LOG_VALUE_LONG(key, value) "%s=%ld", key, value +#endif + +// Format for pairs with a pointer value +#ifndef ANY_LOG_VALUE_PTR +#define ANY_LOG_VALUE_PTR(key, value) "%s=%p", key, value +#endif + +// Format for pairs with a double value +#ifndef ANY_LOG_VALUE_DOUBLE +#define ANY_LOG_VALUE_DOUBLE(key, value) "%s=%lf", key, value +#endif + +// Format for pairs with a string value +#ifndef ANY_LOG_VALUE_STRING +#define ANY_LOG_VALUE_STRING(key, value) "%s=\"%s\"", key, value +#endif + +#ifndef ANY_LOG_NO_GENERIC + +// Format custom types with the given formatter function +#ifndef ANY_LOG_VALUE_GENERIC +#define ANY_LOG_VALUE_GENERIC(key, stream, formatter, value) \ + do { \ + fprintf(stream, "%s=", key); \ + formatter(stream, value); \ + } while (false) +#endif + +#endif + +// The default is to use string +#ifndef ANY_LOG_VALUE_DEFAULT +#define ANY_LOG_VALUE_DEFAULT(key, value) ANY_LOG_VALUE_STRING(key, value) +#define ANY_LOG_VALUE_DEFAULT_TYPE char * +#endif + +// This is used as a separator between different pairs +#ifndef ANY_LOG_VALUE_PAIR_SEP +#define ANY_LOG_VALUE_PAIR_SEP ", " +#endif + +void any_log_value(any_log_level_t level, const char *module, + const char *func, const char *message, ...) +{ + if (level > any_log_level) + return; + + fprintf(any_log_stream, ANY_LOG_VALUE_BEFORE(level, module, func, message)); + + va_list args; + va_start(args, message); + + // NOTE: This function should be called with at least one pair + char *key = va_arg(args, char *); + + while (key != NULL) { + if (key[0] != '\0' && key[1] == ANY_LOG_VALUE_TYPE_SEP) { + key += 2; + switch (tolower(key[-2])) { + case 'b': { + int value = va_arg(args, int); + fprintf(any_log_stream, ANY_LOG_VALUE_BOOL(key, value)); + break; + } + + case 'd': + case 'i': { + int value = va_arg(args, int); + fprintf(any_log_stream, ANY_LOG_VALUE_INT(key, value)); + break; + } + + case 'x': + case 'u': { + unsigned int value = va_arg(args, unsigned int); + fprintf(any_log_stream, ANY_LOG_VALUE_HEX(key, value)); + break; + } + + case 'l': { + long int value = va_arg(args, long int); + fprintf(any_log_stream, ANY_LOG_VALUE_LONG(key, value)); + break; + } + + case 'p': { + void *value = va_arg(args, void *); + fprintf(any_log_stream, ANY_LOG_VALUE_PTR(key, value)); + break; + } + + case 'f': { + double value = va_arg(args, double); + fprintf(any_log_stream, ANY_LOG_VALUE_DOUBLE(key, value)); + break; + } + + case 's': { + char *value = va_arg(args, char *); + fprintf(any_log_stream, ANY_LOG_VALUE_STRING(key, value)); + break; + } + +#ifndef ANY_LOG_NO_GENERIC + case 'g': { + any_log_formatter_t formatter = va_arg(args, any_log_formatter_t); + ANY_LOG_VALUE_GENERIC_TYPE value = va_arg(args, ANY_LOG_VALUE_GENERIC_TYPE); + ANY_LOG_VALUE_GENERIC(key, any_log_stream, formatter, value); + break; + } +#endif + default: + goto tdefault; + } + } else { +tdefault: + ANY_LOG_VALUE_DEFAULT_TYPE value = va_arg(args, ANY_LOG_VALUE_DEFAULT_TYPE); + fprintf(any_log_stream, ANY_LOG_VALUE_DEFAULT(key, value)); + } + + key = va_arg(args, char *); + if (key == NULL) + break; + + fprintf(any_log_stream, ANY_LOG_VALUE_PAIR_SEP); + } + + va_end(args); + fprintf(any_log_stream, ANY_LOG_VALUE_AFTER(level, module, func, message)); + + (void)module; + (void)func; + (void)message; +} + +// Using log_panic results in a call to any_log_panic, which should terminate +// the program. The value of ANY_LOG_EXIT is used to specify an action to +// take at the end of the aforementioned function. +// By default it is abort +// +// NOTE: This function should never return! +// +#ifndef ANY_LOG_EXIT +#define ANY_LOG_EXIT(file, line, module, func) abort() +#endif + +// Format for any_log_panic (used at the start) +#ifndef ANY_LOG_PANIC_BEFORE +#define ANY_LOG_PANIC_BEFORE(file, line, module, func) \ + "[%s%s%s %s%s%s] %s%s%s: ", any_log_colors[ANY_LOG_ALL + 1], module, any_log_colors[ANY_LOG_ALL], any_log_colors[ANY_LOG_ALL + 2], \ + func, any_log_colors[ANY_LOG_ALL], any_log_colors[ANY_LOG_PANIC], any_log_level_strings[ANY_LOG_PANIC], any_log_colors[ANY_LOG_ALL] +#endif + +// Format for any_log_panic (used at the start) +#ifndef ANY_LOG_PANIC_AFTER +#define ANY_LOG_PANIC_AFTER(file, line, module, func) \ + "\n%spanic was invoked from%s %s:%d (%s%s%s)\n", any_log_colors[ANY_LOG_PANIC], any_log_colors[ANY_LOG_ALL], \ + file, line, any_log_colors[ANY_LOG_ALL + 1], module, any_log_colors[ANY_LOG_ALL] +#endif + +// NOTE: This function *exceptionally* gets more location information +// because we want to be specific at least for fatal errors +// +void any_log_panic(const char *file, int line, const char *module, + const char *func, const char *format, ...) +{ + fprintf(any_log_stream, ANY_LOG_PANIC_BEFORE(file, line, module, func)); + + va_list args; + va_start(args, format); + vfprintf(any_log_stream, format, args); + va_end(args); + + fprintf(any_log_stream, ANY_LOG_PANIC_AFTER(file, line, module, func)); + + (void)module; + (void)func; + (void)file; + (void)line; + + ANY_LOG_EXIT(file, line, module, func); + + // In a way or another, this function shall not return + abort(); +} + +#endif + +// MIT License +// +// Copyright (c) 2024 Federico Angelilli +// +// 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: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// diff --git a/src/comet.c b/src/comet.c new file mode 100644 index 0000000..72bc3b5 --- /dev/null +++ b/src/comet.c @@ -0,0 +1,29 @@ +#include <stdlib.h> +#include <locale.h> + +#include "window.h" + +#define ANY_LOG_IMPLEMENT +#include "any_log.h" + +int main(int argc, char **argv) +{ + setlocale(LC_CTYPE, ""); + any_log_init(stdout, ANY_LOG_DEBUG); + + display_t display; + display_init(&display); + + window_t window; + window_init(&window, &display); + + window_move(&window, 100, 100); + window_resize(&window, 1000, 1000); + + while (true); + + window_close(&window); + display_close(&display); + + return 0; +} diff --git a/src/display.c b/src/display.c new file mode 100644 index 0000000..09549ce --- /dev/null +++ b/src/display.c @@ -0,0 +1,168 @@ +#include <stdlib.h> +#include <stdbool.h> +#include <assert.h> +#include <string.h> + +#include "any_log.h" +#include "display.h" + +static bool query_xrm(display_t *display, const char *resource, char **value) +{ + bool found = xcb_xrm_resource_get_string(display->database, resource, NULL, value) >= 0; + log_value_debug("Xrm query", + "b:found", found, + "s:resource", resource, + "s:value", *value ? *value : ""); + return found; +} + +static void update_xrm(display_t *display) +{ + xcb_flush(display->connection); + xcb_xrm_database_t *database = xcb_xrm_database_from_default(display->connection); + + if (database == NULL) { + log_warn("Xrm database couldn't be updated"); + return; + } + + xcb_xrm_database_free(display->database); + display->database = database; + log_debug("Xrm database updated"); +} + +static void update_scale(display_t *display) +{ + char *dpi_value; + if (query_xrm(display, "Xft.dpi", &dpi_value)) { + display->screen_dpi = strtod(dpi_value, NULL); + free(dpi_value); + + // Ignore invalid values + if (display->screen_dpi != 0) + return; + } + + display->screen_dpi = (double)display->screen_size->height * 25.4 / (double)display->screen_size->mheight; + log_debug("Fallback dpi value '%.2lf'", display->screen_dpi); +} + +// Check if point (px, py) is inside a rectangle in (x, y), (x+w, y), (x, y+h) and (w+h, y+h) +static inline bool in_rect(int px, int py, int x, int y, int w, int h) +{ + return px >= x && px <= x + w && py >= y && py <= y + h; +} + +// Check if point (px, py) is inside a circle of radius r and center (x, y) +static inline bool in_circle(int px, int py, int x, int y, int r) +{ + int dx = x - px; + int dy = y - py; + return (dx * dx + dy * dy) <= r * r; +} + +// Check if point (px, py) is inside a capsule in (x, y), (x+w, y), (x, y+h) and (w+h, y+h) +static inline bool in_capsule(int px, int py, int x, int y, int w, int h) +{ + assert(w >= h); + int radius = h / 2; + + // Trivial case + if (w == h) + return in_circle(px, py, x + radius, y + radius, radius); + + // General case + return in_circle(px, py, x + radius, y + radius, radius) + || in_circle(px, py, x + w - radius, y + radius, radius) + || in_rect(px, py, x + radius, y, w - 2 * radius, h); +} + +void display_init(display_t *display) +{ + memset(display, 0, sizeof(display_t)); + + int preferred_screen = 0; + display->connection = xcb_connect(NULL, &preferred_screen); + assert(display->connection != NULL && !xcb_connection_has_error(display->connection)); + + log_value_debug("Xcb connection established", + "i:preferred_screen", preferred_screen); + + xcb_screen_iterator_t iter = xcb_setup_roots_iterator(xcb_get_setup(display->connection)); + while (preferred_screen != 0 && iter.rem) { + xcb_screen_next(&iter); + preferred_screen--; + } + + display->screen = iter.data; + xcb_generic_error_t *error; + + xcb_randr_query_version_cookie_t version_cookie = xcb_randr_query_version(display->connection, + XCB_RANDR_MAJOR_VERSION, + XCB_RANDR_MINOR_VERSION); + + xcb_randr_query_version_reply_t *randr_version = xcb_randr_query_version_reply(display->connection, + version_cookie, + &error); + + assert(error == NULL); + log_value_debug("RandR loaded", + "i:major", randr_version->major_version, + "i:minor", randr_version->minor_version); + + assert(randr_version->major_version >= 1); + free(randr_version); + + xcb_randr_get_screen_info_cookie_t cookie = xcb_randr_get_screen_info(display->connection, display->screen->root); + display->info_reply = xcb_randr_get_screen_info_reply(display->connection, cookie, &error); + assert(error == NULL); + + display->screen_size = xcb_randr_get_screen_info_sizes(display->info_reply); + assert(display->screen_size != NULL); + log_debug("Display size [width=%d, height=%d]", display->screen_size->width, display->screen_size->height); + + xcb_randr_select_input(display->connection, + display->screen->root, + XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE | + XCB_RANDR_NOTIFY_MASK_OUTPUT_CHANGE | + XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY); + + log_debug("Searching 32 bit visual"); + display->visual_type = xcb_aux_find_visual_by_attrs(display->screen, XCB_VISUAL_CLASS_TRUE_COLOR, 32); + display->screen_depth = 32; + + if (display->visual_type == NULL) { + // NOTE: 24 bit visuals don't have the alpha channel for transparency + log_debug("Falling back to 24 bit visual"); + display->visual_type = xcb_aux_find_visual_by_attrs(display->screen, XCB_VISUAL_CLASS_TRUE_COLOR, 24); + display->screen_depth = 24; + } + + assert(display->visual_type != 0); + + xcb_intern_atom_cookie_t *ewmh_cookie = xcb_ewmh_init_atoms(display->connection, &display->ewmh); + assert(xcb_ewmh_init_atoms_replies(&display->ewmh, ewmh_cookie, &error)); + + assert(error == NULL); + log_debug("Xcb EWMH initialized"); + + display->database = xcb_xrm_database_from_default(display->connection); + assert(display->database != NULL); + + // TODO: Dpi aware scaling + update_scale(display); + + assert(xcb_errors_context_new(display->connection, &display->errors) == 0); + log_debug("Xcb errors loaded"); + + xcb_flush(display->connection); +} + +void display_close(display_t *display) +{ + xcb_ewmh_connection_wipe(&display->ewmh); + xcb_errors_context_free(display->errors); + xcb_xrm_database_free(display->database); + xcb_disconnect(display->connection); + free(display->info_reply); +} diff --git a/src/display.h b/src/display.h new file mode 100644 index 0000000..8d99cad --- /dev/null +++ b/src/display.h @@ -0,0 +1,28 @@ +#ifndef COMET_DISPLAY_H +#define COMET_DISPLAY_H + +#include <xcb/xcb.h> +#include <xcb/xcb_aux.h> +#include <xcb/xcb_ewmh.h> +#include <xcb/xcb_xrm.h> +#include <xcb/xcb_errors.h> +#include <xcb/randr.h> + +typedef struct { + xcb_connection_t *connection; + xcb_screen_t *screen; + xcb_randr_get_screen_info_reply_t *info_reply; + xcb_randr_screen_size_t *screen_size; + double screen_dpi; + int screen_depth; + xcb_visualtype_t *visual_type; + xcb_xrm_database_t *database; + xcb_errors_context_t *errors; + xcb_ewmh_connection_t ewmh; +} display_t; + +void display_init(display_t *display); + +void display_close(display_t *display); + +#endif diff --git a/src/window.c b/src/window.c new file mode 100644 index 0000000..e45eb27 --- /dev/null +++ b/src/window.c @@ -0,0 +1,296 @@ +#include <string.h> +#include <math.h> +#include <assert.h> +#include <unistd.h> + +#include <cairo.h> +#include <cairo-xcb.h> +#include <xcb/xcb.h> +#include <xcb/xproto.h> +#include <xcb/xcb_aux.h> +#include <xcb/xcb_icccm.h> +#include <xcb/xcb_ewmh.h> +#include <xcb/xcb_xrm.h> +#include <xcb/xcb_errors.h> +#include <xcb/randr.h> +#include <xcb/shape.h> + +#include "any_log.h" +#include "window.h" + +static xcb_atom_t intern_atom(window_t *window, const char *atom) +{ + xcb_generic_error_t *error; + xcb_intern_atom_cookie_t cookie = xcb_intern_atom(window->display->connection, false, strlen(atom), atom); + xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(window->display->connection, cookie, &error); + + assert(error == NULL); + xcb_atom_t id = reply->atom; + free(reply); + return id; +} + +static void wm_set_size(window_t *window) +{ + xcb_size_hints_t hints = { 0 }; + xcb_icccm_size_hints_set_size(&hints, false, window->width, window->height); + xcb_icccm_size_hints_set_min_size(&hints, window->width, window->height); + xcb_icccm_size_hints_set_max_size(&hints, window->width, window->height); + xcb_icccm_size_hints_set_base_size(&hints, window->width, window->height); + xcb_icccm_size_hints_set_position(&hints, false, window->x, window->y); + + xcb_icccm_set_wm_size_hints(window->display->connection, window->window, XCB_ATOM_WM_NORMAL_HINTS, &hints); + log_debug("Xcb icccm size hints updated"); +} + +static void wm_set_struts(window_t *window) +{ + const long end = window->x + window->width - 1; + uint32_t values[12] = { 0 }; + + values[2] = window->height; // top + values[8] = window->x; // top y0 + values[9] = end > 0 ? end : 0; // top y1 + + xcb_change_property(window->display->connection, + XCB_PROP_MODE_REPLACE, + window->window, + window->display->ewmh._NET_WM_STRUT, + XCB_ATOM_CARDINAL, + 32, + 4, + values); + + xcb_change_property(window->display->connection, + XCB_PROP_MODE_REPLACE, + window->window, + window->display->ewmh._NET_WM_STRUT_PARTIAL, + XCB_ATOM_CARDINAL, + 32, + 12, + values); + + log_debug("Xcb EWMH struts updated"); +} + +static void wm_setup(window_t *window) +{ + const char *title = "comet"; + xcb_icccm_set_wm_name(window->display->connection, window->window, XCB_ATOM_STRING, 8, strlen(title), title); + + const char class[] = "comet\0Comet"; + xcb_icccm_set_wm_class(window->display->connection, window->window, strlen(class), class); + + log_value_debug("Updated window information", + "s:title", title, + "s:class", "comet\\0Comet"); + + xcb_generic_error_t *error; + xcb_intern_atom_cookie_t *ewmh_cookie = xcb_ewmh_init_atoms(window->display->connection, &window->display->ewmh); + + assert(xcb_ewmh_init_atoms_replies(&window->display->ewmh, ewmh_cookie, &error)); + assert(error == NULL); + log_debug("Xcb EWMH connected"); + + xcb_ewmh_set_wm_window_type(&window->display->ewmh, window->window, 1, &window->display->ewmh._NET_WM_WINDOW_TYPE_DOCK); + xcb_ewmh_set_wm_desktop(&window->display->ewmh, window->window, 0xFFFFFFFF); + xcb_ewmh_set_wm_pid(&window->display->ewmh, window->window, getpid()); + + xcb_atom_t state[] = { + window->display->ewmh._NET_WM_STATE_STICKY, + window->display->ewmh._NET_WM_STATE_ABOVE + }; + xcb_ewmh_set_wm_state(&window->display->ewmh, window->window, 2, state); + + // TODO: These should be updated depdending on the situation! + wm_set_size(window); + wm_set_struts(window); +} + +void window_init(window_t *window, display_t *display) +{ + memset(window, 0, sizeof(window_t)); + window->display = display; + + // Set mask and params for CreateWindow + window->cw_mask = XCB_CW_BACK_PIXMAP | XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL + | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP; + + uint32_t event_mask = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY + | XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_FOCUS_CHANGE + | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS; + //| XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_POINTER_MOTION_HINT; + + xcb_colormap_t colormap = xcb_generate_id(display->connection); + xcb_create_colormap(display->connection, XCB_COLORMAP_ALLOC_NONE, colormap, + display->screen->root, display->visual_type->visual_id); + + XCB_AUX_ADD_PARAM(&window->cw_mask, &window->cw_params, back_pixmap, XCB_NONE); + XCB_AUX_ADD_PARAM(&window->cw_mask, &window->cw_params, back_pixel, 0x00000000); + XCB_AUX_ADD_PARAM(&window->cw_mask, &window->cw_params, border_pixel, 0x00000000); + XCB_AUX_ADD_PARAM(&window->cw_mask, &window->cw_params, override_redirect, true); + XCB_AUX_ADD_PARAM(&window->cw_mask, &window->cw_params, event_mask, event_mask); + XCB_AUX_ADD_PARAM(&window->cw_mask, &window->cw_params, colormap, colormap); + + // Temporary position and size + window->width = window->height = 1; + window->x = window->y = 0; + + window->window = xcb_generate_id(display->connection); + xcb_aux_create_window(display->connection, + display->screen_depth, + window->window, + display->screen->root, + window->x, window->y, + window->width, window->height, + 0, // border + XCB_WINDOW_CLASS_COPY_FROM_PARENT, + display->visual_type->visual_id, + window->cw_mask, + &window->cw_params); + + log_value_debug("Xcb window created", + "u:id", window->window); + + wm_setup(window); + log_debug("Updated window WM options"); + + xcb_map_window(display->connection, window->window); + xcb_flush(display->connection); + + window->surface = cairo_xcb_surface_create(display->connection, + window->window, + display->visual_type, + display->screen_size->width, + display->screen_size->height); + + assert(cairo_surface_status(window->surface) == CAIRO_STATUS_SUCCESS); + log_trace("Cairo surface created"); + + window->cr = cairo_create(window->surface); + assert(cairo_status(window->cr) == CAIRO_STATUS_SUCCESS); + log_trace("Cairo context created"); +} + +//double window_get_scale(window_t *window) +//{ +// const int n = 4; +// return MAX(1, floor((window->display->screen_dpi / 96.0) * n) * (1.0 / n)); +//} + +void window_move(window_t *window, int x, int y) +{ + if (window->x == x && window->y == y) + return; + + const uint32_t values[] = { x, y }; + xcb_configure_window(window->display->connection, + window->window, + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, + values); + + log_value_debug("Updated window position", + "i:x", x, + "i:y", y); + + window->x = x; + window->y = y; +} + +static void window_reshape(window_t *window) +{ + // TODO: Actually make this a parameter + int radius = window->height / 2; + double degree = M_PI / 180.0; + + // TODO: Check this value + int depth = 1; + + xcb_pixmap_t bitmap = xcb_generate_id(window->display->connection); + xcb_create_pixmap(window->display->connection, depth, bitmap, window->window, window->width, window->height); + log_debug("Xcb pixmap created [id=%u]", bitmap); + + cairo_surface_t *surface = cairo_xcb_surface_create_for_bitmap(window->display->connection, + window->display->screen, + bitmap, + window->width, + window->height); + + cairo_t *cr = cairo_create(surface); + + // TODO: Fix antialiasing situation + //cairo_set_antialias(cr, CAIRO_ANTIALIAS_GOOD); + + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE); + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + + cairo_set_source_rgba(cr, 0, 0, 0, 0); + cairo_paint(cr); + + cairo_set_source_rgba(cr, 1, 1, 1, 1); + cairo_arc(cr, radius, radius, radius, 90.0 * degree, 270 * degree); + cairo_arc(cr, window->width - radius, radius, radius, 270 * degree, 450 * degree); + cairo_fill(cr); + + log_trace("Xcb shape painted"); + + cairo_show_page(cr); + cairo_destroy(cr); + cairo_surface_flush(surface); + cairo_surface_destroy(surface); + + xcb_shape_mask(window->display->connection, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, window->window, 0, 0, bitmap); + xcb_shape_select_input(window->display->connection, window->window, XCB_SHAPE_NOTIFY); + log_debug("Xcb shape mask updated"); + + xcb_free_pixmap(window->display->connection, bitmap); +} + +void window_resize(window_t *window, int width, int height) +{ + if (window->width == width && window->height == height) + return; + + const uint32_t values[] = { width, height }; + xcb_configure_window(window->display->connection, + window->window, + XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, + values); + + log_value_debug("Updated window size", + "i:width", width, + "i:height", height); + + window->width = width; + window->height = height; + + // Update wm hints + wm_set_size(window); + + // Update shape mask + window_reshape(window); +} + +void window_present(window_t *window, cairo_surface_t *surface, int width, int height) +{ + cairo_xcb_surface_set_size(window->surface, width, height); + xcb_clear_area(window->display->connection, false, window->window, 0, 0, 0, 0); + + cairo_save(window->cr); + cairo_set_source_surface(window->cr, surface, 0, 0); + + cairo_paint(window->cr); + cairo_show_page(window->cr); + + cairo_surface_flush(window->surface); + cairo_restore(window->cr); + + xcb_circulate_window(window->display->connection, XCB_CIRCULATE_RAISE_LOWEST, window->window); + xcb_flush(window->display->connection); +} + +void window_close(window_t *window) +{ + cairo_destroy(window->cr); + cairo_surface_destroy(window->surface); +} diff --git a/src/window.h b/src/window.h new file mode 100644 index 0000000..74cc231 --- /dev/null +++ b/src/window.h @@ -0,0 +1,31 @@ +#ifndef COMET_WINDOW_H +#define COMET_WINDOW_H + +#include <cairo.h> +#include <xcb/xcb.h> +#include <xcb/xcb_aux.h> + +#include "display.h" + +typedef struct { + display_t *display; + xcb_drawable_t window; + uint32_t cw_mask; + xcb_params_cw_t cw_params; + cairo_surface_t *surface; + cairo_t *cr; + int x, y; + int width, height; +} window_t; + +void window_init(window_t *window, display_t *display); + +void window_move(window_t *window, int x, int y); + +void window_resize(window_t *window, int width, int height); + +void window_present(window_t *window, cairo_surface_t *surface, int width, int height); + +void window_close(window_t *window); + +#endif |
