version 1.0.0, I guess

This commit is contained in:
Lephenixnoir 2021-03-12 16:19:43 +01:00
commit 120b33c9f3
No known key found for this signature in database
GPG key ID: 1BBA026E13FC0495
38 changed files with 4181 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# Build files
build-fx
build-cg
# GiteaPC support
giteapc-config.make
giteapc-config-*.make
# Developer's files
*.sublime-*

51
CMakeLists.txt Normal file
View file

@ -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}")

24
README.md Normal file
View file

@ -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.

10
TODO Normal file
View file

@ -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)

View file

@ -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

BIN
assets/input-modes-cg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

BIN
assets/input-modes-fx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

19
cmake/FindJustUI.cmake Normal file
View file

@ -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()

33
doc/hierarchy.md Normal file
View file

@ -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.

87
doc/layout.md Normal file
View file

@ -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.

3
doc/scene.md Normal file
View file

@ -0,0 +1,3 @@
# JustUI: Scenes and events
TODO.

163
doc/widgets.md Normal file
View file

@ -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 `<justui/widget-api.h>`, 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
`<justui/jevent.h>`.
```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.

25
giteapc.make Normal file
View file

@ -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

View file

@ -0,0 +1,10 @@
//---
// Just UI: Compile-time configuration
//---
#ifndef _JUSTUI_CONFIG
#define _JUSTUI_CONFIG
#define J_VERSION "@JustUI_VERSION@"
#endif /* _JUSTUI_CONFIG */

62
include/justui/defs.h Normal file
View file

@ -0,0 +1,62 @@
//---
// JustUI.defs: Type and utility definitions
//---
#ifndef _J_DEFS
#define _J_DEFS
#include <gint/defs/types.h>
#include <gint/defs/util.h>
#include <gint/defs/attributes.h>
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
/* 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 */

50
include/justui/jevent.h Normal file
View file

@ -0,0 +1,50 @@
//---
// JustUI.jevent: GUI union event
//---
#ifndef _J_EVENT
#define _J_EVENT
#include <justui/defs.h>
#include <justui/jwidget.h>
#include <gint/keyboard.h>
/* 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 */

104
include/justui/jfkeys.h Normal file
View file

@ -0,0 +1,104 @@
//---
// JustUI.jfkeys: Row of function keys
//---
#ifndef _J_JFKEYS
#define _J_JFKEYS
#include <justui/defs.h>
#include <justui/jwidget.h>
#include <gint/display.h>
/* 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 */

91
include/justui/jinput.h Normal file
View file

@ -0,0 +1,91 @@
//---
// JustUI.jinput: One-line input field
//---
#ifndef _J_JINPUT
#define _J_JINPUT
#include <justui/defs.h>
#include <justui/jwidget.h>
#include <justui/p/vec.h>
#include <gint/display.h>
/* 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 */

134
include/justui/jlabel.h Normal file
View file

@ -0,0 +1,134 @@
//---
// JustUI.jlabel: Simple one-line or multi-line text label without formatting
//---
#ifndef _J_JLABEL
#define _J_JLABEL
#include <justui/defs.h>
#include <justui/jwidget.h>
#include <justui/p/vec.h>
#include <gint/display.h>
/* 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 <gint/display.h>. 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 */

118
include/justui/jlayout.h Normal file
View file

@ -0,0 +1,118 @@
//---
// JustUI.jlayout: Widget positioning mechanisms
//---
#ifndef _J_JLAYOUT
#define _J_JLAYOUT
#include <justui/defs.h>
/* 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 */

65
include/justui/jpainted.h Normal file
View file

@ -0,0 +1,65 @@
//---
// JustUI.jpainted: Widget defined by just a size and painting function
//---
#ifndef _J_JPAINTED
#define _J_JPAINTED
#include <justui/defs.h>
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
/* 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 */

105
include/justui/jscene.h Normal file
View file

@ -0,0 +1,105 @@
//---
// JustUI.jscene: Root object that provides keyboard focus and event handling
//---
#ifndef _J_JSCENE
#define _J_JSCENE
#include <justui/defs.h>
#include <justui/jwidget.h>
#include <justui/jevent.h>
#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 */

View file

@ -0,0 +1,158 @@
//---
// JustUI.jwidget-API: API for subclassed widget types
//---
#ifndef _J_JWIDGET_API
#define _J_JWIDGET_API
#include <justui/defs.h>
#include <justui/jevent.h>
#include <gint/keyboard.h>
//---
// 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 */

370
include/justui/jwidget.h Normal file
View file

@ -0,0 +1,370 @@
//---
// JustUI.jwidget: Base object for all widgets
//---
#ifndef _J_JWIDGET
#define _J_JWIDGET
#include <justui/defs.h>
#include <justui/jlayout.h>
/* 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 <gint/display.h>) */
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 */

49
include/justui/p/vec.h Normal file
View file

@ -0,0 +1,49 @@
//---
// JustUI.util.vector: Dynamic arrays
//---
#ifndef _J_UTIL_VECTOR
#define _J_UTIL_VECTOR
#include <justui/defs.h>
/* 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 */

179
src/jfkeys.c Normal file
View file

@ -0,0 +1,179 @@
#include <justui/jfkeys.h>
#include <justui/jwidget-api.h>
#include <gint/std/stdlib.h>
#include <gint/std/string.h>
/* 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");
}

351
src/jinput.c Normal file
View file

@ -0,0 +1,351 @@
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#include <justui/jinput.h>
#include "util.h"
#include <gint/display.h>
#include <gint/keyboard.h>
#include <gint/std/stdlib.h>
/* 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();
}

360
src/jlabel.c Normal file
View file

@ -0,0 +1,360 @@
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#include <justui/jlabel.h>
#include <gint/display.h>
#include <gint/std/stdio.h>
#include <gint/std/stdlib.h>
#include <gint/std/string.h>
#include <stdarg.h>
/* 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");
}

349
src/jlayout_box.c Normal file
View file

@ -0,0 +1,349 @@
#include <justui/jlayout.h>
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#include "jlayout_p.h"
#include "util.h"
#include <openlibm.h>
//---
// 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;
}
}
}

17
src/jlayout_grid.c Normal file
View file

@ -0,0 +1,17 @@
#include <justui/jlayout.h>
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#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)
{
}

31
src/jlayout_p.h Normal file
View file

@ -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 */

66
src/jlayout_stack.c Normal file
View file

@ -0,0 +1,66 @@
#include <justui/jlayout.h>
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#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;
}
}

55
src/jpainted.c Normal file
View file

@ -0,0 +1,55 @@
#include <justui/jpainted.h>
#include <gint/std/stdlib.h>
/* 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");
}

203
src/jscene.c Normal file
View file

@ -0,0 +1,203 @@
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#include <justui/jscene.h>
#include <gint/display.h>
#include <gint/keyboard.h>
#include <gint/drivers/keydev.h>
#include <gint/clock.h>
#include <gint/std/stdlib.h>
#include <gint/gint.h>
#include <gint/drivers/t6k11.h>
/* 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();
}

677
src/jwidget.c Normal file
View file

@ -0,0 +1,677 @@
#include <justui/jwidget.h>
#include <justui/jwidget-api.h>
#include <justui/jscene.h>
#include <justui/jevent.h>
#include "jlayout_p.h"
#include "util.h"
#include <gint/display.h>
#include <gint/std/stdlib.h>
#include <gint/std/string.h>
#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();
}

56
src/keymap.c Normal file
View file

@ -0,0 +1,56 @@
#include "util.h"
#include <gint/keyboard.h>
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;
}

23
src/util.h Normal file
View file

@ -0,0 +1,23 @@
//---
// JustUI.util: Header-level utilities that cannot be exposed to users
//---
#ifndef _J_UTIL
#define _J_UTIL
#include <justui/defs.h>
/* 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 */

65
src/vec.c Normal file
View file

@ -0,0 +1,65 @@
#include <justui/p/vec.h>
#include <gint/std/stdlib.h>
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;
}