commit 120b33c9f35de8dee6cab1fc24d33e4d357213a4 Author: Lephenixnoir Date: Fri Mar 12 16:19:43 2021 +0100 version 1.0.0, I guess diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25cb699 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Build files +build-fx +build-cg + +# GiteaPC support +giteapc-config.make +giteapc-config-*.make + +# Developer's files +*.sublime-* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8ae3ebf --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,51 @@ +# Just UI + +cmake_minimum_required(VERSION 3.16) +project(JustUI VERSION 1.0 LANGUAGES C) +find_package(Gint 2.1 REQUIRED) +include(Fxconv) + +configure_file(include/justui/config.h.in include/justui/config.h) + +set(ASSETS_fx + assets/input-modes-fx.png +) +set(ASSETS_cg + assets/input-modes-cg.png +) +fxconv_declare_assets(${ASSETS_fx} ${ASSETS_cg}) + +set(NAME "justui-${FXSDK_PLATFORM}") +add_library(${NAME} STATIC + src/jwidget.c + src/jlayout_box.c + src/jlayout_stack.c + src/jlayout_grid.c + src/jlabel.c + src/jscene.c + src/jinput.c + src/jpainted.c + src/jfkeys.c + src/vec.c + src/keymap.c + ${ASSETS_${FXSDK_PLATFORM}} +) + +target_compile_options(${NAME} PUBLIC + -Wall -Wextra -std=c11 -Os) +target_include_directories(${NAME} PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}/include" + "${FXSDK_COMPILER_INSTALL}/include/openlibm") +target_link_libraries(${NAME} + Gint::Gint -lopenlibm) + +install(TARGETS ${NAME} + DESTINATION "${FXSDK_COMPILER_INSTALL}") +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include" + DESTINATION "${FXSDK_COMPILER_INSTALL}" + FILES_MATCHING PATTERN "*.h") +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/include/justui/config.h" + DESTINATION "${FXSDK_COMPILER_INSTALL}/include/justui") +install(FILES cmake/FindJustUI.cmake + DESTINATION "${FXSDK_CMAKE_MODULE_PATH}") diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc4f146 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Just User Interfaces + +JustUI is a "small" library to make GUIs on fx-9860G and fx-CG 50 with gint +(and I mean small by GUI library standards). I've built it to improve user +interfaces in [gintctl](https://gitea.planet-casio.com/Lephenixnoir/gintctl), +but it's still documented enough that you can try it if you need GUIs. + +Features include: + +* Widget trees with parent/children ownership +* Basic `jwidget` and derived types (`jlabel`, `jinput`, etc.) +* A good layout system to determine the size and position of widgets +* Low-effort custom widgets (`jpainted`) and completely custom widget types +* Decent keyboard focus and event system + +Screenshots: TODO + +Documentation: + +* [Widget types](doc/widgets.md) +* [Widget hierarchy](doc/hierarchy.md) +* [Space distribution and layout](doc/layout.md) +* [Scenes and events](doc/scene.md) (TODO) +* Everything else is explained in the headers. diff --git a/TODO b/TODO new file mode 100644 index 0000000..77fd9ad --- /dev/null +++ b/TODO @@ -0,0 +1,10 @@ +* Implement grid layout +* Other useful widgets + +* Event subscriptions + +* Simplify jlabel code using dfont_default() + +Box layout: +* align-items (don't always center along the cross axis) +* justify-content (distribute empty space along the main axis) diff --git a/assets/fxconv-metadata.txt b/assets/fxconv-metadata.txt new file mode 100644 index 0000000..199811f --- /dev/null +++ b/assets/fxconv-metadata.txt @@ -0,0 +1,8 @@ +input-modes-fx.png: + name: j_img_input_modes + type: bopti-image + +input-modes-cg.png: + name: j_img_input_modes + type: bopti-image + profile: p4 diff --git a/assets/input-modes-cg.png b/assets/input-modes-cg.png new file mode 100644 index 0000000..587838a Binary files /dev/null and b/assets/input-modes-cg.png differ diff --git a/assets/input-modes-fx.png b/assets/input-modes-fx.png new file mode 100644 index 0000000..f68ad3c Binary files /dev/null and b/assets/input-modes-fx.png differ diff --git a/cmake/FindJustUI.cmake b/cmake/FindJustUI.cmake new file mode 100644 index 0000000..b8c2fff --- /dev/null +++ b/cmake/FindJustUI.cmake @@ -0,0 +1,19 @@ +include(FindSimpleLibrary) +include(FindPackageHandleStandardArgs) + +find_package(Gint 2.1 REQUIRED) + +find_simple_library("libjustui-${FXSDK_PLATFORM}.a" include/justui/config.h + "J_VERSION" PATH_VAR J_PATH VERSION_VAR J_VERSION) + +find_package_handle_standard_args(JustUI + REQUIRED_VARS J_PATH J_VERSION + VERSION_VAR J_VERSION) + +if(JustUI_FOUND) + add_library(JustUI::JustUI UNKNOWN IMPORTED) + set_target_properties(JustUI::JustUI PROPERTIES + IMPORTED_LOCATION "${J_PATH}" + INTERFACE_LINK_OPTIONS -ljustui-${FXSDK_PLATFORM} + IMPORTED_LINK_INTERFACE_LIBRARIES Gint::Gint) +endif() diff --git a/doc/hierarchy.md b/doc/hierarchy.md new file mode 100644 index 0000000..d8487ac --- /dev/null +++ b/doc/hierarchy.md @@ -0,0 +1,33 @@ +# JustUI: Widget hierarchy + +## The tree hierarchy + +Every widget (either of type `jwidget` or of a structure type that begins with +a `jwidget`) has a parent. This relationship forms a tree. The widgets that +have a common parent *w* are called the children of *w*. + +The motivation for the widget hierarchy is to have groups of widgets behave as +one. A complex UI element can have many widgets to implement its complex +interface, but group them together as children of a parent and expose only the +parent. This is an essential tool to build complex interfaces out of smaller +components. + +When a widget is created, for instance with `jwidget_create()` or +`jlabel_create()`, its parent is specified as the last parameter. + +In the tree one of the widgets is the ancestor of all the others, and is called +the *root* of the scene. In JustUI the root of the tree is normally a `jscene`, +because that is how event handling and keyboard input are managed. + +## Managing ownership + +Every widget *owns* its children with regards to memory allocation. This means +that when a widget is destroyed, its children are destroyed along with it. +Thus, even though the user program performs a function call for each widget to +allocate in a scene, a single call freeing the root will destroy all of them. + +Whenever children are moved around, they change ownership. This is important +because we always want the parent to outlive the children. Specifically, if a +source widget holds a pointer of reference to a target widget, then the source +should make sure that either the target is one of its parents, or it is +informed when the target is destroyed. diff --git a/doc/layout.md b/doc/layout.md new file mode 100644 index 0000000..c948d37 --- /dev/null +++ b/doc/layout.md @@ -0,0 +1,87 @@ +# JustUI: Space distribution and layout + +Layout is the process through which space is allocated to widgets, which are +then sized and positioned. + +## Content widgets and containers + +Widgets in a scene usually fulfill one of two roles: + +* **Content widgets** such as labels, input fields, or menu browsers, display + the interface and receive events. They often have no children, and do their + own rendering. +* **Containers** such as rows and columns, stacks, or grids, organize other + widgets so they align nicely and don't overlap. The widgets they organize are + their children; they themselves often perform no rendering. + +JustUI does not enforce this separation, and a single widget can both handle +contents and organize children. But the features are usually designed with one +of the two roles in mind. + +## Layouts + +Layouts are parameters that can be attached to widgets to automatically size +and position their widgets in a useful fashion. This is mostly designed for +containers. There are currently 4 types of layouts: + +* **Horizontal boxes** and **vertical boxes** arrange children in a row or a + column, respectively. Each widget gets its desired size; if there is space + left, widgets can expand according to stretch parameters (more on that + later). +* **Stacks** arrange all the widgets on top of each other. Only one widget is + visible at a time. This is useful for tabbed interfaces. +* **Grids** arrange all widgets in a grid. (TODO: WIP) + +A widget that does not have a layout needs to manually determine its own +desired size as well as the position its children. + +## The layout process + +The layout process has two phases. + +1. **Size estimation.** In the first phase, each widget declares a desired + size. This size often depends on the size of the children, so this phase + proceeds bottom-up: first the children declare their desired sizes, then the + parents deduce their own desired sizes, and so on. + +2. **Space distribution.** In the second phase, space is distributed by the + parents to the children. If the parents have more available space than the + children request, extra space is distributed as well. This phase is + top-down: first the root distributes the available space to its children, + then these children split their shares between their own children, etc. + +All of this proceeds automatically unless some widgets cannot provide a natural +size (or only a useless one), in which case the user should give a hint. + +## Internals + +During the first phase, the content size of each widget is evaluated by either +the layout's `csize()` function, or the polymorphic widget's `csize()` +override. Then `jwidget_msize()` adds in the geometry and stores the margin-box +size in the `w` and `h` attributes. + +During the second phase, `jwidget_layout_apply()` distributes the space by +dispatching to the layout's `apply()` function, or the polymorphic widget's +`apply()` override. It proceeds recursively in a depth-first, prefix order. + +## Layout control + +The widget type provides a natural content size, but the user has the final say +in the size of any widget. Any widget can have a minimum and maximum size +specified, and every layout guarantees that the allocated size falls in this +range (note that limits are examined during the second phase, and the natural +content size does not need to be in the acceptable range). + +Additionally, the user can provide stretch rates indicating whether the widget +can use more space horizontally and vertically. When there is space to +distribute and several widgets are competing to use it, space is allocated in +proportion to stretch rates. For instance, a widget with double the stretch +rate of its competitors will get twice as much space as them. + +In certain occasions, one might want to disregard the natural content size +entirely and distribute space based only on stretch rates, for instance to +split a screen in evenly-sized columns even when the contents of the columns +have different natural sizes. In this case, one can set the columns to a fixed +width of 0 while enabling stretching-beyond-limits. Stretching beyond limits +will allow the widgets to grow despite being of fixed size, and because they +all start with the same width of 0, they will all end up with the same size. diff --git a/doc/scene.md b/doc/scene.md new file mode 100644 index 0000000..cd93db0 --- /dev/null +++ b/doc/scene.md @@ -0,0 +1,3 @@ +# JustUI: Scenes and events + +TODO. diff --git a/doc/widgets.md b/doc/widgets.md new file mode 100644 index 0000000..3206122 --- /dev/null +++ b/doc/widgets.md @@ -0,0 +1,163 @@ +# JustUI: Widget types + +## General widgets and void types + +JustUI defines a base structure `jwidget` useful for containers, as well as a +number of derived structures for content widgets. Because C does not support +any type of polymorphism, JustUI uses `void *` pointers when referring to "any +widget", which accepts pointers to any base or derived structure. + +This relaxed typing scheme mostly applies to the `jwidget_*()` functions that +operate on the widget level. Widget-specific functions such as `jinput_value()` +request exactly their type of widget as input (in this case, `jinput`). +Essentially, this means that `jwidget` can be inherited from transparently, +but the other types cannot (a structure attribute must be used). This is a +compromise to work with polymorphic types in the C type system. + +## Widget collection + +**`jwidget`** is the base type. It provides children/parent management (see +[Widget hierarchy](hierarchy.md)), built-in layouts (see [Space distribution +and layout](layout.md)) and size constraints, geometry (margin, border, +padding, background), and automatic repaint/relayout mechanisms. + +**`jlabel`** is a content widget that shows a single-line or multi-line piece +of text. It supports various text alignment options (left/right/center), line +wrapping (letter and word level), and a couple of graphical options. + +**`jscene`** is a special widget designed to be at the root of the widget tree. +It provides event dispatching, automatic repaint and layout, keyboard input, +and a generic input loop that is suitable for most GUI programs. See [Scenes +and events](scene.md). + +**`jinput`** is a one-line input field. It supports direct key input, delayed +and instant SHIFT and ALPHA modifiers, as well as modifier locking, with visual +hints. + +**`jpainted`** is a very simple wigdet that paints itself with a user-provided +function. It is intended to make custom widgets with very little effort. A +`jpainted` can be positioned, sized and managed by the widget tree while the +user only provides a drawing function. + +**`jfkeys`** represents a function key bar, normally at the bottom of the +screen. On fx-9860G, it uses an image to show keys; on fx-CG 50, it supports a +string specification that looks like `"@JUMP;;#ROM;#RAM;#ILRAM;#ADDIN"`. It can +change options dynamically. + +## Custom widgets and polymorphic operations + +For custom widgets that just have custom rendering a no event management, one +can simply use a `jpainted` instance with well-chosen options. However, for +reusable widgets that have internal state or event handling, a new widget type +should be created. + +A custom widget must be a structure that starts with a `jwidget`. The type +itself should be register with `j_register_widget()`, and provide a couple of +polymorphic functions. Here is an example with a very trivial widget that holds +an integer counter. + +```c +typedef struct { + jwidget widget; + int counter; +} jcounter; +``` + +To be registered in JustUI, a custom widget type must provide a couple of +functions, in the form of a `jwidget_poly` structure. All of the following +functions are detailed in ``, but here is a brief review +of the requirements (the `type_poly_` prefix is customary). All of the +functions receive pointers to `jcounter` structures, but the type is `void *` +because of the limitations mentioned earlier. + +```c +/* Receives a (jcounter *). Should set its (w) and (h) attributes to the + natural dimensions of the widget. Cannot be NULL. */ +void jcounter_poly_csize(void *counter); + +/* Paint a jcounter at (x,y). */ +void jcounter_poly_render(void *counter, int x, int y); + +/* Destroy the resources of a jcounter. (If there are not malloc'd pointers in + the structure, this is generally not needed.) */ +void jcounter_poly_destroy(void *counter); +``` + +The following functions should be implemented if the custom widget needs to +receive events (such as keyboard input). Events are defined in +``. + +```c +/* Handle event (e) sent to a jcounter. Should return true if the event was + accepted/used, false if it was ignored/unused. */ +bool jcounter_poly_event(void *counter, jevent e); +``` + +The following function is used if (1) an instance of the custom widget has +children, and (2) this instance does not have a layout. This is rarely needed. +See [Space distribution and layout](layout.md) + +```c +/* Receives a (jcounter *) that has its full size set in the (w) and (h) + attributes. Should determine the position and size of the children. */ +void jcounter_poly_layout(void *counter); +``` + +In general, most of the work consists in specifying the `csize()` and +`render()` functions. `destroy()` just has to release the resources allocated +during widget creation, `event()` is straightforward, and `layout()` is very +rarely needed at all. + +Once the required functions are implemented, a polymorphic widget structure can +be defined and registered as a new type. A good way to do this is to register +the widget in a constructor located in the same file as the widget creation +function, so that it runs automatically if the widget is used. + +```c +static jwidget_poly type_jcounter = { + .name = "jcounter", + .csize = jcounter_poly_csize, + .layout = NULL, + .render = jcounter_poly_render, + .event = NULL, + .destroy = NULL, +}; + +static int jcounter_type_id; + +__attribute__((constructor(2001))) +static void j_register_jcounter(void) +{ + jcounter_type_id = j_register_widget(&type_jcounter, "jwidget"); +} +``` + +The second parameter to `j_register_widget()` specifies inheritance. `jcounter` +inherits from `jwidget`, which means that the unspecified polymorphic functions +(`layout`, `event` and `destroy`) will use the default behavior of `jwidget`. +This is mostly useful if you don't specify `csize` (the default behavior is to +select the smallest size where all children fit) or `render` (the default +behavior is to render all visible children). + +The type ID returned by `j_register_widget()` is how JustUI differentiates +labels from input fields from custom counters. When creating the widget, you +should initialize the `jwidget` field with `jwidget_init()` and specify the +type ID. Note how the parent is also passed at creation time so that the new +widget can automatically be attached to the tree. + +```c +jcounter *jcounter_create(int initial_value, void *parent) +{ + if(jcounter_type_id < 0) return NULL; + + jcounter *c = malloc(sizeof *c); + jwidget_init(&c->widget, jcounter_type_id, parent); + + c->counter = initial_value; + return c; +} +``` + +That's pretty much it. There is a fair amount of boilerplate, but this part +is the same for every widget. See `jpainted` for a simple example and `jinput` +for a full-featured one. diff --git a/giteapc.make b/giteapc.make new file mode 100644 index 0000000..115fab9 --- /dev/null +++ b/giteapc.make @@ -0,0 +1,25 @@ +# giteapc: version=1 depends=Lephenixnoir/gint + +-include giteapc-config.make + +configure: + @ fxsdk build-fx -c $(JUSTUI_CMAKEOPTS_FX) + @ fxsdk build-cg -c $(JUSTUI_CMAKEOPTS_CG) + +build: + @ fxsdk build-fx + @ fxsdk build-cg + +install: + @ fxsdk build-fx install + @ fxsdk build-cg install + +uninstall: + @ if [ -e build-fx/install_manifest.txt ]; then \ + xargs rm -f < build-fx/install_manifest.txt; \ + fi + @ if [ -e build-cg/install_manifest.txt ]; then \ + xargs rm -f < build-cg/install_manifest.txt; \ + fi + +.PHONY: configure build install uninstall diff --git a/include/justui/config.h.in b/include/justui/config.h.in new file mode 100644 index 0000000..68a4b76 --- /dev/null +++ b/include/justui/config.h.in @@ -0,0 +1,10 @@ +//--- +// Just UI: Compile-time configuration +//--- + +#ifndef _JUSTUI_CONFIG +#define _JUSTUI_CONFIG + +#define J_VERSION "@JustUI_VERSION@" + +#endif /* _JUSTUI_CONFIG */ diff --git a/include/justui/defs.h b/include/justui/defs.h new file mode 100644 index 0000000..e47fab8 --- /dev/null +++ b/include/justui/defs.h @@ -0,0 +1,62 @@ +//--- +// JustUI.defs: Type and utility definitions +//--- + +#ifndef _J_DEFS +#define _J_DEFS + +#include +#include +#include + +#include +#include +#include + +/* j_dirs_t: Quadruplet with four directions */ +typedef struct { + uint8_t top; + uint8_t right; + uint8_t bottom; + uint8_t left; +} jdirs; + +/* j_align_t: Alignment options with both horizontal and vertical names */ +typedef enum { + /* Horizontal names */ + J_ALIGN_LEFT = 0, + J_ALIGN_CENTER = 1, + J_ALIGN_RIGHT = 2, + /* Vertical names */ + J_ALIGN_TOP = 0, + J_ALIGN_MIDDLE = 1, + J_ALIGN_BOTTOM = 2, + +} __attribute__((packed)) jalign; + +/* j_arg_t: Standard 4-byte argument types for callbacks */ +typedef union { + /* Pointers of different qualifiers */ + void *p; + void const *cp; + volatile void *vp; + volatile void const *vcp; + /* Integers */ + int32_t i32; + uint32_t u32; + +} GTRANSPARENT j_arg_t; + +/* Vararg macro to cast (void *) parameters to (jwidget *), useful in generic + widget functions. J_CAST(x, y, ...) will expand to + jwidget *x = x0; + jwidget *y = y0; + ... + and accepts from 1 to 4 parameters. */ +#define J_CAST0(x) _Pragma("GCC error \"J_CAST takes only up to 4 arguments\"") +#define J_CAST1(x, ...) jwidget *x = x ## 0; __VA_OPT__(J_CAST0(__VA_ARGS__)) +#define J_CAST2(x, ...) jwidget *x = x ## 0; __VA_OPT__(J_CAST1(__VA_ARGS__)) +#define J_CAST3(x, ...) jwidget *x = x ## 0; __VA_OPT__(J_CAST2(__VA_ARGS__)) +#define J_CAST(x, ...) jwidget *x = x ## 0; __VA_OPT__(J_CAST3(__VA_ARGS__)) + +#endif /* _J_DEFS */ diff --git a/include/justui/jevent.h b/include/justui/jevent.h new file mode 100644 index 0000000..989a604 --- /dev/null +++ b/include/justui/jevent.h @@ -0,0 +1,50 @@ +//--- +// JustUI.jevent: GUI union event +//--- + +#ifndef _J_EVENT +#define _J_EVENT + +#include +#include + +#include + +/* jevent: GUI event + + This type is mostly a union type that provides details on every event that + occurs in the GUI. These are mostly widget signaling state changes, + validations, and other GUI specifics that might require attention. Events + can either be reported to the user by the scene (upwards event) or notify + widgets of something occuring to them (downwards event). + + JustUI tries hard to not invert flow control and leave the user to decide + when to produce downwards events. In a normal situation, events from + getkey() are passed to the scene using jscene_process_key() while reading + GUI events moving upwards with jscene_pollevent(). This way, the user can + decide to filter their key events or even craft some. + + For the sake of convenience, a single function jscene_run() is provided that + implements a common form of main loop, which forwards keyboard events to the + scene and reports upwards GUI events and ignored key events. + + Event IDs can be registered with j_register_event() and (usually) exposed as + global variables. Extensions are meaningful for custom widget types that + need their own upwards events. */ +typedef struct { + /* Widget that emitted the event (if upwards), NULL otherwise */ + void *source; + /* Type of event */ + uint16_t type; + /* Reserved for future use */ + uint16_t _; + + /* Event details or data */ + union { + /* Downward JWIDGET_KEY event or upwards JSCENE_KEY */ + key_event_t key; + }; + +} jevent; + +#endif /* _J_EVENT */ diff --git a/include/justui/jfkeys.h b/include/justui/jfkeys.h new file mode 100644 index 0000000..4a72e7b --- /dev/null +++ b/include/justui/jfkeys.h @@ -0,0 +1,104 @@ +//--- +// JustUI.jfkeys: Row of function keys +//--- + +#ifndef _J_JFKEYS +#define _J_JFKEYS + +#include +#include + +#include + +/* jfkeys: Functions keys indicating functions for the F1..F6 keys + + This widget is the typical function key bar with a slightly different + design. There are four types of keys, with conventional guidelines: + + * MENU KEYS are used for functions that open menus or navigate between tabs + on a same application. The name comes from their primary usage in the + system apps. Navigation functions should be easilty reversible and fairly + failproof. Menu keys are black rectangular keys with a chipped corner. + + * ENTRY KEYS are used for catalog entries such as the leaves of PRGM's many + nested input menus. They represent entries to be chosen from. Entry keys + are black rectangular keys. + + * ACTION KEYS are used for generic safe and unsafe actions. Action keys are + black round keys. + + * SPECIAL KEYS are used for special functions, such as scrolling to the next + set of functions keys when there are several pages, important functions + that should catch attention, or particulary unsafe actions. They are round + white keys. + + On fx-CG 50, the keys are drawn dynamically using gint's default font, and + specified using 6 strings that give the type and name of the keys: + + * "/NAME" for a menu key; + * ".NAME" for an entry key; + * "@NAME" for an action key; + " "#NAME" for a special key. + + The names are separated by semicolons, eg "/F1;;/F3;.F4;@F5;#F6". Several + sets of function keys can be defined if separated by a '|' character. For + instance, "/F1;#F2|/F1" represents a function bar where the F2 + function can be hidden by swithing from level 0 to level 1. + + On fx-9860G, there is not enough space to generate keys on-the-fly, so the + full specification is just an image. The convention for the image is to be + 128x8 pixels, with each function key (i) positioned at (x = 21*i + 2) of + width 19. Several levels can be stacked up (n levels in an image of height + 9n-1) and selected independently. */ +typedef struct { + jwidget widget; + int8_t level; + + #ifdef FX9860G + bopti_image_t const *img; + #endif + + #ifdef FXCG50 + char const *labels; + #endif + +} jfkeys; + +/* jfkeys_create(): Create a set of function keysed + + The arguments are obviously different on fx-9860G and fx-CG 50. If your + application supports both, you might want to specify arguments for both + platforms in a single call with jfkeys_create2() which will filter them out + for you. Referencing an image unavailable on fx-CG 50 in jfkeys_create2() is + safe since the preprocessor will remove that text. */ + +#ifdef FX9860G +jfkeys *jfkeys_create(bopti_image_t const *img, void *parent); +#define jfkeys_create2(img, labels, parent) jfkeys_create(img, parent) +#endif + +#ifdef FXCG50 +jfkeys *jfkeys_create(char const *labels, void *parent); +#define jfkeys_create2(img, labels, parent) jfkeys_create(labels, parent) +#endif + +/* jfkeys_set(): Replace functions + This will also reset the level to 0. */ + +#ifdef FX9860G +void jfkeys_set(jfkeys *keys, bopti_image_t const *img); +#define jfkeys_set2(keys, img, labels) jfkeys_set(keys, img) +#endif + +#ifdef FXCG50 +void jfkeys_set(jfkeys *keys, char const *labels); +#define jfkeys_set2(keys, img, labels) jfkeys_set(keys, labels) +#endif + +/* jfkeys_level(): Return the current function key level */ +int jfkeys_level(jfkeys *keys); + +/* jfkeys_set_level(): Set the function key level */ +void jfkeys_set_level(jfkeys *keys, int level); + +#endif /* _J_JFKEYS */ diff --git a/include/justui/jinput.h b/include/justui/jinput.h new file mode 100644 index 0000000..1fd7c45 --- /dev/null +++ b/include/justui/jinput.h @@ -0,0 +1,91 @@ +//--- +// JustUI.jinput: One-line input field +//--- + +#ifndef _J_JINPUT +#define _J_JINPUT + +#include +#include +#include + +#include + +/* jinput: One-line input field + + This widget is used to read input from the user. It has a single line of + text which can be edited when focused, and an optional prompt. On the right, + an indicator displays the status of modifier keys. + + The edition rules support both the OS' native one-key-at-time input system, + and the usual computer modifer-keys-held method. + + * The normal insertion mode is used by default. + * When pressing SHIFT or ALPHA in combination with a key (without releasing + SHIFT or ALPHA, as on a computer), the secondary or alphabetic function of + the key is used. + * Pressing then releasing SHIFT or ALPHA activates the secondary or + alphabetic function for the next key press. + * Double-tapping SHIFT or ALPHA locks the corresponding mode on until the + locked mode is disabled by another press-release of the same modifier key. + + TODO: jinput: Selection with SHIFT + TODO: jscene: Clipboard support + + A timer is used to make the cursor blink, sending JSCENE_REPAINT events + every second or so. The timer is held only during editing and freed when + input stops. If no timer is available the cursor is simply not animated for + the corresponding input sequence. + + Events: + * JINPUT_VALIDATED when EXE is pressed during edition + * JINPUT_CANCELED when EXIT is pressed during edition */ +typedef struct { + jwidget widget; + + /* Color and font of text */ + int color; + font_t const *font; + + /* Optional prompt */ + char const *prompt; + + /* Text being input */ + char *text; + /* Current size (including NUL) and maximum size */ + uint16_t size; + uint16_t max; + + /* Current cursor position */ + int16_t cursor; + /* Current input mode */ + uint8_t mode; + /* Timer ID for the cursor state */ + int8_t timer; + +} jinput; + +/* Type IDs */ +extern uint16_t JINPUT_VALIDATED; +extern uint16_t JINPUT_CANCELED; + +/* jinput_create(): Create an input field + + The input field is disabled until it receives focus from its scene. The + edited text is initially empty and is allocated when needed. The length + specifies the maximum amount of bytes in the input. */ +jinput *jinput_create(char const *prompt, size_t length, void *parent); + +/* Trivial properties */ +void jinput_set_text_color(jinput *input, int color); +void jinput_set_font(jinput *input, font_t const *font); +void jinput_set_prompt(jinput *input, char const *prompt); + +/* Current value visible in the widget, normally useful upon receiving the + JINPUT_VALIDATED event, not guaranteed otherwise */ +char const *jinput_value(jinput *input); + +/* jinput_clear(): Clear current text */ +void jinput_clear(jinput *input); + +#endif /* _J_JINPUT */ diff --git a/include/justui/jlabel.h b/include/justui/jlabel.h new file mode 100644 index 0000000..d98a136 --- /dev/null +++ b/include/justui/jlabel.h @@ -0,0 +1,134 @@ +//--- +// JustUI.jlabel: Simple one-line or multi-line text label without formatting +//--- + +#ifndef _J_JLABEL +#define _J_JLABEL + +#include +#include +#include + +#include + +/* jlabel_wrapmode: Text wrapping options */ +typedef enum jwrapmode { + /* Wrap only at \n characters */ + J_WRAP_NONE, + /* Break only at spaces; if a word is longer than a full line, breaking in + that word is allowed */ + J_WRAP_WORD, + /* Break only at spaces and tabs, even if a word is longer than a line */ + J_WRAP_WORD_ONLY, + /* Break at any letter */ + J_WRAP_LETTER, + +} __attribute__((packed)) jwrapmode; + +/* jlabel: One-line or multi-line piece of text, without formatting. + + This widget is used for mundane text printing. All the text in the label is + printed with a single font. The text can be aligned horizontally and + vertically within the widget ("block alignment"), and the lines can also be + aligned horizontally within the text block ("text alignment"). + + +-------------+ + | label | Text is centered within the widget (block centered). + | 1 | Lines are aligned by their center (text centered). + +-------------+ + + +-------------+ + | label | Text is centered within the widget (block centered). + | 2 | Lines are aligned by their left side (text left). + +-------------+ + + +-------------+ + |label | Text is aligned left within the widget (block left). + | 3 | Lines are aligned by their center (text centered). + +-------------+ + + Lines are broken at '\n' characters. Depending on the word wrap settings, + lines can also be broken whenever the edge of the widget is reached. Extra + line spacing (even negative) can be specified. + + The natural size of the widget is always computed based on newline + characters, since there is no way to wrap until a width has been assigned. + If you want predictable results, use size constraints. + + The text color and font can also be set, using the same color values and + fonts as . If the font is set to NULL, gint's default font + is used. The background color for the whole widget is handled by jwidget. */ +typedef struct { + jwidget widget; + + /* Horizontal block alignment */ + jalign block_halign; + /* Vertical block alignment */ + jalign block_valign; + /* Text alignment */ + jalign text_align; + /* Pixels of spacing between each line, in addition to font->height */ + int8_t line_spacing; + + /* Text to display */ + char const *text; + /* List of line break offsets; indexes 2n and 2n+1 give the start and end + of line number n */ + DECLARE_VEC(uint16_t, breaks); + + /* Text wrapping mode */ + enum jwrapmode wrap_mode; + /* Whether the text has been allocated by the label or supplied by user */ + int8_t owns_text; + /* Block width (maximum length of a rendered line) */ + uint16_t block_width; + + /* Color and font of text; if NULL, gint's default font is used */ + int color; + font_t const *font; + +} jlabel; + +/* jlabel_create(): Create a label with a fixed text + + Initially the label has the supplied string as text (which must outlive the + widget). If you want dynamic text, you can provide an empty string and use + jlabel_printf() later. */ +jlabel *jlabel_create(char const *text, void *parent); + +/* jlabel_set_text(): Set a fixed string + This function sets the label text to a fixed string. This is the same as in + jlabel_create(). The string is not copied, it must outlive the label. This + is the most useful when the provided string is a literal. */ +void jlabel_set_text(jlabel *l, char const *text); + +/* jlabel_asprintf(): Generate and set a formatted string + This function generates the label string with printf-formatting. The + resulting string is created with malloc() and owned by the label; it is + destroyed when the text is replaced of the label is destroyed. Because of + how asprintf() works, the string is generated twice. Returns the number of + characters printed. */ +int jlabel_asprintf(jlabel *l, char const *format, ...); + +/* jlabel_snprintf(): Generate and set a formatted string + Similar to jlabel_asprintf(), but an upper bound on the length of the result + string has to be provided. This avoids generating the string twice. Return + the number of characters printed. */ +int jlabel_snprintf(jlabel *l, size_t size, char const *format, ...); + +/* jlabel_text(): Get the current string */ +char const *jlabel_text(jlabel *l); + +/* Set block and text alignment, individually */ +void jlabel_set_block_alignment(jlabel *l, jalign horz, jalign vert); +void jlabel_set_text_alignment(jlabel *l, jalign align); +/* Set both horizontal alignments at the same time (most natural) */ +void jlabel_set_alignment(jlabel *l, jalign horizontal_align); + +/* Trivial properties */ +void jlabel_set_line_spacing(jlabel *l, int line_spacing); +void jlabel_set_wrap_mode(jlabel *l, jwrapmode mode); +void jlabel_set_text_color(jlabel *l, int color); +void jlabel_set_font(jlabel *l, font_t const *font); + +#endif /* _J_JLABEL */ diff --git a/include/justui/jlayout.h b/include/justui/jlayout.h new file mode 100644 index 0000000..63ae891 --- /dev/null +++ b/include/justui/jlayout.h @@ -0,0 +1,118 @@ +//--- +// JustUI.jlayout: Widget positioning mechanisms +//--- + +#ifndef _J_JLAYOUT +#define _J_JLAYOUT + +#include + +/* jlayout_type: Built-in layout mechanisms */ +typedef enum { + /* User or sub-class places children manually */ + J_LAYOUT_NONE = 0, + /* Children are laid out in a vertical box; spacing applies. Extra + space is redistributed proportionally to the stretch attribute. */ + J_LAYOUT_VBOX, + /* Same as J_LAYOUT_VBOX, but horizontally. */ + J_LAYOUT_HBOX, + /* Children are stacked, only one is visible at a time. */ + J_LAYOUT_STACK, + /* Children are laid out in a grid; spacing applies. */ + J_LAYOUT_GRID, + +} jlayout_type; + +//--- +// Manual layout +//--- + +/* jlayout_set_manual(): Remove a layout from a widget + This function removes the current layout of the widget. */ +void jlayout_set_manual(void *w); + +//--- +// Box layout +//--- + +/* jlayout_box: Parameters for VBOX and HBOX layouts */ +typedef struct { + /* Spacing between elements */ + uint8_t spacing; + uint :24; + +} jlayout_box; + +/* jlayout_get_hbox(), jlayout_get_vbox(): Get configuration for box layouts + These functions return the jlayout_box parameters for widgets that have a + box layout, and NULL otherwise. */ +jlayout_box *jlayout_get_hbox(void *w); +jlayout_box *jlayout_get_vbox(void *w); + +/* jlayout_set_hbox(), jlayout_set_vbox(): Create box layouts + These functions configure the specified widget to have a box layout, and + return the jlayout_box parameters to configure that layout. The parameters + are initialized even if the widget previously had a box layout. */ +jlayout_box *jlayout_set_hbox(void *w); +jlayout_box *jlayout_set_vbox(void *w); + +//--- +// Stack layouts +//--- + +/* jlayout_stack: Parameters for STACK layouts */ +typedef struct { + /* Index of currently-visible child */ + int8_t active; + uint :24; + +} jlayout_stack; + +/* jlayout_get_stack(): Get configuration for stack layouts + For widgets that have a stack layout, returns a pointer to the parameters. + Otherwise, returns NULL. */ +jlayout_stack *jlayout_get_stack(void *w); + +/* jlayout_set_stack(): Create stack layouts + Configure the specified widget to have a stack layout and return the new + parameters. The new layout is cleared even if the widget previously had a + stack layout. */ +jlayout_stack *jlayout_set_stack(void *w); + +//--- +// Grid layouts +//--- + +typedef enum { + /* Rows from top to bottom */ + J_LAYOUT_GRID_TOPDOWN, + /* Rows from bottom to top */ + J_LAYOUT_GRID_BOTTOMUP, + /* Columns from left to right */ + J_LAYOUT_GRID_LEFTRIGHT, + /* Columns from right to left */ + J_LAYOUT_GRID_RIGHTLEFT, + +} jlayout_grid_order; + +typedef struct { + /* Spacing between rows and between columns */ + uint8_t row_spacing; + uint8_t col_spacing; + /* Child order. The major order specifies whether the grid is filled by + rows or by columns. The minor order specifies how these rows or + columns are filled themselves. There must be exactly one vertical + setting and one horizontal setting. */ + uint major_order :2; + uint minor_order :2; + /* Number of rows, and number of columns; default to -1, in which case + the minor size is determined based on the first minor group and the + major size is calculated greedily. */ + uint rows :6; + uint cols :6; + +} jlayout_grid; + +/* TODO: Functions for grid layouts */ + +#endif /* _J_JLAYOUT */ diff --git a/include/justui/jpainted.h b/include/justui/jpainted.h new file mode 100644 index 0000000..d60bed4 --- /dev/null +++ b/include/justui/jpainted.h @@ -0,0 +1,65 @@ +//--- +// JustUI.jpainted: Widget defined by just a size and painting function +//--- + +#ifndef _J_JPAINTED +#define _J_JPAINTED + +#include +#include +#include + +/* jpainted: Simple widget designed to integrate low-effort rendering + + This widget is simply a rectangle on which a custom function does the + rendering. It allows the GUI to have custom rendering with very little code, + as the widget only requires a natural size and a renderer. + + The natural size doesn't need to be exactly the size of the widget; ideally + it should be larger than what the rendered needs. In any case the natural + size is not the final one, as stretch can be used in layouts. + + This widget will typically be used like this: + + void draw_something(int x, int y) { + dimage(x, y, ...); + dtext(x+2, y+10, ...); + } + jpainted *widget = jpainted_create(draw_something, NULL, 10, 10, parent); + + The rendering function can optionally take an argument; this argument can be + an integer or a pointer (essentially). + + void draw_value(int x, int y, uint32_t *value) { + dprint(x, y, "%08X", *value); + } + uint32_t my_value; + jpainted *widget = jpainted_create(draw_value, &my_value, 30, 8, parent); + + The type of the argument is defined as j_arg_t but you don't need to create + a j_arg_t object, just pass the argument directly. If you don't have an + argument, pass NULL. + + Note that in this example you will need to set (widget->update = 1) whenever + you change (my_value), in order for the scene to produce a PAINT event. + Otherwise, a change in (my_value) will not be seen on-screen until the next + time the scene is painted. + + The layout and rendering will be performed automatically, allowing the GUI + to benefit from automated positioning with virtually no overhead. */ +typedef struct { + jwidget widget; + /* Renderer function and its argument */ + void (*paint)(int x, int y, j_arg_t arg); + j_arg_t arg; + /* Natural size */ + int16_t natural_w; + int16_t natural_h; + +} jpainted; + +/* jpainted_create(): Create a simple painted widget */ +jpainted *jpainted_create(void *function, j_arg_t arg, int natural_w, + int natural_h, void *parent); + +#endif /* _J_JPAINTED */ diff --git a/include/justui/jscene.h b/include/justui/jscene.h new file mode 100644 index 0000000..4622d3b --- /dev/null +++ b/include/justui/jscene.h @@ -0,0 +1,105 @@ +//--- +// JustUI.jscene: Root object that provides keyboard focus and event handling +//--- + +#ifndef _J_JSCENE +#define _J_JSCENE + +#include +#include +#include + +#define JSCENE_QUEUE_SIZE 32 + +/* jscene: A widget scene with keyboard focus and event handling + + This widget is designed to be the root of a widget tree. It keeps track of + widgets with keyboard focus, feeds them keyboard events, and catches other + useful events to store them in an event queue. */ +typedef struct { + jwidget widget; + + /* Location on screen */ + int16_t x, y; + /* Widget with focus */ + jwidget *focus; + + /* Circular event queue */ + jevent queue[JSCENE_QUEUE_SIZE]; + uint8_t queue_first; + uint8_t queue_next; + + /* Number of events lots */ + uint16_t lost_events; + +} jscene; + +/* Events */ +extern uint16_t JSCENE_NONE; +extern uint16_t JSCENE_PAINT; +extern uint16_t JSCENE_KEY; + +/* jscene_create(): Create a new scene at that specified screen position */ +jscene *jscene_create(int x, int y, int w, int h, void *parent); + +/* jscene_create_fullscreen(): Create a fullscreen scene + The position is (0,0) and the size is (DWIDTH,DHEIGHT). */ +jscene *jscene_create_fullscreen(void *parent); + +/* jscene_layout(): Layout a scene + This is automatically called by jscene_render(), but may be useful if you + need to know the size of your widgets before rendering. The layout is + recomputed only if something in the scene has changed. */ +#define jscene_layout jwidget_layout + +/* jscene_render(): Layout and render a scene + Layout is lazy and performed only if needed. The scene is rendered at its + (x,y) point. */ +void jscene_render(jscene *scene); + +//--- +// Events sent from the scene to the user +//--- + +/* jscene_read_event(): Get the next upwards event from the queue + If there is no event, returns an event of type JSCENE_NONE. */ +jevent jscene_read_event(jscene *scene); + +/* jscene_queue_event(): Queue an upwards event to be later read by the user + This function records an event in the scene's queue, which will later be + returned by jscene_pollevent(). This is mostly used by widget code to signal + stuff that warrants attention. */ +void jscene_queue_event(jscene *scene, jevent event); + +//--- +// Keyboard focus and keyboard events +//--- + +/* jscene_focused_widget(): Query the widget that currently has focus */ +void *jscene_focused_widget(jscene *scene); + +/* jscene_set_focused_widget(): Move the focus to a widget + The selected widget, obviously, must be a descendant of the scene. */ +void jscene_set_focused_widget(jscene *scene, void *widget); + +/* jscene_process_key(): Send a downwards key event to the focused widget + Returns true if the event was accepted, false if it was ignored. */ +bool jscene_process_key(jscene *scene, key_event_t event); + +/* jscene_process_event(): Send a downards GUI event to the focused widget */ +// bool jscene_process_event(jscene *scene, jevent event); + + +/* jscene_run(): Run a scene's main loop + + This function implements a main control loop that sleeps when there is + nothing to do, forwards all key events to the scene, and returns only to + notify GUI events or hand over key events that have been ignored by the + scene. + + If a scene event occurs, returns it. If a key event occurs, an event of type + JSCENE_KEY is return and its .key attribute contains the details of the + forwarded keyboard event. */ +jevent jscene_run(jscene *scene); + +#endif /* _J_JSCENE */ diff --git a/include/justui/jwidget-api.h b/include/justui/jwidget-api.h new file mode 100644 index 0000000..ccb78c9 --- /dev/null +++ b/include/justui/jwidget-api.h @@ -0,0 +1,158 @@ +//--- +// JustUI.jwidget-API: API for subclassed widget types +//--- + +#ifndef _J_JWIDGET_API +#define _J_JWIDGET_API + +#include +#include +#include + +//--- +// Polymorphic operations on widgets +// +// The following operations are widget-type-dependent. Derived widget types +// must provide their implementation through a jwidget_poly structure. The +// supplied functions will then be called at different points during the +// lifetime of the derived widgets. +//--- + +/* jwidget_poly_csize_t: Determine the natural content size of a widget + + This function is called during layout. It should set the natural size of the + content box of the widget in (w->w) and (w->h). If the widget has a layout, + this function will not be called; instead, the layout will determine the + natural content size. Thus, this function is mostly useful for content + widgets and not useful for containers. + + Implementations of this function should use jwidget_msize() on the children + to position their margin box within the widget's content box. The size set + by this function needs not be in the minimum/maximum range of the widget. */ +typedef void jwidget_poly_csize_t(void *w); + +/* jwidget_poly_layout_t: Layout a widget after its size has been set + + This function is called during the second phase of the layout process, if + the widget has no layout. The margin-box size allocated to the widget has + been set in (w->w) and (w->h); the widget must now position its contents and + children. If the widget has a layout, the layout's specialized function is + called instead of this one. */ +typedef void jwidget_poly_layout_t(void *w); + +/* jwidget_poly_render_t: Render a widget + + This function is called during rendering after the widget's geometry is + drawn. (x,y) are the coordinates of the content box. This function must + render widget-specific visuals; there is no clipping, so the widget should + honor its width and height. + + This function should call jwidget_render() for all children that need to be + rendered. jwidget_render() handles the geometry and takes as parameters the + coordinates of the margin box, so it can be called as: + + jwidget_render(child, x + child->x, y + child->y). + + It will draw the geometry and call the polymorphic renderer of the child at + its content-box coordinates. Normally you can ignore geometry altogether. */ +typedef void jwidget_poly_render_t(void *w, int x, int y); + +/* jwidget_poly_event_t: Handle an event + + This function is called when an event is targeted at a widget, including for + key events. This function is somewhat of a catch-all function for dynamic + occurrences. The widget should either accept the event, do something with + it, and return true, or refuse the event, do nothing and return false. This + is influences events that propagate, such as key events. */ +typedef bool jwidget_poly_event_t(void *w, jevent e); + +/* jwidget_poly_destroy_t: Destroy a widget's specific resources + + This function must destroy the widget-specific resources. It is called by + jwidget_destroy(), which follows it by freeing the widget's standard data + and destroying the children. This function can be NULL if there are no + widget-specific resources to free. */ +typedef void jwidget_poly_destroy_t(void *w); + +/* jwidget_poly: Polymorphic interface for a widget type */ +typedef struct { + /* Type name, used for display and inheritance */ + char const *name; + /* Polymorphic functions */ + jwidget_poly_csize_t *csize; + jwidget_poly_layout_t *layout; + jwidget_poly_render_t *render; + jwidget_poly_event_t *event; + jwidget_poly_destroy_t *destroy; + +} jwidget_poly; + +//--- +// Widget registration +//--- + +/* j_register_widget(): Register a new widget type + + This function returns a new widget type ID to pass to jwidget_init() when + creating widgets of the custom type. Returns -1 if registration fails. The + polymorphic structure must outlive all widgets of the custom type. + + If (inherits) is non-NULL, the new type will inherit from the widget type + with the provided name. All NULL functions in (poly) will be replaced by the + parent type's functions. This mechanism only works if widgets are declared + in inheritance order, which is normally enforced by constructor priority. */ +int j_register_widget(jwidget_poly *poly, char const *inherits); + +/* j_register_event(): Register a new event type + + This function returns a new ID to set in the (type) field of jevent objects. + The ID is unique, and can be trusted to be always valid (unless you register + more than 64k events in which case you asked for the trouble). */ +int j_register_event(void); + +//--- +// Helpers to implement custom widget types +//--- + +/* jwidget_init(): Initialize a widget + + This function should be called in the constructor for the subclassed widget + type, preferably as soon as possible. It initializes common widget + attributes, sets the widget type, and declares the parent. + + A subclassed widget type must have a jwidget as a first member (so that the + address of any instance of the subclassed widget is a valid pointer to + jwidget), and that jwidget must be initialized with jwidget_init(). + + @w Widget to initialize + @type Type ID, as returned by j_register_widget() + @parent Parent, same as in jwidget_create() */ +void jwidget_init(jwidget *w, int type, void *parent); + +/* jwidget_msize(): Compute and apply the natural size of a widget's margin-box + + This function computes the widget's natural margin-box size. It determines + the natural content size with the csize() function of either the layout or + the widget type, then adds the geometry. + + The margin-box size is stored in the (w) and (h) attributes of the widget. + This function should only be called during the first phase of the layout, + to implement subclassed csize() functions. Usually, the parent will + implement a customn csize() function by combining the position and msize() + of its children. */ +void jwidget_msize(void *w); + +/* jwidget_emit(): Emit an upwards event from this widget + + This function walks up the tree until it finds a jscene that can store the + event. If there is no jscene in the tree, the event is ignored. The (source) + field can be omitted and will be set to the widget's address by default. */ +void jwidget_emit(void *w, jevent e); + +/* jwidget_event(): Send a downwards event to a widget + + This function calls the polymorphic event function of the targeted widget to + notify it of the specified event. */ +bool jwidget_event(void *w, jevent e); + +#endif /* _J_JWIDGET_API */ diff --git a/include/justui/jwidget.h b/include/justui/jwidget.h new file mode 100644 index 0000000..5aa0b61 --- /dev/null +++ b/include/justui/jwidget.h @@ -0,0 +1,370 @@ +//--- +// JustUI.jwidget: Base object for all widgets +//--- + +#ifndef _J_JWIDGET +#define _J_JWIDGET + +#include +#include + +/* jwidget: Base object for all widgets. + + Functions that ought to work on any widget take void * parameters; this + includes jwidget and derived types whose first attribute is a jwidget. A + void * parameter named "w" is implicitly a widget. + + Widgets for a scene are arranged in a tree hierarchy; each object has a + unique parent and a number of distinct children, which they own. Widgets + destroy their children when they are destroyed. The parent of all the + widgets in the scene is called the root, it's the only one without a parent. + + Every widget has an HTML-like box model described by its geometry object. + See the jwidget_geometry type below for details. By default widgets have no + geometry to save memory, it is only created when actually used. + + The size of each widget can be controlled in two ways. Initially, the + widget's contents suggest a "natural size". The user can then restrict + acceptable sizes by specifying a size hint along with a policy. See the + jwidget_size_policy type below for details. Additionally, the user can set + stretch parameters to allow the widget to grow and occupy available space. + + Widgets usually have one of two roles; either they are "containers" for + other widgets, or they are "content widgets" that render text and images, or + take input from the user. In order to make containers easy to design, each + widget can be equipped with a "layout" that automatically arranges children + in useful ways, such as vertical lists or grids. Content widgets usually + don't have layouts. + + Polymorphic operations are defined for each widget type and accessed through + the type attribute. See the widget extension API at the end of this header + for details on polymorphic operations. + + The following attributes can be accessed by the user: + .parent (read-only) + .children[] (read-only) + .child_count (read-only) */ +typedef struct jwidget { + /* Parent and children in the widget tree */ + struct jwidget *parent; + struct jwidget **children; + + /* Location within the content-box of the parent (after layout) */ + int16_t x, y; + /* Margin-box size in pixels (after layout) */ + int16_t w, h; + /* Size hints: these are user-provided sizes, which are combined with the + size policy to determine acceptable widget dimensions */ + int16_t min_w, min_h; + int16_t max_w, max_h; + + /* Widget geometry, defaults to a fixed geometry object */ + struct jwidget_geometry *geometry; + /* Layout data, access with the jlayout_{get,set}_*() functions) */ + union { + jlayout_box layout_box; + jlayout_stack layout_stack; + jlayout_grid layout_grid; + }; + + /* Widget type, used to find polymorphic operations */ + uint8_t type; + /* Number of children */ + uint8_t child_count; + /* Number of pointers allocated in the children array */ + uint8_t child_alloc; + /* Horizontal and vertical stretch rates */ + uint stretch_x :4; + uint stretch_y :4; + + /* Type of layout (see the jlayout_type enum) */ + uint layout :4; + /* Whether stretch can go beyond the maximum size */ + uint stretch_force :1; + /* Whether the layout needs to be recomputed */ + uint dirty :1; + /* Whether the widget needs to be redrawn */ + uint update :1; + /* Whether widget is visible inside its parent */ + uint visible :1; + + uint :24; + +} jwidget; + +/* jwidget_border_style: Styles of widget borders */ +typedef enum { + /* No border */ + J_BORDER_NONE, + /* Border is a solid color */ + J_BORDER_SOLID, + /* TODO: More border styles (especially on fx-CG 50) */ + +} jwidget_border_style; + +/* jwidget_geometry: Built-in positioning and border geometry + + Every widget has a "geometry", which consists of a border and two layers of + spacing around the widget: + * The "padding" is the spacing between the widget's contents and its border + * The "border" is a visible decoration around the widget's contents + * The "margin" is the spacing between the border and the widget's edge + + For users familiar with the CSS box model, this is it. Note, however, that + unlike the common CSS property (box-sizing: border-box), JustUI counts the + margin as widget-owned space and measures widgets using the margin box. */ +typedef struct jwidget_geometry { + /* Padding (in pixels) on all four sides; access either using padding.top, + .right, .bottom and .left, or using paddings[0] through paddings[3] */ + union { + uint8_t paddings[4]; + jdirs padding; + }; + /* Width of the border on all four sides */ + union { + uint8_t borders[4]; + jdirs border; + }; + /* Size of the margin (in pixel) on all four sides */ + union { + uint8_t margins[4]; + jdirs margin; + }; + /* Border color (as in ) */ + int border_color; + /* Border style */ + jwidget_border_style border_style; + /* Background color */ + int background_color; + +} jwidget_geometry; + +/* Downwards key event: widget is notified of a key press that ocurred while it + had active focus. + -> .data.key: Key event */ +extern uint16_t JWIDGET_KEY; +/* Downwards focus-in event: the widget has just received focus */ +extern uint16_t JWIDGET_FOCUS_IN; +/* Downwards focus-out event: the widget has just lost focus */ +extern uint16_t JWIDGET_FOCUS_OUT; + +//--- +// Creation and destruction +//--- + +/* jwidget_create(): Create a widget + + This function creates a type-less widget. If you want to create labels, + buttons, input fields... you need to use the specific creation functions + such as jlabel_create(). This function only creates empty widgets, which are + primarily useful as containers. + + If a non-NULL parent is specified, then the new widget becomes a child of + that parent. The parent will then handle the positioning and sizing of the + new widget, and destroy it automatically. + + After creating a container with jwidget_create(), it is common to give it a + layout and add children to it, either with jwidget_add_child() or through + the children's constructors. + + @parent This widget's parent. + -> Returns the new widget (NULL on error). */ +jwidget *jwidget_create(void *parent); + +/* jwidget_destroy(): Destroy a widget + This function destroys the specified widget and its children. If the + destroyed widget has a parent, the parent is notified, so the widget tree + cannot become invalid. However, the layout process should be re-run to + layout the remaining scene elements. */ +void jwidget_destroy(void *w); + +//--- +// Widget tree manipulation +//--- + +/* jwidget_set_parent(): Change a widget's parent + Moves the widget from its current parent to another parent. If the widget + already had a parent, it is notified. If the new parent is NULL, the widget + is left without a parent. */ +void jwidget_set_parent(void *w, void *parent); + +/* jwidget_add_child(): Add a child at the end of a widget's child list + If the widget already had a parent, that parent is notified. */ +void jwidget_add_child(void *w, void *child); + +/* jwidget_insert_child(): Insert a child in the widget's child list + Similar to jwidget_add_child(), but the child is added at the requestd + position in the parent's child list. The position must be in the range + [0 ... w->child_count]. */ +void jwidget_insert_child(void *w, void *child, int position); + +/* jwidget_remove_child(): Remove a child from a widget + (w) must be the parent of (child). The child is left without a parent. */ +void jwidget_remove_child(void *w, void *child); + +//--- +// Sizing and stretching +//--- + +/* Functions to set the minimum width, minimum height, or both. The minimum + size can be cleared by specifying 0. */ +void jwidget_set_minimum_width(void *w, int min_width); +void jwidget_set_minimum_height(void *w, int min_height); +void jwidget_set_minimum_size(void *w, int min_width, int min_height); + +/* Functions to set the maximum width, maximum height, or both. The maximum + size can be cleared by specifying -1. */ +void jwidget_set_maximum_width(void *w, int max_width); +void jwidget_set_maximum_height(void *w, int max_height); +void jwidget_set_maximum_size(void *w, int max_width, int max_height); + +/* Functions to set both the minimum and maximum size at the same time. */ +void jwidget_set_fixed_width(void *w, int width); +void jwidget_set_fixed_height(void *w, int height); +void jwidget_set_fixed_size(void *w, int width, int height); + +/* jwidget_set_stretch(): Set the stretch factors for a widget + + Stretch factors indicate how much a widget wants to grow. Stretch is used in + all size policies except the fixed one. Due to storage limits, the stretch + factors should be in the range [0 ... 15]. + + The last parameter indicates whether to allow stretching beyond the maximum + size of the widget. In almost all situations this should be false. However, + in some cases you might want to completely ignore natural size and allocate + space based uniquely on stretch factors. In this case, you can set a fixed + size of 0 and enable stretching beyond limits. */ +void jwidget_set_stretch(void *w, int stretch_x, int stretch_y, + bool stretch_beyond_limits); + +//--- +// Geometry +//-- + +/* jwidget_geometry_r(): Get read-only access to a widget's geometry + + This function returns a read-only pointer to the specified widget's + geometry. Because most widgets don't have customized geometry, JustUI + doesn't store any data until the geometry is modified, to save memory. This + is why it makes sense to separate read-only and read-write accesses. + + For widgets without customized geometry, this functions returns a pointer to + a fixed constant geometry with zero padding, border and margin. */ +jwidget_geometry const *jwidget_geometry_r(void *w); + +/* jwidget_geometry_rw(): Get read-write access to a widget's geometry + + This function returns a read-write pointer to the specified widget's + geometry. For widgets that don't have customized geometry yet, this will + duplicate the default settings. This avoids memory consumption on widgets + that don't need custom geometry. + + Returns NULL if duplication fails because of memory exhaustion. */ +jwidget_geometry *jwidget_geometry_rw(void *w); + +/* jwidget_set_border(): Set a uniform border around a widget + This is a shorthand to set (border_style), (border_color), and a uniform + border width on a widget's geometry. */ +void jwidget_set_border(void *w, jwidget_border_style s, int width, int color); + +/* jwidget_set_padding(): Set all padding distances around a widget */ +void jwidget_set_padding(void *w, int top, int right, int bottom, int left); + +/* jwidget_set_margin(): Set all margin distances around a widget */ +void jwidget_set_margin(void *w, int top, int right, int bottom, int left); + +/* jwidget_set_background(): Set the widget's background color + The default is C_NONE. The background covers content and padding. */ +void jwidget_set_background(void *w, int color); + +//--- +// Layout +//--- + +/* jwidget_layout_dirty(): Check whether the tree needs to be laid out + + This function checks the dirty bit of every widget in the tree. If any + widget changes size, the whole tree needs to be laid out again (there are + possible optimizations, but they are not implemented yet). */ +bool jwidget_layout_dirty(void *scene_root); + +/* jwidget_layout(): Layout a widget tree + + This function lays out the specified widget (computing its size and the + position of its children) and its children recursively. Because this is a + two-phase process going from the children to their parents and then from the + parents to their children, it only makes sense to layout the whole tree at + once. You should thus call jwidget_layout() only with your scene root. + + A scene's layout should always be up-to-date before rendering. There is no + need to layout at every frame (this would be a waste of resources), but you + need to layout after doing any of the following things: + + * Creating the scene + * Adding, removing, or moving visible children around + * Changing a widget's contents in a way that affects its natural size + * Changing geometry or layout parameters on a widget + + The layout process determines the size and position of every widget in the + tree. Thus, if you need to access this size and position information, you + need to keep the layout up-to-date before doing it. */ +void jwidget_layout(void *scene_root); + +/* jwidget_width(): With of a widget's content box + jwidget_height(): Height of a widget's content box + + These functions return the size of the content box of a widget. The content + box does not comprise the geometry (padding, border and margins). These + dimensions are known only after layout; calling these functions when the + layout is not up-to-date will return funny results. */ +int jwidget_content_width(void *w); +int jwidget_content_height(void *w); + +/* jwidget_full_width(): Width of a widget's margin box + jwidget_full_height(): Height of a widget's margin box + + These functions return the whole size of the margin box of a widgets; this + includes the contents, padding, border and margins. These functions only + make sense to call when the layout is up-to-date. */ +int jwidget_full_width(void *w); +int jwidget_full_height(void *w); + +//--- +// Rendering +//--- + +/* jwidget_visible(): Whether widget is visible + A non-visible widget occupies no space and is not rendered, as if it did not + exist at all. */ +bool jwidget_visible(void *w); + +/* jwidget_set_visible(): Hide or show a widget */ +void jwidget_set_visible(void *w, bool visible); + +/* jwidget_needs_update(): Check whether the tree needs to be re-rendered + + If this function returns true, you should re-render the tree. Aditionally, + if jwidget_layout_dirty() returns true, you should re-layout the tree and + repaint it. jscene_render() will layout automatically if needed, so you just + need to call it if either function returns true. + + When using jscene_run(), a JSCENE_REPAINT event will be emitted in this + exact conditions, so just jscene_render() upon JSCENE_REPAINT. */ +bool jwidget_needs_update(void *w); + +/* jwidget_render(): Render a widget + This function renders the widget. The specified location (x,y) is the + top-left corner of the margin box of the widget. There is no clipping. + Unlike jscene_render(), this function does not automatically layout the + widgets if there has been changes. */ +void jwidget_render(void *w, int x, int y); + +//--- +// Misc +//--- + +/* jwidget_type(): Get a widget's human-readable type name + This is the name specified in the jwidgetPoly structure for the type. */ +char const *jwidget_type(void *w); + +#endif /* _J_JWIDGET */ diff --git a/include/justui/p/vec.h b/include/justui/p/vec.h new file mode 100644 index 0000000..ddb539b --- /dev/null +++ b/include/justui/p/vec.h @@ -0,0 +1,49 @@ +//--- +// JustUI.util.vector: Dynamic arrays +//--- + +#ifndef _J_UTIL_VECTOR +#define _J_UTIL_VECTOR + +#include + +/* Vector metadata, on four bytes; the pointer is included, but the user will + have aliased union access from the macro definition */ +typedef struct { + /* Pointer to elements of the vector */ + void *data; + /* Number of elements used in the vector */ + uint16_t size; + /* Number of free elements (never more than 255) */ + uint8_t free; + /* Element size, in bytes */ + uint8_t elsize; + +} vec_t; + +/* Macro to declare a vector and have typed access to its data */ +#define DECLARE_VEC(type, name) \ + union { \ + type *name; \ + vec_t name ## _vec; \ + }; + +/* Initialize a vector; the element size can be specified with the typed + pointer, like this: vec_init(&name_vector, sizeof *name); */ +void vec_init(vec_t *v, size_t elsize); + +/* Free a vector's data, can also be used to clear all elements */ +void vec_clear(vec_t *v); + +//--- +// Size management +//--- + +/* Add elements to the vector. The elements should be assigned by the owner + through the typed pointer, this only allocates and maintains the size. */ +bool vec_add(vec_t *v, size_t elements); + +/* Remove elements from the vector */ +bool vec_remove(vec_t *v, size_t elements); + +#endif /* _J_UTIL_VECTOR */ diff --git a/src/jfkeys.c b/src/jfkeys.c new file mode 100644 index 0000000..c393518 --- /dev/null +++ b/src/jfkeys.c @@ -0,0 +1,179 @@ +#include +#include +#include +#include + +/* Type identified for jfkeys */ +static int jfkeys_type_id = -1; + +#ifdef FX9860G +jfkeys *jfkeys_create(bopti_image_t const *img, void *parent) +{ + if(jfkeys_type_id < 0) return NULL; + + jfkeys *f = malloc(sizeof *f); + jwidget_init(&f->widget, jfkeys_type_id, parent); + jfkeys_set(f, img); + return f; +} + +void jfkeys_set(jfkeys *f, bopti_image_t const *img) +{ + f->img = img; + f->level = 0; +} +#endif /* FX9860G */ + +#ifdef FXCG50 +jfkeys *jfkeys_create(char const *labels, void *parent) +{ + if(jfkeys_type_id < 0) return NULL; + + jfkeys *f = malloc(sizeof *f); + jwidget_init(&f->widget, jfkeys_type_id, parent); + jfkeys_set(f, labels); + return f; +} + +void jfkeys_set(jfkeys *f, char const *labels) +{ + f->labels = labels; + f->level = 0; +} + +/* get_level(): Find the level inside a function definition */ +static char const *get_level(char const *labels, int level) +{ + /* Navigate to level */ + while(level > 0) labels = strchrnul(labels, '|'); + return (*labels == 0) ? NULL : labels; +} + +/* get_label(): Find a key within a level */ +static char const *get_label(char const *level, int key, size_t *len) +{ + int current_key = 0; + + while(current_key <= key) { + /* We reached the end of the level without finding the key */ + if(*level == 0 || *level == '|') return NULL; + /* Next entry */ + if(*level == ';') { + current_key++; + level++; + continue; + } + /* Found contents */ + if(current_key == key) { + *len = 0; + while(level[*len] != ';' && level[*len] != '|' && level[*len]) + (*len)++; + return level; + } + + level++; + } + + return NULL; +} +#endif /* FXCG50 */ + +//--- +// Polymorphic widget operations +//--- + +static void jfkeys_poly_csize(void *f0) +{ + jfkeys *f = f0; + + #ifdef FX9860G + f->widget.w = 128; + f->widget.h = 8; + #endif + + #ifdef FXCG50 + f->widget.w = 396; + f->widget.h = 17; + #endif +} + +static void jfkeys_poly_render(void *f0, int base_x, int y) +{ + jfkeys *f = f0; + + #ifdef FX9860G + dsubimage(base_x, y, f->img, 0, 9*f->level, f->img->width, 8, DIMAGE_NONE); + #endif + + #ifdef FXCG50 + font_t const *old_font = dfont(dfont_default()); + char const *level = get_level(f->labels, f->level); + if(!level) return; + + for(int position = 0; position < 6; position++) { + size_t length = 0; + char const *text = get_label(level, position, &length); + if(!text || (*text != '.' && *text != '/' && *text != '@' + && *text != '#')) continue; + + int x = base_x + 4 + 65 * position; + int w = 63; + int color = (text[0] == '#') ? C_BLACK : C_WHITE; + + if(text[0] == '.') { + drect(x, y, x + w - 1, y + 14, C_BLACK); + } + if(text[0] == '/' || text[0] == '@') { + dline(x + 1, y, x + w - 2, y, C_BLACK); + dline(x + 1, y + 14, x + w - 2, y + 14, C_BLACK); + drect(x, y + 1, x + w - 1, y + 13, C_BLACK); + } + if(text[0] == '/') { + dline(x + w - 1, y + 10, x + w - 5, y + 14, C_WHITE); + dline(x + w - 1, y + 11, x + w - 4, y + 14, C_WHITE); + dline(x + w - 1, y + 12, x + w - 3, y + 14, C_WHITE); + dline(x + w - 1, y + 13, x + w - 2, y + 14, C_WHITE); + } + if(text[0] == '#') { + dline(x + 1, y, x + w - 2, y, C_BLACK); + dline(x + 1, y + 14, x + w - 2, y + 14, C_BLACK); + drect(x, y + 1, x + 1, y + 13, C_BLACK); + drect(x + w - 2, y + 1, x + w - 1, y + 13, C_BLACK); + } + + dtext_opt(x + (w >> 1), y + 3, color, C_NONE, DTEXT_CENTER, DTEXT_TOP, + text + 1, length - 1); + } + + dfont(old_font); + #endif /* FXCG50 */ +} + +int jfkeys_level(jfkeys *f) +{ + return f->level; +} + +void jfkeys_set_level(jfkeys *f, int level) +{ + if(f->level == level) return; + f->level = level; + f->widget.update = 1; +} + +/* jfkeys type definition */ +static jwidget_poly type_jfkeys = { + .name = "jfkeys", + .csize = jfkeys_poly_csize, + .layout = NULL, + .render = jfkeys_poly_render, + .event = NULL, + .destroy = NULL, +}; + +/* Type registration */ +__attribute__((constructor(1005))) +static void j_register_jfkeys(void) +{ + jfkeys_type_id = j_register_widget(&type_jfkeys, "jwidget"); +} diff --git a/src/jinput.c b/src/jinput.c new file mode 100644 index 0000000..0f847cb --- /dev/null +++ b/src/jinput.c @@ -0,0 +1,351 @@ +#include +#include +#include +#include "util.h" + +#include +#include +#include + +/* Type identifier for jinput */ +static int jinput_type_id = -1; + +/* Events */ +uint16_t JINPUT_VALIDATED; +uint16_t JINPUT_CANCELED; + +/* Input modes. Valid combinations are: + * (SHIFT | ALPHA)? | SIMUL + * (SHIFT or SHIFT_LOCK)? | (ALPHA or ALPHA_LOCK)? */ +enum { + JINPUT_FLAT = 0x00, + JINPUT_SHIFT = 0x01, + JINPUT_ALPHA = 0x02, + JINPUT_SHIFT_LOCK = 0x04, + JINPUT_ALPHA_LOCK = 0x08, + JINPUT_SIMUL = 0x10, +}; + +/* Mode indicators and their size */ +extern bopti_image_t j_img_input_modes; +#ifdef FX9860G +#define JINPUT_INDICATOR 9 +#endif +#ifdef FXCG50 +#define JINPUT_INDICATOR 12 +#endif + +jinput *jinput_create(char const *prompt, size_t length, void *parent) +{ + if(jinput_type_id < 0) return NULL; + + jinput *i = malloc(sizeof *i); + if(!i) return NULL; + + jwidget_init(&i->widget, jinput_type_id, parent); + + i->color = C_BLACK; + jinput_set_font(i, NULL); + i->prompt = (prompt ? prompt : ""); + + i->text = malloc(length + 1); + i->text[0] = 0; + i->size = 1; + i->max = length + 1; + i->cursor = -1; + + i->mode = JINPUT_FLAT; + i->timer = -1; + + return i; +} + +void jinput_set_text_color(jinput *i, int color) +{ + i->color = color; + i->widget.update = 1; +} + +void jinput_set_font(jinput *i, font_t const *font) +{ + if(!font) font = dfont_default(); + i->font = font; + i->widget.dirty = 1; +} + +void jinput_set_prompt(jinput *i, char const *prompt) +{ + i->prompt = prompt; + i->widget.dirty = 1; +} + +//--- +// Input helpers +//--- + +static void insert_str(jinput *i, char const *str, size_t n) +{ + if(i->size + n > i->max) return; + + /* Insert at cursor_pos, shift everything else right n places */ + for(int k = i->size - 1; k >= i->cursor; k--) + i->text[k + n] = i->text[k]; + + for(int k = 0; k < (int)n; k++) + i->text[i->cursor + k] = str[k]; + + i->size += n; + i->cursor += n; +} + +static void insert_code_point(jinput *i, uint32_t p) +{ + char str[4] = { 0x00, 0x80, 0x80, 0x80 }; + size_t size = 0; + + if(p <= 0x7f) { + str[0] = p; + size = 1; + } + else if(p <= 0x7ff) { + str[0] = 0xc0 | (p >> 6); + str[1] |= (p & 0x3f); + size = 2; + } + else if(p <= 0xffff) { + str[0] = 0xe0 | (p >> 12); + str[1] |= ((p > 6) & 0x3f); + str[2] |= (p & 0x3f); + size = 3; + } + else { + str[0] = 0xf0 | (p >> 18); + str[1] |= ((p >> 12) & 0x3f); + str[2] |= ((p >> 6) & 0x3f); + str[3] |= (p & 0x3f); + size = 4; + } + + insert_str(i, str, size); +} + +static int previous(char const *str, int position) +{ + if(position == 0) + return position; + + while((str[--position] & 0xc0) == 0x80) {} + return position; +} + +static int next(char const *str, int position) +{ + if(str[position] == 0) return position; + + while((str[++position] & 0xc0) == 0x80) {} + return position; +} + +static void delete(jinput *i) +{ + int prev = previous(i->text, i->cursor); + int diff = i->cursor - prev; + if(!diff) return; + + /* Move everything from (i->cursor) to (prev) */ + for(int k = prev; k < i->size - diff; k++) { + i->text[k] = i->text[k + diff]; + } + + i->size -= diff; + i->cursor = prev; +} + +static void clear(jinput *i) +{ + i->text[0] = 0; + i->size = 1; + i->cursor = 0; +} + +char const *jinput_value(jinput *i) +{ + return i->text; +} + +void jinput_clear(jinput *i) +{ + clear(i); +} + +//--- +// Polymorphic widget operations +//--- + +static void jinput_poly_csize(void *i0) +{ + jinput *i = i0; + int w, h; + + dsize(i->prompt, i->font, &w, &h); + + i->widget.w = w + 16 + JINPUT_INDICATOR; + i->widget.h = max(h, i->font->data_height); + i->widget.h = max(i->widget.h, (int)j_img_input_modes.height); +} + +static void jinput_poly_render(void *i0, int x, int y) +{ + jinput *i = i0; + font_t const *old_font = dfont(i->font); + int prompt_w, cursor_w, h; + + dsize(i->prompt, i->font, &prompt_w, &h); + dtext(x, y, i->color, i->prompt); + + if(i->text) { + dtext(x + prompt_w, y, i->color, i->text); + } + if(i->cursor >= 0) { + if(i->text) dnsize(i->text, i->cursor, i->font, &cursor_w, NULL); + else cursor_w = 0; + + int cursor_x = x + prompt_w + cursor_w; + dline(cursor_x, y, cursor_x, y + h - 1, i->color); + #ifdef FXCG50 + dline(cursor_x + 1, y, cursor_x + 1, y + h - 1, i->color); + #endif + } + + dfont(old_font); + + /* Mode indicators. Only one indicator is needed most of the time, unless + one level is 1 and the other is 2. In this case, the temporary level (of + value 1) is displayed instead. This leaves only 8 cases to deal with. */ + if(i->mode == 0) return; + int sl = (i->mode & JINPUT_SHIFT_LOCK) ? 2 : (i->mode & JINPUT_SHIFT) != 0; + int al = (i->mode & JINPUT_ALPHA_LOCK) ? 2 : (i->mode & JINPUT_ALPHA) != 0; + int mode = (sl + al == 3) ? 5 : ((sl | al) + (al ? (sl ? 4 : 2) : 0)); + + x += jwidget_content_width(i) - JINPUT_INDICATOR; + dsubimage(x, y, &j_img_input_modes, (JINPUT_INDICATOR + 1) * (mode - 1), 0, + JINPUT_INDICATOR, j_img_input_modes.height, DIMAGE_NONE); +} + +static bool jinput_poly_event(void *i0, jevent e) +{ + jinput *i = i0; + + if(e.type == JWIDGET_FOCUS_IN) { + i->cursor = i->size - 1; + i->mode = 0; + i->widget.update = 1; + } + + if(e.type == JWIDGET_FOCUS_OUT) { + i->cursor = -1; + i->mode = 0; + i->widget.update = 1; + } + + if(e.type == JWIDGET_KEY) { + key_event_t ev = e.key; + + /* Releasing modifiers */ + if(ev.type == KEYEV_UP && ev.key == KEY_SHIFT) { + if(i->mode & JINPUT_SIMUL) i->mode &= ~JINPUT_SHIFT; + i->widget.update = 1; + return true; + } + if(ev.type == KEYEV_UP && ev.key == KEY_ALPHA) { + if(i->mode & JINPUT_SIMUL) i->mode &= ~JINPUT_ALPHA; + i->widget.update = 1; + return true; + } + if(i->mode == JINPUT_SIMUL) i->mode = 0; + + if(ev.type != KEYEV_DOWN) return false; + + if(ev.key == KEY_EXE) { + jevent e = { .source = i, .type = JINPUT_VALIDATED }; + jwidget_emit(i, e); + } + else if(ev.key == KEY_EXIT) { + clear(i); + jevent e = { .source = i, .type = JINPUT_CANCELED }; + jwidget_emit(i, e); + } + else if(ev.key == KEY_RIGHT) { + i->cursor = next(i->text, i->cursor); + } + else if(ev.key == KEY_LEFT) { + i->cursor = previous(i->text, i->cursor); + } + else if(ev.key == KEY_DEL) { + delete(i); + } + else if(ev.key == KEY_ACON) { + clear(i); + } + else if(ev.key == KEY_SHIFT) { + if(i->mode & JINPUT_SHIFT_LOCK) + i->mode ^= JINPUT_SHIFT_LOCK; + else if(i->mode & JINPUT_SHIFT && !(i->mode & JINPUT_SIMUL)) + i->mode ^= JINPUT_SHIFT | JINPUT_SHIFT_LOCK; + else + i->mode |= JINPUT_SHIFT; + } + else if(ev.key == KEY_ALPHA) { + if(i->mode & JINPUT_ALPHA_LOCK) + i->mode ^= JINPUT_ALPHA_LOCK; + else if(i->mode & JINPUT_ALPHA && !(i->mode & JINPUT_SIMUL)) + i->mode ^= JINPUT_ALPHA | JINPUT_ALPHA_LOCK; + else + i->mode |= JINPUT_ALPHA; + } + else { + uint32_t code_point = keymap_translate(ev.key, + (i->mode & JINPUT_SHIFT) || (i->mode & JINPUT_SHIFT_LOCK), + (i->mode & JINPUT_ALPHA) || (i->mode & JINPUT_ALPHA_LOCK) + ); + if(code_point) { + insert_code_point(i, code_point); + /* Mark modifiers as simultaneous if they're not released */ + if((keydown(KEY_SHIFT) && !(i->mode & JINPUT_SHIFT_LOCK)) || + (keydown(KEY_ALPHA) && !(i->mode & JINPUT_ALPHA_LOCK))) + i->mode |= JINPUT_SIMUL; + /* Remove modifiers otherwise */ + else i->mode &= ~(JINPUT_SHIFT | JINPUT_ALPHA); + } + else return false; + } + + i->widget.update = 1; + } + + return true; +} + +static void jinput_poly_destroy(void *i0) +{ + jinput *i = i0; + free(i->text); +} + +/* jinput type definition */ +static jwidget_poly type_jinput = { + .name = "jinput", + .csize = jinput_poly_csize, + .layout = NULL, + .render = jinput_poly_render, + .event = jinput_poly_event, + .destroy = jinput_poly_destroy, +}; + +__attribute__((constructor(1003))) +static void j_register_jinput(void) +{ + jinput_type_id = j_register_widget(&type_jinput, "jwidget"); + JINPUT_VALIDATED = j_register_event(); + JINPUT_CANCELED = j_register_event(); +} diff --git a/src/jlabel.c b/src/jlabel.c new file mode 100644 index 0000000..1ffbfde --- /dev/null +++ b/src/jlabel.c @@ -0,0 +1,360 @@ +#include +#include +#include + +#include +#include +#include +#include +#include + +/* Type identifier for jlabel */ +static int jlabel_type_id = -1; + +jlabel *jlabel_create(char const *text, void *parent) +{ + if(jlabel_type_id < 0) return NULL; + + jlabel *l = malloc(sizeof *l); + if(!l) return NULL; + + jwidget_init(&l->widget, jlabel_type_id, parent); + + l->block_halign = J_ALIGN_LEFT; + l->block_valign = J_ALIGN_MIDDLE; + l->text_align = J_ALIGN_LEFT; + + l->line_spacing = 1; + l->color = C_BLACK; + l->font = NULL; + + l->text = text; + l->owns_text = false; + + vec_init(&l->breaks_vec, sizeof *l->breaks); + + return l; +} + +/* Revert the label to an empty string. */ +static void remove_text(jlabel *l) +{ + if(l->owns_text) free((char *)l->text); + l->text = NULL; + l->owns_text = false; + vec_clear(&l->breaks_vec); +} + +/* Add a breaking point at the specified string offset. */ +static void add_break(jlabel *l, uint16_t br) +{ + if(!vec_add(&l->breaks_vec, 1)) return; + l->breaks[l->breaks_vec.size - 1] = br; +} + +static char const *word_boundary(char const *start, char const *cursor, bool + look_ahead) +{ + char const *str = cursor; + + /* Look for a word boundary behind the cursor */ + while(1) { + /* Current position is end-of-string: suitable */ + if(*str == 0) return str; + /* Current position is start of string: bad */ + if(str <= start) break; + + /* Look for heteregoneous neighboring characters */ + int space_l = (str[-1] == ' ' || str[-1] == '\n'); + int space_r = (str[0] == ' ' || str[0] == '\n'); + + if(!space_l && space_r) return str; + str--; + } + + /* If we can't look ahead, return the starting position to force a cut */ + if(!look_ahead) return cursor; + str++; + + /* Otherwise, look ahead */ + while(*str) { + int space_l = (str[-1] == ' ' || str[-1] == '\n'); + int space_r = (str[0] == ' ' || str[0] == '\n'); + + if(!space_l && space_r) return str; + str++; + } + + /* If there's really nothing, return end-of-string */ + return str; +} + +//--- +// Text manipulation +//--- + +void jlabel_set_text(jlabel *l, char const *text) +{ + remove_text(l); + l->text = text; + l->owns_text = false; + l->widget.dirty = 1; +} + +int jlabel_asprintf(jlabel *l, char const *format, ...) +{ + remove_text(l); + char *text = NULL; + + va_list args; + va_start(args, format); + int count = vasprintf(&text, format, args); + va_end(args); + + l->text = text; + l->owns_text = (text != NULL && count >= 0); + l->widget.dirty = 1; + return count; +} + +int jlabel_snprintf(jlabel *l, size_t size, char const *format, ...) +{ + char *text = malloc(size + 1); + if(!text) return -1; + + va_list args; + va_start(args, format); + int count = vsnprintf(text, size, format, args); + va_end(args); + + l->text = text; + l->owns_text = true; + l->widget.dirty = 1; + return count; +} + +char const *jlabel_text(jlabel *l) +{ + return l->text; +} + +//--- +// Property setters +//--- + +void jlabel_set_block_alignment(jlabel *l, jalign horz, jalign vert) +{ + l->block_halign = horz; + l->block_valign = vert; + l->widget.dirty = 1; +} + +void jlabel_set_text_alignment(jlabel *l, jalign align) +{ + l->text_align = align; + l->widget.dirty = 1; +} + +void jlabel_set_alignment(jlabel *l, jalign horz) +{ + l->block_halign = horz; + l->text_align = horz; + l->widget.dirty = 1; +} + +void jlabel_set_line_spacing(jlabel *l, int line_spacing) +{ + l->line_spacing = line_spacing; + l->widget.dirty = 1; +} + +void jlabel_set_wrap_mode(jlabel *l, jwrapmode mode) +{ + l->wrap_mode = mode; + l->widget.dirty = 1; +} + +void jlabel_set_text_color(jlabel *l, int color) +{ + l->color = color; + l->widget.update = 1; +} + +void jlabel_set_font(jlabel *l, font_t const *font) +{ + /* A NULL font is acceptable here, dfont() will handle it later */ + l->font = font; + l->widget.dirty = 1; +} + +//--- +// Polymorphic widget operations +//--- + +static void jlabel_poly_csize(void *l0) +{ + jlabel *l = l0; + jwidget *w = &l->widget; + w->w = 0; + w->h = 0; + + font_t const *old_font = dfont(l->font); + + /* Cut at newline characters */ + char const *str = l->text; + while(*str) + { + char const *end_of_line = strchrnul(str, '\n'); + int line_w, line_h; + + dnsize(str, end_of_line - str, NULL, &line_w, &line_h); + w->w = max(w->w, line_w); + w->h += (w->h > 0 ? l->line_spacing : 0) + line_h; + + str = end_of_line + (*end_of_line == '\n'); + } + + dfont(old_font); +} + +static void jlabel_poly_layout(void *l0) +{ + jlabel *l = l0; + vec_clear(&l->breaks_vec); + + int cw = jwidget_content_width(l); + + /* Configure the font for dnsize() below; we can't pass l->font directly as + NULL would default to the current font rather than gint's default */ + font_t const *old_font = dfont(l->font); + + /* Determine the end of each line, influenced by the wrap mode */ + char const *str = l->text; + while(*str) + { + /* Start of line */ + add_break(l, str - l->text); + + /* A "\n" forces a newline in all wrap omdes */ + char const *end_of_line = strchrnul(str, '\n'); + + /* Also consider word or letters boundaries */ + if(l->wrap_mode != J_WRAP_NONE) { + char const *end_of_widget = drsize(str, NULL, cw, NULL); + + if(end_of_widget < end_of_line) { + /* In WRAP_LETTER mode, stop exactly at the limiting letter */ + if(l->wrap_mode == J_WRAP_LETTER) + end_of_line = end_of_widget; + /* In WRAP_WORD, try to find a word boundary behind; if this + fails, we fall back to end_of_line */ + else if(l->wrap_mode == J_WRAP_WORD) + end_of_line = word_boundary(str, end_of_widget, false); + /* In WRAP_WORD_ONLY, we want a word boundary, even if ahead */ + else if(l->wrap_mode == J_WRAP_WORD_ONLY) + end_of_line = word_boundary(str, end_of_widget, true); + } + } + + char const *next_start = end_of_line + (*end_of_line == '\n'); + + /* Skip trailing spaces on this line */ + while(end_of_line > str && end_of_line[-1] == ' ') + end_of_line--; + /* Skip leading spaces on the next line */ + while(next_start[0] == ' ') + next_start++; + + add_break(l, end_of_line - l->text); + + /* Compute the length of this line (this is needed even if drsize() has + been used since spaces may have been removed) */ + int line_width; + dnsize(str, end_of_line - str, NULL, &line_width, NULL); + + l->block_width = max(l->block_width, line_width); + str = next_start; + } + + dfont(old_font); +} + +static void jlabel_poly_render(void *l0, int x, int y) +{ + jlabel *l = l0; + font_t const *old_font = dfont(l->font); + /* Set the font again; this returns l->font most of the time, except when + l->font is NULL, in which case it gives the default font's address*/ + font_t const *f = dfont(l->font); + + /* Available content width and height */ + int cw = jwidget_content_width(l); + int ch = jwidget_content_height(l); + + /* Position the block vertically */ + + int lines = l->breaks_vec.size / 2; + int block_height = lines * (f->line_height + l->line_spacing) - + l->line_spacing; + + if(l->block_valign == J_ALIGN_MIDDLE) + y += (ch - block_height) / 2; + if(l->block_valign == J_ALIGN_BOTTOM) + y += ch - block_height; + + /* Position the block horizontally */ + + if(l->block_halign == J_ALIGN_CENTER) + x += (cw - l->block_width) / 2; + if(l->block_halign == J_ALIGN_RIGHT) + x += cw - l->block_width; + + /* Render lines */ + + for(int i = 0; i < lines; i++) { + char const *str = l->text + l->breaks[2*i]; + int line_length = l->breaks[2*i+1] - l->breaks[2*i]; + + /* Handle horizontal alignment */ + int dx = 0; + if(l->text_align != J_ALIGN_LEFT) { + int line_width; + dnsize(str, line_length, NULL, &line_width, NULL); + + if(l->text_align == J_ALIGN_CENTER) + dx = (l->block_width - line_width) / 2; + if(l->text_align == J_ALIGN_RIGHT) + dx = l->block_width - line_width; + } + + dtext_opt(x + dx, y, l->color, C_NONE, DTEXT_LEFT, DTEXT_TOP, + str, line_length); + + y += f->line_height + l->line_spacing; + str = l->text + l->breaks[i]; + } + + dfont(old_font); +} + +static void jlabel_poly_destroy(void *l) +{ + remove_text(l); +} + +/* jlabel type definition */ +static jwidget_poly type_jlabel = { + .name = "jlabel", + .csize = jlabel_poly_csize, + .layout = jlabel_poly_layout, + .render = jlabel_poly_render, + .event = NULL, + .destroy = jlabel_poly_destroy, +}; + +/* Type registration */ +__attribute__((constructor(1002))) +static void j_register_jlabel(void) +{ + jlabel_type_id = j_register_widget(&type_jlabel, "jwidget"); +} diff --git a/src/jlayout_box.c b/src/jlayout_box.c new file mode 100644 index 0000000..539961e --- /dev/null +++ b/src/jlayout_box.c @@ -0,0 +1,349 @@ +#include +#include +#include +#include "jlayout_p.h" +#include "util.h" + +#include + +//--- +// Flexbox-like box layout +//--- + +jlayout_box *jlayout_get_hbox(void *w0) +{ + J_CAST(w) + return (w->layout == J_LAYOUT_HBOX) ? &w->layout_box : NULL; +} + +jlayout_box *jlayout_get_vbox(void *w0) +{ + J_CAST(w) + return (w->layout == J_LAYOUT_VBOX) ? &w->layout_box : NULL; +} + +jlayout_box *jlayout_set_hbox(void *w0) +{ + J_CAST(w) + w->layout = J_LAYOUT_HBOX; + jlayout_box *l = &w->layout_box; + + l->spacing = 0; + return l; +} + +jlayout_box *jlayout_set_vbox(void *w0) +{ + J_CAST(w) + w->layout = J_LAYOUT_VBOX; + jlayout_box *l = &w->layout_box; + + l->spacing = 0; + return l; +} + +void jlayout_box_csize(void *w0) +{ + /* Compute as if the layout is vertical first */ + J_CAST(w) + jlayout_box *l = &w->layout_box; + int horiz = (w->layout == J_LAYOUT_HBOX); + + int main = 0; + int cross = 0; + + for(int k = 0; k < w->child_count; k++) { + jwidget *child = w->children[k]; + if(!child->visible) continue; + jwidget_msize(child); + + main += (k > 0 ? l->spacing : 0) + (horiz ? child->w : child->h); + cross = max(cross, (horiz ? child->h : child->w)); + } + + if(horiz) { + w->w = main; + w->h = cross; + } + else { + w->w = cross; + w->h = main; + } +} + +//--- +// Distribution system +// +// The basic idea of space redistribution is to give each widget extra space +// proportional to their stretch rates in the relevant direction. However, the +// addition of maximum size constraints means that widgets can decline some of +// the extra space being allocated. +// +// This system defines the result of expansion as a function of the "expansion +// factor". As the expansion factor increases, every widget stretches at a +// speed proportional to its stretch rate, until it reaches its maximum size. +// +// Extra widget size +// | +// + .-------- Maximum size +// | .` +// | .` <- Slope: widget stretch rate +// |.` +// 0 +-------+------> Expansion factor +// 0 ^ +// Breaking point +// +// The extra space allocated to widgets is the sum of this function for every +// widget considered for expansion. Since every widget has a possibly different +// breaking point, a maximal interval of expansion factor that has no breaking +// point is called a "run". During each run, the slope for the total space +// remains constant, and a unit of expansion factor corresponds to one pixel +// being allocated in the container. Thus, whenever the expansion factor +// increases of (slope), every widget (w) gets (w->stretch) new pixels. +// +// The functions below simulate the expansion by determining the breaking +// points of the widgets and allocating extra space during each run. Once the +// total extra space allocated reaches the available space, simulation stops +// and the allocation is recorded by assigning actual size to widgets. +//--- + +/* This "expansion" structure tracks information relating to a single child + widget during the space distribution process. */ +typedef struct { + /* Child index */ + uint8_t id; + /* Stretch rate, sum of stretch rates is the "slope" */ + uint8_t stretch; + /* Maximum size augmentation */ + int16_t max; + /* Extra space allocate in the previous runs, in pixels */ + float allocated; + /* Breaking point for the current run, as a number of pixels to distribute + to the whole system */ + float breaking_point; +} exp_t; + +/* Determine whether a widget can expand any further. */ +static bool can_expand(exp_t *e) +{ + return (e->stretch > 0 && e->allocated < e->max); +} + +/* Compute the slope for the current run. */ +static uint compute_slope(exp_t elements[], size_t n) +{ + uint slope = 0; + for(size_t i = 0; i < n; i++) { + if(can_expand(&elements[i])) slope += elements[i].stretch; + } + return slope; +} + +/* Compute the breaking point for every expanding widget. Returns the amount of + pixels to allocate in order to reach the next breaking point. */ +static float compute_breaking_points(exp_t elements[], size_t n, uint slope) +{ + float closest = HUGE_VALF; + + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + if(!can_expand(e)) continue; + + /* Up to (e->max - e->allocated) pixels can be added to this widget. + With the factor of (slope / e->stretch), we get the number of pixels + to add to the container in order to reach the threshold. */ + e->breaking_point = (e->max - e->allocated) * (slope / e->stretch); + closest = min(e->breaking_point, closest); + } + + return closest; +} + +/* Allocate floating-point space to widgets. This is the core of the + distribution system, it produces (e->allocated) for every element. */ +static void allocate_space(exp_t elements[], size_t n, float available) +{ + /* One iteration per run */ + while(available > 0) { + /* Slope for this run; if zero, no more widget can grow */ + uint slope = compute_slope(elements, n); + if(!slope) break; + + /* Closest breaking point, amount of space to distribute this run */ + float breaking = compute_breaking_points(elements, n, slope); + float run_budget = min(breaking, available); + + /* Give everyone their share of run_budget */ + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + if(!can_expand(e)) continue; + + e->allocated += (run_budget * e->stretch) / slope; + + /* Avoid floating-point errors when reaching the maximum size: test + (e->breaking) instead of (e->allocated), and assign (e->max) to + eliminate risks of rounding down */ + if(e->breaking_point == run_budget) e->allocated = e->max; + } + + available -= run_budget; + } +} + +/* Stable insertion sort: order children by decreasing fractional allocation */ +static void sort_by_fractional_allocation(exp_t elements[], size_t n) +{ + for(size_t spot = 0; spot < n - 1; spot++) { + /* Find the element with the max fractional value in [spot..size] */ + float max_frac = 0; + int max_frac_who = -1; + + for(size_t i = spot; i < n; i++) { + exp_t *e = &elements[i]; + + float frac = e->allocated - floorf(e->allocated); + + if(max_frac_who < 0 || frac > max_frac) { + max_frac = frac; + max_frac_who = i; + } + } + + /* Give that element the spot */ + exp_t temp = elements[spot]; + elements[spot] = elements[max_frac_who]; + elements[max_frac_who] = temp; + } +} + +/* Round allocations so that they add up to the available space */ +static void round_allocations(exp_t elements[], size_t n, int available_space) +{ + /* Prepare to give everyone the floor of their allocation */ + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + available_space -= floorf(e->allocated); + } + + /* Sort by decreasing fractional allocation then add one extra pixel to + the (available_space) children with highest fractional allocation */ + sort_by_fractional_allocation(elements, n); + + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + e->allocated = floorf(e->allocated); + + if(can_expand(e) && (int)i < available_space) e->allocated += 1; + } +} + +void jlayout_box_apply(void *w0) +{ + J_CAST(w) + jlayout_box const *l = &w->layout_box; + int horiz = (w->layout == J_LAYOUT_HBOX); + + if(!w->child_count) return; + + /* Content width and height */ + int cw = jwidget_content_width(w); + int ch = jwidget_content_height(w); + /* Allocatable width and height (which excludes spacing) */ + int total_spacing = (w->child_count - 1) * l->spacing; + int aw = cw - (horiz ? total_spacing : 0); + int ah = ch - (horiz ? 0 : total_spacing); + /* Length along the main axis, including spacing */ + int length = 0; + + /* Expanding widgets' information for extra space distribution */ + size_t n = w->child_count; + exp_t elements[n]; + + bool has_started = false; + for(size_t i = 0; i < w->child_count; i++) { + jwidget *child = w->children[i]; + + /* Maximum size to enforce: this is the acceptable size closest to our + allocatable size */ + int max_w = clamp(aw, child->min_w, child->max_w); + int max_h = clamp(ah, child->min_h, child->max_h); + + /* Start by setting every child to an acceptable size */ + child->w = clamp(child->w, child->min_w, max_w); + child->h = clamp(child->h, child->min_h, max_h); + + /* Initialize expanding widgets' information */ + elements[i].id = i; + elements[i].allocated = 0.0f; + elements[i].breaking_point = -1.0f; + + /* Determine natural length along the container, and stretch child + along the perpendicular direction if possible */ + + if(!child->visible) { + elements[i].stretch = 0; + elements[i].max = 0; + continue; + } + + if(has_started) length += l->spacing; + if(horiz) { + length += child->w; + if(child->stretch_y > 0) child->h = max_h; + + elements[i].stretch = child->stretch_x; + elements[i].max = max(max_w - child->w, 0); + + if(child->stretch_force && child->stretch_x > 0) + elements[i].max = max(aw - child->w, 0); + } + else { + length += child->h; + if(child->stretch_x > 0) child->w = max_w; + + elements[i].stretch = child->stretch_y; + elements[i].max = max(max_h - child->h, 0); + + if(child->stretch_force && child->stretch_y > 0) + elements[i].max = max(ah - child->h, 0); + } + + has_started = true; + } + + /* Distribute extra space along the line */ + int extra_space = (horiz ? cw : ch) - length; + allocate_space(elements, n, extra_space); + round_allocations(elements, n, extra_space); + + /* Update widgets for extra space */ + for(size_t i = 0; i < n; i++) { + exp_t *e = &elements[i]; + jwidget *child = w->children[e->id]; + if(!child->visible) continue; + + if(horiz) + child->w += e->allocated; + else + child->h += e->allocated; + } + + /* Position everyone */ + int position = 0; + + for(size_t i = 0; i < n; i++) { + jwidget *child = w->children[i]; + if(!child->visible) continue; + + if(horiz) { + child->x = position; + child->y = (ch - child->h) / 2; + position += child->w + l->spacing; + } + else { + child->x = (cw - child->w) / 2; + child->y = position; + position += child->h + l->spacing; + } + } +} diff --git a/src/jlayout_grid.c b/src/jlayout_grid.c new file mode 100644 index 0000000..a9d4198 --- /dev/null +++ b/src/jlayout_grid.c @@ -0,0 +1,17 @@ +#include +#include +#include +#include "jlayout_p.h" + +/* TODO: Functions for grid layouts */ + +void jlayout_grid_csize(void *w0) +{ + J_CAST(w) + w->w = 0; + w->h = 0; +} + +void jlayout_grid_apply(GUNUSED void *w0) +{ +} diff --git a/src/jlayout_p.h b/src/jlayout_p.h new file mode 100644 index 0000000..c914336 --- /dev/null +++ b/src/jlayout_p.h @@ -0,0 +1,31 @@ +//--- +// JustUI.jlayout (private): Internal layout functions +//--- + +#ifndef _J_JLAYOUT_P +#define _J_JLAYOUT_P + +/* jlayout_*_csize(): Natural size of content box for widgets with layouts + + This function finds the size of the children of (w) with jwidget_msize(), + and then deduces the natural content size of (w). The results are stored in + (w->w) and (w->h), as usual for the first phase of the layout process. + + This function is called only by jwidget_msize() during layout. */ +void jlayout_box_csize(void *w); +void jlayout_stack_csize(void *w); +void jlayout_grid_csize(void *w); + +/* jlayout_*_apply(): Layout widgets with layouts + + This function is essentially the second phase of the layout process for + widgets with layouts. Given that the margin-box size is set in (w->w) and + (w->h), the layout splits available content space between children and + positions them, before calling jwidget_layout() on the children. + + This function is called only by jwidget_layout() during layout. */ +void jlayout_box_apply(void *w); +void jlayout_stack_apply(void *w); +void jlayout_grid_apply(void *w); + +#endif /* _J_JLAYOUT_P */ diff --git a/src/jlayout_stack.c b/src/jlayout_stack.c new file mode 100644 index 0000000..6e1d893 --- /dev/null +++ b/src/jlayout_stack.c @@ -0,0 +1,66 @@ +#include +#include +#include +#include "jlayout_p.h" +#include "util.h" + +jlayout_stack *jlayout_get_stack(void *w0) +{ + J_CAST(w) + return (w->layout == J_LAYOUT_STACK) ? &w->layout_stack : NULL; +} + +jlayout_stack *jlayout_set_stack(void *w0) +{ + J_CAST(w) + w->layout = J_LAYOUT_STACK; + jlayout_stack *l = &w->layout_stack; + + l->active = -1; + return l; +} + +void jlayout_stack_csize(void *w0) +{ + J_CAST(w) + w->w = 0; + w->h = 0; + + for(int k = 0; k < w->child_count; k++) { + jwidget *child = w->children[k]; + if(!child->visible) continue; + jwidget_msize(child); + + w->w = max(w->w, child->w); + w->h = max(w->h, child->h); + } +} + +void jlayout_stack_apply(void *w0) +{ + J_CAST(w) + + int cw = jwidget_content_width(w); + int ch = jwidget_content_height(w); + + for(int k = 0; k < w->child_count; k++) { + jwidget *child = w->children[k]; + + /* Maximum size to enforce: this is the acceptable size closest to our + content size (that space we have to distribute) */ + int max_w = clamp(cw, child->min_w, child->max_w); + int max_h = clamp(ch, child->min_h, child->max_h); + + /* Set every child to an acceptable size */ + child->w = clamp(child->w, child->min_w, max_w); + child->h = clamp(child->h, child->min_h, max_h); + + /* Expand each child if any level of stretch is specified */ + if(child->stretch_x > 0) child->w = max_w; + if(child->stretch_y > 0) child->h = max_h; + + /* Center each child in the container */ + child->x = (cw - child->w) / 2; + child->y = (ch - child->h) / 2; + } +} diff --git a/src/jpainted.c b/src/jpainted.c new file mode 100644 index 0000000..7859e3a --- /dev/null +++ b/src/jpainted.c @@ -0,0 +1,55 @@ +#include +#include + +/* Type identified for jpainted */ +static int jpainted_type_id = -1; + +jpainted *jpainted_create(void *function, j_arg_t arg, int natural_w, + int natural_h, void *parent) +{ + if(jpainted_type_id < 0) return NULL; + + jpainted *p = malloc(sizeof *p); + jwidget_init(&p->widget, jpainted_type_id, parent); + + p->paint = function; + p->arg = arg; + p->natural_w = natural_w; + p->natural_h = natural_h; + + return p; +} + +//--- +// Polymorphic widget operations +//--- + +static void jpainted_poly_csize(void *p0) +{ + jpainted *p = p0; + p->widget.w = p->natural_w; + p->widget.h = p->natural_h; +} + +static void jpainted_poly_render(void *p0, int x, int y) +{ + jpainted *p = p0; + p->paint(x, y, p->arg); +} + +/* jpainted type definition */ +static jwidget_poly type_jpainted = { + .name = "jpainted", + .csize = jpainted_poly_csize, + .layout = NULL, + .render = jpainted_poly_render, + .event = NULL, + .destroy = NULL, +}; + +/* Type registration */ +__attribute__((constructor(1004))) +static void j_register_jpainted(void) +{ + jpainted_type_id = j_register_widget(&type_jpainted, "jwidget"); +} diff --git a/src/jscene.c b/src/jscene.c new file mode 100644 index 0000000..41e78ea --- /dev/null +++ b/src/jscene.c @@ -0,0 +1,203 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/* Type identifier for jscene */ +static int jscene_type_id = -1; +/* Events */ +uint16_t JSCENE_NONE; +uint16_t JSCENE_PAINT; +uint16_t JSCENE_KEY; + +/* Keyboard transformation for inputs in a jscene */ +static int jscene_repeater(int key, GUNUSED int duration, int count) +{ + if(key != KEY_LEFT && key != KEY_RIGHT && key != KEY_UP && key != KEY_DOWN) + return -1; + + return (count ? 40 : 400) * 1000; +} +static keydev_transform_t jscene_tr = { + .enabled = + KEYDEV_TR_DELAYED_SHIFT | + KEYDEV_TR_INSTANT_SHIFT | + KEYDEV_TR_DELAYED_ALPHA | + KEYDEV_TR_INSTANT_ALPHA | + KEYDEV_TR_REPEATS, + .repeater = jscene_repeater, +}; + +jscene *jscene_create(int x, int y, int w, int h, void *parent) +{ + if(jscene_type_id < 0) return NULL; + + jscene *s = malloc(sizeof *s); + if(!s) return NULL; + + jwidget_init(&s->widget, jscene_type_id, parent); + jwidget_set_fixed_size(s, w, h); + + s->x = x; + s->y = y; + + s->focus = NULL; + s->queue_first = 0; + s->queue_next = 0; + s->lost_events = 0; + + /* Prepare first layout/paint operation */ + s->widget.dirty = 1; + + return s; +} + +jscene *jscene_create_fullscreen(void *parent) +{ + return jscene_create(0, 0, DWIDTH, DHEIGHT, parent); +} + +void jscene_render(jscene *scene) +{ + jwidget_layout(scene); + jwidget_render(scene, scene->x, scene->y); +} + +//--- +// Event management +//--- + +jevent jscene_read_event(jscene *s) +{ + if(s->queue_first == s->queue_next) + return (jevent){ .source = NULL, .type = JSCENE_NONE }; + + jevent e = s->queue[s->queue_first]; + s->queue_first = (s->queue_first + 1) % JSCENE_QUEUE_SIZE; + return e; +} + +void jscene_queue_event(jscene *s, jevent e) +{ + /* Prevent filling and overflowing the queue */ + int next = (s->queue_next + 1) % JSCENE_QUEUE_SIZE; + if(next == s->queue_first) + { + s->lost_events++; + return; + } + + s->queue[s->queue_next] = e; + s->queue_next = next; +} + +//--- +// Keyboard focus and keyboard events +//--- + +void *jscene_focused_widget(jscene *s) +{ + return s->focus; +} + +void jscene_set_focused_widget(jscene *s, void *w0) +{ + J_CAST(w) + + /* Check that (s) is an ancestor of (w) */ + if(w) for(jwidget *anc = w; anc != (jwidget *)s; anc = anc->parent) { + if(anc == NULL) return; + } + + /* Focus out old focused widget */ + if(s->focus) jwidget_event(s->focus,(jevent){ .type = JWIDGET_FOCUS_OUT }); + + s->focus = w; + + /* Focus in newly-selected widget */ + if(w) jwidget_event(w, (jevent){ .type = JWIDGET_FOCUS_IN }); +} + +/* jscene_process_event(): Send an event to the focused widget */ +bool jscene_process_key_event(jscene *scene, key_event_t event) +{ + jwidget *candidate = scene->focus; + jevent e = { .type = JWIDGET_KEY, .key = event }; + + while(candidate) { + if(jwidget_event(candidate, e)) return true; + candidate = candidate->parent; + } + + return false; +} + +/* jscene_run(): Run a scene's main loop */ +jevent jscene_run(jscene *s) +{ + keydev_t *d = keydev_std(); + keydev_transform_t tr0 = keydev_transform(d); + keydev_set_transform(d, jscene_tr); + + jevent e; + while(1) { + /* Create repaint events (also handle relayout if needed) */ + if(jwidget_layout_dirty(s) || jwidget_needs_update(s)) { + jscene_queue_event(s, (jevent){ .type = JSCENE_PAINT }); + } + + /* Queued GUI events */ + e = jscene_read_event(s); + if(e.type != JSCENE_NONE) break; + + /* Queued keyboard events */ + key_event_t k = keydev_read(d); + + if(k.type == KEYEV_DOWN && k.key == KEY_MENU && !k.shift && !k.alpha) { + gint_osmenu(); + continue; + } + #ifdef FX9860G + if(k.type == KEYEV_DOWN && k.key == KEY_OPTN && k.shift && !k.alpha) { + t6k11_backlight(-1); + continue; + } + #endif + if(k.type != KEYEV_NONE && !jscene_process_key_event(s, k)) { + e.type = JSCENE_KEY; + e.key = k; + break; + } + + sleep(); + } + + keydev_set_transform(d, tr0); + return e; +} + +/* jscene type definition */ +static jwidget_poly type_jscene = { + .name = "jscene", + .csize = NULL, + .layout = NULL, + .render = NULL, + .event = NULL, + .destroy = NULL, +}; + +__attribute__((constructor(1001))) +static void j_register_jscene(void) +{ + jscene_type_id = j_register_widget(&type_jscene, "jwidget"); + JSCENE_NONE = j_register_event(); + JSCENE_PAINT = j_register_event(); + JSCENE_KEY = j_register_event(); +} diff --git a/src/jwidget.c b/src/jwidget.c new file mode 100644 index 0000000..645ebd7 --- /dev/null +++ b/src/jwidget.c @@ -0,0 +1,677 @@ +#include +#include +#include +#include +#include "jlayout_p.h" +#include "util.h" + +#include +#include +#include + +#define WIDGET_TYPES_MAX 32 + +/* Polymorphic functions for jwidget */ +static jwidget_poly_render_t jwidget_poly_render; +static jwidget_poly_csize_t jwidget_poly_csize; + +/* jwidget type definition */ +static jwidget_poly type_jwidget = { + .name = "jwidget", + .csize = jwidget_poly_csize, + .layout = NULL, + .render = jwidget_poly_render, + .event = NULL, + .destroy = NULL, +}; + +/* List of registered widget types */ +static jwidget_poly *widget_types[WIDGET_TYPES_MAX] = { + &type_jwidget, + NULL, +}; + +/* Events */ +uint16_t JWIDGET_KEY; +uint16_t JWIDGET_FOCUS_IN; +uint16_t JWIDGET_FOCUS_OUT; + +//--- +// Polymorphic functions for widgets +//--- + +static void jwidget_poly_render(void *w0, int x, int y) +{ + J_CAST(w) + jlayout_stack *l; + + /* If there is a stack layout, render active child */ + if((l = jlayout_get_stack(w)) && l->active >= 0) { + jwidget *child = w->children[l->active]; + if(child->visible) jwidget_render(child, x+child->x, y+child->y); + } + /* Otherwise, simply render all children */ + else { + for(int k = 0; k < w->child_count; k++) { + jwidget *child = w->children[k]; + if(child->visible) jwidget_render(child, x+child->x, y+child->y); + } + } +} + +static void jwidget_poly_csize(void *w0) +{ + J_CAST(w) + + /* The content box of this children is the union of the border boxes of the + children. This function is called only for widgets without a layout, in + which case the children must have their positions set. */ + w->w = 0; + w->h = 0; + + for(int k = 0; k < w->child_count; k++) { + jwidget *child = w->children[k]; + if(!child->visible) continue; + jwidget_msize(child); + + w->w = max(w->w, child->x + child->w); + w->h = max(w->h, child->y + child->h); + } +} + +//--- +// Initialization +//--- + +jwidget *jwidget_create(void *parent) +{ + jwidget *w = malloc(sizeof *w); + if(!w) return NULL; + + /* Type ID 0 is for jwidget */ + jwidget_init(w, 0, parent); + return w; +} + +void jwidget_init(jwidget *w, int type, void *parent) +{ + w->parent = NULL; + w->children = NULL; + w->child_count = 0; + w->child_alloc = 0; + w->dirty = 1; + w->visible = 1; + + w->type = type; + w->geometry = NULL; + + w->x = 0; + w->y = 0; + w->w = 0; + w->h = 0; + w->min_w = 0; + w->min_h = 0; + w->max_w = 0x7fff; + w->max_h = 0x7fff; + + w->layout = J_LAYOUT_NONE; + w->stretch_x = 0; + w->stretch_y = 0; + w->stretch_force = 0; + + jwidget_set_parent(w, parent); +} + +void jwidget_destroy(void *w0) +{ + J_CAST(w) + jwidget_set_parent(w, NULL); + + /* Run the custom destructor */ + jwidget_poly const *poly = widget_types[w->type]; + if(poly->destroy) poly->destroy(w); + + for(int i = 0; i < w->child_count; i++) { + /* This will prevent us from unregistering the child from w in the + recursive call, which saves some time in pointer manipulation */ + w->children[i]->parent = NULL; + jwidget_destroy(w->children[i]); + } + + if(w->children) free(w->children); + if(w->geometry) free(w->geometry); + free(w); +} + +//--- +// Ownership +//--- + +void jwidget_set_parent(void *w0, void *parent0) +{ + J_CAST(w, parent) + if(w->parent == parent) return; + + if(w->parent != NULL) + jwidget_remove_child(w->parent, w); + if(parent != NULL) + jwidget_add_child(parent, w); +} + +void jwidget_add_child(void *w0, void *child0) +{ + J_CAST(w, child) + jwidget_insert_child(w, child, w->child_count); +} + +void jwidget_insert_child(void *w0, void *child0, int position) +{ + J_CAST(w, child) + if(child->parent == w) return; + if(position < 0 || position > w->child_count) return; + + /* Don't overflow the child_count and child_alloc fields! */ + if(w->child_count >= 256 - 4) return; + + /* Make room for a new child if needed */ + if(w->child_count + 1 > w->child_alloc) { + size_t new_size = (w->child_alloc + 4) * sizeof(jwidget *); + jwidget **new_children = realloc(w->children, new_size); + if(!new_children) return; + + w->children = new_children; + w->child_alloc += 4; + } + + /* Insert new child */ + for(int k = w->child_count; k > position; k--) + w->children[k] = w->children[k - 1]; + + w->children[position] = child; + w->child_count++; + + /* Remove the existing parent at the last moment, in order to keep the tree + in a safe state if allocations fail or parameters are invalid */ + if(child->parent != NULL) + jwidget_remove_child(child->parent, child); + + child->parent = w; + + /* Update stack layouts to keep showing the same child */ + if(w->layout == J_LAYOUT_STACK && w->layout_stack.active >= position) { + w->layout_stack.active++; + } + /* Update stack layout to show the first child if none was visible yet */ + if(w->layout == J_LAYOUT_STACK && w->layout_stack.active == -1) { + w->layout_stack.active = 0; + } + + /* Force a later recomputation of the layout */ + w->dirty = 1; +} + +void jwidget_remove_child(void *w0, void *child0) +{ + J_CAST(w, child) + if(child->parent != w) return; + + int write = 0; + int index = -1; + + /* Remove all occurrences of (child) from (w->children) */ + for(int read = 0; read < w->child_count; read++) { + if(w->children[read] != child) { + w->children[write++] = w->children[read]; + } + else if(index < 0) { + index = read; + } + } + + /* Remove the parent from the child */ + child->parent = NULL; + + /* Update stack layouts to not show the removed child */ + if(w->layout == J_LAYOUT_STACK && w->layout_stack.active == index) { + w->layout_stack.active = -1; + } + + /* Force a later recomputation of the layout */ + w->dirty = 1; +} + +//--- +// Sizing and stretching +//--- + +void jwidget_set_minimum_width(void *w0, int min_width) +{ + J_CAST(w) + w->min_w = clamp(min_width, 0, 0x7fff); + w->dirty = 1; +} + +void jwidget_set_minimum_height(void *w0, int min_height) +{ + J_CAST(w) + w->min_h = clamp(min_height, 0, 0x7fff); + w->dirty = 1; +} + +void jwidget_set_minimum_size(void *w, int min_width, int min_height) +{ + jwidget_set_minimum_width(w, min_width); + jwidget_set_minimum_height(w, min_height); +} + +void jwidget_set_maximum_width(void *w0, int max_width) +{ + J_CAST(w) + w->max_w = clamp(max_width, 0, 0x7fff); + w->dirty = 1; +} + +void jwidget_set_maximum_height(void *w0, int max_height) +{ + J_CAST(w) + w->max_h = clamp(max_height, 0, 0x7fff); + w->dirty = 1; +} + +void jwidget_set_maximum_size(void *w, int max_width, int max_height) +{ + jwidget_set_maximum_width(w, max_width); + jwidget_set_maximum_height(w, max_height); +} + +void jwidget_set_fixed_width(void *w, int width) +{ + jwidget_set_minimum_width(w, width); + jwidget_set_maximum_width(w, width); +} + +void jwidget_set_fixed_height(void *w, int height) +{ + jwidget_set_minimum_height(w, height); + jwidget_set_maximum_height(w, height); +} + +void jwidget_set_fixed_size(void *w, int width, int height) +{ + jwidget_set_minimum_size(w, width, height); + jwidget_set_maximum_size(w, width, height); +} + +void jwidget_set_stretch(void *w0, int stretch_x, int stretch_y, bool force) +{ + J_CAST(w) + w->stretch_x = clamp(stretch_x, 0, 15); + w->stretch_y = clamp(stretch_y, 0, 15); + w->stretch_force = force; + w->dirty = 1; +} + +//--- +// Geometry +//--- + +static jwidget_geometry default_geometry = { + .margins = { 0, 0, 0, 0 }, + .borders = { 0, 0, 0, 0 }, + .paddings = { 0, 0, 0, 0 }, + .border_color = C_NONE, + .border_style = J_BORDER_NONE, + .background_color = C_NONE, +}; + +jwidget_geometry const *jwidget_geometry_r(void *w0) +{ + J_CAST(w) + return (w->geometry == NULL) ? &default_geometry : w->geometry; +} + +jwidget_geometry *jwidget_geometry_rw(void *w0) +{ + J_CAST(w) + + /* Duplicate default geometry as a copy-on-write tactic to save memory */ + if(w->geometry == NULL) { + w->geometry = malloc(sizeof *w->geometry); + if(!w->geometry) return NULL; + *w->geometry = default_geometry; + } + + /* Assume layout will need to be recomputed */ + w->dirty = 1; + + return w->geometry; +} + +void jwidget_set_border(void *w, jwidget_border_style s, int width, int color) +{ + jwidget_geometry *g = jwidget_geometry_rw(w); + + g->border_style = s; + g->border_color = color; + for(int i = 0; i < 4; i++) g->borders[i] = width; +} + +void jwidget_set_padding(void *w, int top, int right, int bottom, int left) +{ + jwidget_geometry *g = jwidget_geometry_rw(w); + g->padding.top = top; + g->padding.right = right; + g->padding.bottom = bottom; + g->padding.left = left; +} + +void jwidget_set_margin(void *w, int top, int right, int bottom, int left) +{ + jwidget_geometry *g = jwidget_geometry_rw(w); + g->margin.top = top; + g->margin.right = right; + g->margin.bottom = bottom; + g->margin.left = left; +} + +void jwidget_set_background(void *w, int color) +{ + jwidget_geometry *g = jwidget_geometry_rw(w); + g->background_color = color; +} + +//--- +// Layout +//--- + +void jlayout_set_manual(void *w0) +{ + J_CAST(w) + w->layout = J_LAYOUT_NONE; + w->dirty = 1; +} + +void jwidget_msize(void *w0) +{ + J_CAST(w) + int t = w->layout; + + /* Size of contents */ + if(t == J_LAYOUT_NONE) { + jwidget_poly const *poly = widget_types[w->type]; + if(poly->csize) poly->csize(w); + } + + else if(t == J_LAYOUT_HBOX || t == J_LAYOUT_VBOX) + jlayout_box_csize(w); + + else if(t == J_LAYOUT_STACK) + jlayout_stack_csize(w); + + else if(t == J_LAYOUT_GRID) + jlayout_grid_csize(w); + + /* Add the size of the geometry */ + jwidget_geometry const *g = jwidget_geometry_r(w); + + w->w += g->margin.left + g->border.left + g->padding.left; + w->w += g->margin.right + g->border.right + g->padding.right; + w->h += g->margin.top + g->border.top + g->padding.top; + w->h += g->margin.bottom + g->border.bottom + g->padding.bottom; + + w->dirty = 1; +} + +bool jwidget_layout_dirty(void *w0) +{ + J_CAST(w) + if(w->dirty) return true; + + for(int k = 0; k < w->child_count; k++) { + if(!w->children[k]->visible) continue; + if(jwidget_layout_dirty(w->children[k])) return true; + } + + return false; +} + +bool jwidget_visible(void *w0) +{ + J_CAST(w) + return w->visible; +} + +void jwidget_set_visible(void *w0, bool visible) +{ + J_CAST(w) + if(w->visible == visible) return; + + w->visible = visible; + if(w->parent) w->parent->dirty = 1; +} + +bool jwidget_needs_update(void *w0) +{ + J_CAST(w) + if(w->update) return true; + + jlayout_stack *l = jlayout_get_stack(w); + + /* Ignore invisible children, because their update bits are not reset after + a repaint, resulting in infinite REPAINT events */ + for(int k = 0; k < w->child_count; k++) { + if(l && l->active != k) continue; + if(!w->children[k]->visible) continue; + if(jwidget_needs_update(w->children[k])) return true; + } + + return false; +} + +/* Apply layout recursively on this widget and its children */ +static void jwidget_layout_apply(void *w0) +{ + J_CAST(w) + if(!w->visible) return; + int t = w->layout; + + if(t == J_LAYOUT_NONE) { + jwidget_poly const *poly = widget_types[w->type]; + if(poly->layout) poly->layout(w); + } + else if(t == J_LAYOUT_HBOX || t == J_LAYOUT_VBOX) { + jlayout_box_apply(w); + } + else if(t == J_LAYOUT_STACK) { + jlayout_stack_apply(w); + } + else if(t == J_LAYOUT_GRID) { + jlayout_grid_apply(w); + } + + /* The layout is now up-to-date and will not be recomputed until a widget + in the hierarchy requires it */ + w->dirty = 0; + + for(int k = 0; k < w->child_count; k++) + jwidget_layout_apply(w->children[k]); +} + +void jwidget_layout(void *root0) +{ + J_CAST(root) + if(!jwidget_layout_dirty(root)) return; + + /* Phase 1: Compute the natural margin size of every widget (bottom-up) */ + jwidget_msize(root); + + /* Decide on the size of the root; first make sure it's acceptable */ + root->w = clamp(root->w, root->min_w, root->max_w); + root->h = clamp(root->h, root->min_h, root->max_h); + /* Now if there is stretch and a maximum size, stretch */ + if(root->stretch_x && root->max_w != 0x7fff) + root->w = root->max_w; + if(root->stretch_y && root->max_h != 0x7fff) + root->h = root->max_h; + + /* Phase 2: Distribute space recursively (top-down) */ + jwidget_layout_apply(root); +} + +int jwidget_content_width(void *w0) +{ + J_CAST(w) + jwidget_geometry const *g = jwidget_geometry_r(w); + + return w->w - g->margin.left - g->border.left - g->padding.left + - g->margin.right - g->border.right - g->padding.right; +} + +int jwidget_content_height(void *w0) +{ + J_CAST(w) + jwidget_geometry const *g = jwidget_geometry_r(w); + + return w->h - g->margin.top - g->border.top - g->padding.top + - g->margin.bottom - g->border.bottom - g->padding.bottom; +} + +int jwidget_full_width(void *w0) +{ + J_CAST(w) + return w->w; +} + +int jwidget_full_height(void *w0) +{ + J_CAST(w) + return w->h; +} + +//--- +// Rendering +//--- + +void jwidget_render(void *w0, int x, int y) +{ + J_CAST(w) + if(!w->visible) return; + + /* Render widget border */ + jwidget_geometry const *g = jwidget_geometry_r(w); + jdirs b = g->border; + int color = g->border_color; + + int cw = jwidget_content_width(w); + int ch = jwidget_content_height(w); + + int x1 = x + g->margin.left; + int y1 = y + g->margin.top; + int x2 = x1 + b.left + g->padding.left + cw + g->padding.right; + int y2 = y1 + b.top + g->padding.top + ch + g->padding.bottom; + + if(g->border_style == J_BORDER_NONE || color == C_NONE) { + } + else if(g->border_style == J_BORDER_SOLID) { + drect(x1, y1, x2 + b.right - 1, y1 + b.top - 1, color); + drect(x1, y2, x2 + b.right - 1, y2 + b.bottom - 1, color); + + drect(x1, y1 + b.top, x1 + b.left - 1, y2 - 1, color); + drect(x2, y1 + b.top, x2 + b.right - 1, y2 - 1, color); + } + /* TODO: jwidget_render(): More border types */ + + if(g->background_color != C_NONE) { + drect(x1 + b.left, y1 + b.top, x2, y2, g->background_color); + } + + /* Call the polymorphic render function at the top-left content point */ + x += g->margin.left + b.left + g->padding.left; + y += g->margin.top + b.top + g->padding.top; + + jwidget_poly const *poly = widget_types[w->type]; + if(poly->render) poly->render(w, x, y); + + w->update = 0; +} + +//--- +// Event management +//--- + +bool jwidget_event(void *w0, jevent e) +{ + J_CAST(w) + jwidget_poly const *poly = widget_types[w->type]; + if(poly->event) return poly->event(w, e); + return false; +} + +void jwidget_emit(void *w0, jevent e) +{ + J_CAST(w) + if(!w) return; + + if(e.source == NULL) e.source = w; + + if(!strcmp(jwidget_type(w), "jscene")) { + jscene_queue_event((jscene *)w, e); + } + else { + jwidget_emit(w->parent, e); + } +} + +//--- +// Extension API +//--- + +char const *jwidget_type(void *w0) +{ + J_CAST(w) + return widget_types[w->type]->name; +} + +int j_register_widget(jwidget_poly *poly, char const *inherits) +{ + /* Resolve inheritance */ + if(inherits) { + jwidget_poly const *base = NULL; + + for(int i = 0; i < WIDGET_TYPES_MAX && !base; i++) { + if(widget_types[i] && !strcmp(widget_types[i]->name, inherits)) + base = widget_types[i]; + } + + if(!base) return -1; + + if(!poly->csize) poly->csize = base->csize; + if(!poly->layout) poly->layout = base->layout; + if(!poly->render) poly->render = base->render; + if(!poly->event) poly->event = base->event; + if(!poly->destroy) poly->destroy = base->destroy; + } + + for(int i = 0; i < WIDGET_TYPES_MAX; i++) { + if(widget_types[i] == NULL) { + widget_types[i] = poly; + return i; + } + } + return -1; +} + +int j_register_event(void) +{ + static int event_id = 0; + event_id++; + return event_id; +} + +__attribute__((constructor(1000))) +static void j_register_jwidget(void) +{ + JWIDGET_KEY = j_register_event(); + JWIDGET_FOCUS_IN = j_register_event(); + JWIDGET_FOCUS_OUT = j_register_event(); +} diff --git a/src/keymap.c b/src/keymap.c new file mode 100644 index 0000000..4296533 --- /dev/null +++ b/src/keymap.c @@ -0,0 +1,56 @@ +#include "util.h" +#include + +static int key_id(int keycode) +{ + uint col = (keycode & 0x0f) - 1; + uint row = 9 - ((keycode & 0xf0) >> 4); + + if(col > 5 || row > 8) return -1; + return 6 * row + col; +} + +static uint8_t map_flat[30] = { + 0, 0, '(', ')', ',', '=', + '7', '8', '9', 0, 0, 0, + '4', '5', '6', '*', '/', 0, + '1', '2', '3', '+', '-', 0, + '0', '.', 'e', '-', 0, 0, +}; +static uint8_t map_alpha[36] = { + 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 0, 0, 0, + 'p', 'q', 'r', 's', 't', 0, + 'u', 'v', 'w', 'x', 'y', 0, + 'z', ' ', '"', 0, 0, 0, +}; + +uint32_t keymap_translate(int key, bool shift, bool alpha) +{ + int id = key_id(key); + if(id < 0) return 0; + + if(!shift && !alpha) { + /* The first 4 rows have no useful characters */ + return (id < 24) ? 0 : map_flat[id - 24]; + } + if(shift && !alpha) { + if(key == KEY_MUL) return '{'; + if(key == KEY_DIV) return '}'; + if(key == KEY_ADD) return '['; + if(key == KEY_SUB) return ']'; + if(key == KEY_DOT) return '='; + if(key == KEY_EXP) return 0x3c0; // 'π' + } + if(!shift && alpha) { + /* The first 3 rows have no useful characters */ + return (id < 18) ? 0 : map_alpha[id - 18]; + } + if(shift && alpha) { + int c = keymap_translate(key, false, true); + return (c >= 'a' && c <= 'z') ? (c & ~0x20) : c; + } + + return 0; +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..00d9888 --- /dev/null +++ b/src/util.h @@ -0,0 +1,23 @@ +//--- +// JustUI.util: Header-level utilities that cannot be exposed to users +//--- + +#ifndef _J_UTIL +#define _J_UTIL + +#include + +/* Clamp a value between two ends. */ +__attribute__((always_inline)) +static inline int clamp(int value, int min, int max) +{ + /* Mark the branches as unlikely, that might help */ + if(__builtin_expect(value < min, 0)) return min; + if(__builtin_expect(value > max, 0)) return max; + return value; +} + +/* Code point for a character input */ +uint32_t keymap_translate(int key, bool shift, bool alpha); + +#endif /* _J_UTIL */ diff --git a/src/vec.c b/src/vec.c new file mode 100644 index 0000000..4b99b25 --- /dev/null +++ b/src/vec.c @@ -0,0 +1,65 @@ +#include +#include + +void vec_init(vec_t *v, size_t elsize) +{ + v->data = NULL; + v->size = 0; + v->free = 0; + v->elsize = elsize; +} + +void vec_clear(vec_t *v) +{ + free(v->data); + v->data = NULL; + v->size = 0; + v->free = 0; + /* Leave v->elsize for potential future reuse */ +} + +bool vec_add(vec_t *v, size_t n) +{ + if(n > v->free) { + /* Allocate either size/2 new elements or 4 elements */ + size_t ext = max(v->size / 2, 4); + /* Make sure that no more than 255 free elements will remain */ + if(v->size + v->free + ext > n + 255) + ext = (n + 255) - (v->size + v->free); + + size_t newsize = v->size + v->free + ext; + void *newdata = realloc(v->data, newsize * v->elsize); + if(!newdata) return false; + + v->data = newdata; + v->free = newsize - n; + } + else { + v->free -= n; + } + + v->size += n; + return true; +} + +bool vec_remove(vec_t *v, size_t n) +{ + n = min(n, v->size); + + /* Make sure that the vector is at least half-full and that no more than + 255 free elements remain */ + if(v->size - n <= (v->size + v->free) / 2 || v->free + n > 255) { + size_t newsize = v->size - n; + void *newdata = realloc(v->data, newsize * v->elsize); + if(!newdata) return false; + + v->data = newdata; + v->free = 0; + } + else { + v->free += n; + } + + v->size -= n; + return true; +}