Compare commits

..

No commits in common. "c3" and "rewrite2" have entirely different histories.
c3 ... rewrite2

46 changed files with 3095 additions and 5209 deletions

41
.clang-format Normal file
View File

@ -0,0 +1,41 @@
# linux kernel style formatting
BasedOnStyle: LLVM
IndentWidth: 8
UseTab: AlignWithSpaces
BreakBeforeBraces: Linux
AllowShortIfStatementsOnASingleLine: false
IndentCaseLabels: false
ColumnLimit: 85
InsertBraces: true
SortIncludes: Never
BinPackParameters: false
BinPackArguments: false
Cpp11BracedListStyle: true
SpaceBeforeCpp11BracedList: true
SeparateDefinitionBlocks: Always
AlignAfterOpenBracket: BlockIndent
InsertNewlineAtEOF: true
AlignConsecutiveDeclarations:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: true
PadOperators: false
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true
AlignConsecutiveBitFields:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true
AlignConsecutiveAssignments:
Enabled: true
AcrossEmptyLines: false
AcrossComments: true

14
.gitignore vendored
View File

@ -1,8 +1,8 @@
microgui
ugui
*.o
*.a
build/*
**/.ccls-cache
perf.data*
*.rdc
test_renderer
resources/shaders/compiled/**
test/test
**/compile_commands.json
**/.cache
test
raylib/*

0
.gitmodules vendored
View File

184
ARCHITECTURE.md Normal file
View File

@ -0,0 +1,184 @@
## High level overview
Under the hood every element has an id, this id allows the library to store state
between frames.
Elements are also cached such that when the ui tree is rebuilt at the beginning of
every frame the element data structure doesn't have to be rebuilt.
Elements are arranged in a tree, nodes are container elements that can contain other
elements, leafs are elements that cannot contain other elements.
Every element has a size and a position, containers also have to keep track of their
layout information and some other state.
Elements can push commands into the draw stack, which is a structure that contains
all the draw commands that the user application has to perform do display the ui
correctly, such commands include drawing lines, rectangles, sprites, text, etc.
```text
+-----------+
| ug_init() |
+-----+-----+
|
|
|
+---------v----------+
|ug_input_keyboard() |
|ug_input_mouse() <----+
|ug_input_clipboard()| |
| ... | |
+---------+----------+ |
| |
| |
+-------v--------+ |
|ug_frame_begin()| |
+-------+--------+ |
| |
| |
+---------v----------+ |
|ug_window_start() | |
+---->ug_container_start()| |
| |ug_div_start() | |
| | ... | |
| +---------+----------+ |
| | |
| | |
multiple +--------v---------+ |
times |ug_layout_row() | |
| |ug_layout_column()| |
| |ug_layout_float() | |
| | ... | |
| +--------+---------+ |
| | |
| | |
| +------v------+ |
| |ug_button() | |
| |ug_text_box()| |
| |ug_slider() | |
| | ... | |
| +------+------+ |
| | |
+--------------+ |
| |
+--------v---------+ |
|ug_window_end() | |
|ug_container_end()| |
|ug_div_end() | |
| ... | |
+--------+---------+ |
| |
| |
| |
+------v-------+ |
|ug_frame_end()| |
+------+-------+ |
| |
| |
| |
+------v-------+ |
|user draws the| |
| ui +-------+
+------+-------+
|
|quit
|
+------v-------+
| ug_destroy() |
+--------------+
```
### Layouting
Layouting happens in a dynamic grid, when a new element is inserted in a non-floating
manner it reserves a space in the grid, new elements are placed following this grid.
Every div has two points of origin, one for the row layout and one for the column
layout, named origin_r and origin_c respectively
origin_r is used when the row layout is used and it is used to position the child
elements one next to the other, as such it always points to the top-right edge
of the last row element
```text
Layout: row
#: lost space
Parent div
x---------------------------------+
|[origin_c] |
|[origin_r] |
| |
| |
| |
| |
| |
| |
| |
+---------------------------------+
Parent div
+-----x---------------------------+
| |[origin_r] |
| E1 | |
| | |
x-----+---------------------------+
|[origin_c] |
| | |
| | |
| | |
| | |
| | |
+-----+---------------------------+
Parent div
+-----+----------+-----x----------+
| | E2 | |[origin_r]|
| E1 +----------+ | |
| |##########| E3 | |
+-----+##########| | |
|################| | |
+----------------x-----+----------+
| [origin_c] |
| | |
| | |
| | |
+----------------+----------------+
```
TODO: handle when the content overflows the div
- Use a different concept, like a view or relative space, for example the child
div could have position `[0,0]` but in reality it is relative to the origin of the
parent div
- each div could have a view and a total area of the content, when drawing everything
is clipped to the view and scrollbars are shown
- individual elements accept dimensions and the x/y coordinates could be interpreted
as offset if the layout is row/column or absolute coordinates if the leayout is floating
A div can be marked resizeable or fixed, and static or dynamic. The difference being
that resizeable adds a resize handle to the div and dynamic lets the content overflow
causing scrollbars to be drawn
### Notes
How elements determine if they have focus or not
```C
// in begin_{container} code
calculate focus
set has_focus property
// in the element code
if(PARENT_HAS_FOCUS()) {
update stuff
} else {
fast path to return
}
```
How to get ids:
1. use a name for each element
2. supply an id for each element
3. use a macro and the line position as id and then hash it
4. use a macro, get the code line and hash it

243
LAYOUT
View File

@ -1,243 +0,0 @@
Div Children Alignment
+------------------------------------------------+
|TOP-LEFT TOP TOP-RIGHT|
| |
| |
| |
| |
| |
| |
|LEFT CENTER RIGHT|
| |
| |
| |
| |
| |
| |
|BOTTOM-LEFT BOTTOM BOTTOM-RIGHT|
+------------------------------------------------+
ALIGNMENT CHART:
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
| TOP-LEFT, ROW: | TOP-LEFT, COLUMN: | BOTTOM, ROW: | BOTTOM, COLUMN: | TOP-RIGHT, ROW: | TOP-RIGHT, COLUMN: |
| | | | | | |
| +------------------------- - | +----------- - | | +----+ | | - -----------+ |
| |+-------++-----++-----+ | |+-------+ | | | E1 | | | +-------+| |
| || E1 || E2 || | | || E1 | | | | | | - -----------------------+ | | E1 || |
| || |+-----+| E3 | | || | | | +----+ | +-------++-----++-----+| | | || |
| |+-------+ | | | |+-------+ | +-------+ +---+ | +------+ | | E1 || E2 || || | +-------+| |
| | +-----+ | |+----+ | | E1 |+------+|E3 | | | E2 | | | |+-----+| E3 || | +----+| |
| ' | || E2 | | | || E2 || | | | | | +-------+ | || | | E2 || |
| ' | |+----+ | +-------++------++---+ | +------+ | +-----+| | +----+| |
| | |+---------+ | - ------------------------ - | +--+ | ' | +---------+| |
| | || E3 | | | |E3| | ' | | E3 || |
| | |+---------+ | | +--+ | | +---------+| |
| | ' | | - ----------- - | | ' |
| | ' | | | | ' |
| | | | | | |
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
| LEFT, ROW: | LEFT, COLUMN: | BOTTOM-RIGHT, ROW: | BOTTOM-RIGHT, COLUMN: | TOP, ROW: | TOP, COLUMN: |
| | | | | | |
| | ' | | ' | | - -------------- - |
| ' | |+-------+ | | +-------+| | | +----------+ |
| | +----+ | || E1 | | ' | | E1 || | - ----------------------- - | | E1 | |
| |+------+ | | | || | | +-----+| | | || | +------++----++-----+ | | | |
| || |+-----+| | | |+-------+ | +-------+ | || | +-------+| | | E1 || E2 || E3 | | +----------+ |
| || E1 || E2 || E3 | | |+----+ | | E1 |+-----+| E3 || | +----+| | | |+----+| | | +--------+ |
| || |+-----+| | | || E2 | | | || E2 || || | | E2 || | +------+ | | | | E2 | |
| |+------+ | | | |+----+ | +-------++-----++-----+| | +----+| | +-----+ | +--------+ |
| | +----+ | |+---------+ | - -----------------------+ | +---------+| | | +------+ |
| ' | || E3 | | | | E3 || | | | E3 | |
| ' | |+---------+ | | +---------+| | | | | |
| | ' | | - -----------+ | | +------+ |
| | ' | | | | |
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
| BOTTOM-LEFT, ROW: | BOTTOM-LEFT, COLUMN: | RIGHT, ROW: | RIGHT, COLUMN: | CENTER, ROW: | CENTER, COLUMN: |
| | | | | | |
| | ' | | ' | | | |
| | |+-------+ | | +-------+| | | | +-----------+ |
| | || E1 | | ' | | E1 || | | | | E1 | |
| ' | || | | +----+| | | || | | +----+ | | | | |
| | +-----+ | |+-------+ | +------+ | || | +-------+| | +------+ | | | | +-----------+ |
| |+-------+ | | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ |
| || E1 |+-----+| E3 | | || E2 | | | E1 || E2 || E3 || | | E2 || | ---|--E1--||-E2-||-E3-|--- | ----|---E2----|---- |
| || || E2 || | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ |
| |+-------++-----++-----+ | |+---------+ | +------+ | || | +---------+| | +------+ | | | | +-------+ |
| +------------------------- - | || E3 | | +----+| | | E3 || | | +----+ | | E3 | |
| | |+---------+ | ' | +---------+| | | | | | | |
| | +----------- - | ' | ' | | | +-------+ |
| | | | ' | | | |
| | | | | | |
+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
div (
align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER
size_x/y: EXACT(x) | GROW() | FIT(min, max)
scroll_x/y: true | false
resize_x/y: true | false
layout: ROW | COLUMN
)
align: alignment of the children elements
size: how the div should be sized
scroll: enables scrollbars
layout: the layout direction of the children
COLUMN ROW
+--------------------+ +----------------------------------------------------+
| +----------------+ | |+----------------+ |
| | | | || |+------------+ |
| | | | || || |+------------------+|
| | E1 | | || E1 || E2 || E3 ||
| | | | || || |+------------------+|
| | | | || |+------------+ |
| +----------------+ | |+----------------+ |
| +------------+ | +----------------------------------------------------+
| | | |
| | E2 | |
| | | | (both have center alignment)
| +------------+ |
|+------------------+|
|| ||
|| E3 ||
|| ||
|| ||
|+------------------+|
+--------------------+
Element {
id: uint
sizing: { min_w, min_h max_w, max_h }
bounds: { x, y, w, h }
}
id: unique identifier of the element
sizing: the size that the element wants
bounds: the absoulte bounds that the element got assigned
Rendering
=========
Rendering happens when the element is called (immediately for leaf widgets like buttons and at the end
for root widgets like divs). The drawing is done on the bounds assigned to the widget, these bounds
have a one-frame delay on the current layout.
The layout is calculated by each div at the end of their block and at frame end all the sizes and positions
are assigned at frame end by iterating the element tree.
ElemDiv {
align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER
size_x/y: { min, max }
scroll_x/y: true | false
layout: ROW | COLUMN
children_size_x/y: { min, max }
}
size:
- min != max -> FIT sizing, fit to the content but respect the min and max size
- min == max == 0 -> GROW sizing, grow to the max amount of space possible
- min == max != 0 -> EXACT sizing
children_size: the size of the combined children sizes
root(size_x: screen width, size_y: screen height, layout: ROW) {
div1(layout: COLUMN, size_x: FIT, size_y GROW, resize_x: true) {
E1()
E2()
E3()
E4()
} <-(end div 1)
div2(size_x: GROW, size_y: GROW) {
...
} <-(end div 2)
div3(layout: COLUMN, size_x: FIT, size_y: GROW) {
E5()
E6()
E7()
} <-(end div 3)
} <-(end root)
(frame end)
+-Root-Div------------------------------------------------+
|+-Div-1----------++-Div-2--------------------++-Div-3---+|
||+--------------+|| ||+-------+||
||| E1 ||| ||| E5 |||
||| ||| ||| |||
||+--------------+|| ||+-------+|| [Root Div]
||+--------------+|| ||+-------+|| |
||| E2 ||| ||| E6 ||| +----------+----+-------+
||| ||| ||| ||| v v v
||+--------------+|| ||+-------+|| [Div 1] [Div 2] [Div 3]
||+------+ || ||+-------+|| | |
||| | || ||| E7 ||| +----+----+----+ |
||| E3 | || ||| ||| v v v v |
||| | || ||+-------+|| [E1] [E2] [E3] [E4] +----+----+
||+------+ || || || v v v
||+------+ || || || [E5] [E6] [E7]
||| | || || ||
||| E4 | || || ||
||| | || || ||
||+------+ || || ||
|| || || ||
|+----------------++--------------------------++---------+|
+---------------------------------------------------------+
the call order is as follows
E1() -> updates the children size of div1
E2() -> " "
E3() -> " "
E4() -> " "
end div1() -> updates the children size of root
end div2() -> updates the children size of root
E5() -> updates the children size of div3
E6() -> " "
E7() -> " "
end root() -> does nothing
at frame end:
* Root: the root has a size constraint of fit so the bounds get assigned the whole window
* Div 1: the width has a size of fit, so it gets set to the children bounds, the height is set
to the root height since it has a height of GROW
- E1 to E4 get laid out
* Div 2: it has a width of GROW which is **along** the layout axis, so it gets added to grow list
the height gets set to the root height
* Div 3: the width is FIT, so it gets set to the content width, the height gets se to the root
height.
- E5 to E7 get laid out
* Div 2: is given a width (if there were other contending grow divs along the layout axis they
would also get sized).
- Now that div 2 has a size all it's children can be given a size
Styling
=======
The element bounds include the whole CSS box model:
+---------------------------------------------+
| MARGIN |
| +-----------------------------------+ |
| |xxxxxxxxxxx BORDER xxxxxxxxxxxx| |
| |x+-------------------------------+x| |
| |x| PADDING |x| |
| |x| +-----------------------+ |x| |
| |x| | | |x| |
| |x| | CONTENT | |x| |
| |x| | | |x| |
| |x| +-----------------------+ |x| |
| |x| |x| |
| |x+-------------------------------+x| |
| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| |
| +-----------------------------------+ |
| |
+---------------------------------------------+
Styling happens via a .css file, the sizing strictly refers to the content, so if the the user
requests an exact size of 100px*100px the content box will have those dimensions, but the element
bounds will be larger.

165
LICENSE
View File

@ -1,165 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
CFLAGS = -Wall -Wextra -pedantic -std=c11 -g -Iraylib/include
CC = gcc
LDFLAGS = -Lraylib/lib -lm
all: ugui
ugui: ugui.o vectree.o cache.o timer.o raylib/lib/libraylib.a
ugui.o: ugui.c ugui.h
vectree.o: vectree.c ugui.h
cache.o: cache.c ugui.h
timer.o: timer.c timer.h

View File

@ -1 +0,0 @@
Welcome to the ugui library.

136
TODO
View File

@ -1,136 +0,0 @@
# TODOs, semi-random sorting
[x] Implement glyph draw command
[x] Implement div.view and scrollbars
[x] Port font system from C to C3 (rewrite1)
[ ] Update ARCHITECTURE.md
[x] Write a README.md
[x] Use an arena allocator for cache
[ ] Do not redraw if there was no update (no layout and no draw)
[ ] Do command buffer damage tracking based on a context grid (see rxi writeup)
[x] Better handling of the active and focused widgets, try to maintain focus until mouse release (fix scroll bars)
[x] Clip element bounds to parent div, specifically text
[ ] Resizeable divs
[x] Implement a z index and sort command buffer based on that
[ ] Ctx.set_z_index()
[x] Sort command buffer on insertion
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot
of boiler plate and reuse it
[x] The id combination in gen_id() uses an intger division, which is costly, use another combination
function that is non-linear and doesn't use division
[ ] Animations, somehow
[x] Maybe cache codepoint converted strings
[x] Fix scroll wheel when div is scrolled
[ ] Be consistent with the initialization methods some are foo.new() and some are foo.init()
[ ] Implement image loading (.bmp, .ff, .qoi and .png), in the future even lossy images like .jpg
[x] .qoi
[ ] .ff
[ ] .bmp
[ ] .png
[ ] .jpg
[ ] gif support?
[x] layout_set_max_rows() and layout_set_max_columns()
[x] Maybe SDF sprites??
[x] Stylesheets and stylesheet import
[x] use SDF to draw anti-aliased rounded rectangles https://zed.dev/blog/videogame
[ ] Subdivide modules into ugui::ug for exported functions and ugui::core for
internal use functions (used to create widgets)
[x] The render loop RAPES the gpu, valve pls fix
[x] The way the element structures are implemented wastes a lot of memory since
each struct Elem, struct Cmd, etc. is as big as the largest element. It would
be better to use a different allcation strategy.
[ ] Add a way to handle time events like double clicks
[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the
content, the background color is applied starting from the border. Right now push_rect() offsets
the background rect by both border and padding
[x] Investigate why the debug pointer (cyan rectangle) disappears...
## Layout
[x] Flexbox
[x] Center elements to the row/column
[x] Text wrapping / reflow
[x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct
[x] Correct whitespace handling in text (\t \r etc)
[x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
instead of the curren multi-frame approach.
[x] Implement column/row sizing (min, max)
[x] Implement a way to size the element as the current row/column size
* +-------------+
* | |
* +-------------+
* +--+
* | |
* +--+
* <------------->
* column size
See the calculator example for why it is useful
[ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing
of elements
[x] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly)
[x] Use containing_rect() in position_element() to skip some computing and semplify the function
[x] Rename position_element() to layout_element()
[x] Make functions to mark rows/columns as full, to fix the calculator demo
## Input
[x] Keyboard input
[x] Mouse scroll wheel
[ ] Touch input
[x] Do not set input event to true if the movement was zero (like no mouse movement)
[x] Use input event flags, for example to consume the input event
[x] Fix bug in text box: when spamming keys you can get multiple characters in the text input field
of the context, this causes a bug where only the first char is actually used
## Commands
[x] rect commads should have:
- border width
- border radius
[x] add a command to update an atlas
[x] New window command, useful for popups
[x] Text command returns the text bounds, this way we can avoid the pattern
draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ...
[ ] Rounded rectangle with different radius for each corner
## Atlas
[ ] Add an interface to create, destroy, update and get atlases based on their ids
[ ] Implement multiple font atlases
[ ] Pixel format conversion
## Fonts
[x] Fix the missing alpha channel
[x] Fix the alignment
## Widgets
[x] Dynamic text box to implement an fps counter
[x] Button with label
[x] Text Input box
[ ] Icon Buttons
[x] Switch
[x] Checkbox
[ ] Selectable text box
## API
[ ] Introduce a Layout structure that specifies the positioning of elements inside
a Div element. This would allow specifying alignment, maximum and minimum sizing
margins between children, etc.
This is different from style which is applied per-element.
[ ] Remove Ids for element that don't need them. Such elements are button, toggles,
and all elements which do not have internal data that has to be cached and/or
queried by the user for later use. This allows for smaller caches and in general
reduces some load, since most of the stuff is recomputed for every frame.
## SDL3 Renderer
[x] smart batching
[x] maybe use instancing since we are always drawing the same geometry. With instancing every
different quad could have its coulour, border and radius with much better performance than
issuing a draw call for every quad (and uploading it)
https://rastertek.com/dx11win10tut48.html
https://www.braynzarsoft.net/viewtutorial/q16390-33-instancing-with-indexed-primitives
[ ] implement min and max fps

211
cache.c Normal file
View File

@ -0,0 +1,211 @@
// LRU cache:
/*
* The cache uses a pool (array) containing all the elements and a hash table
* associating the position in the pool with the element id
*/
#include <stdlib.h>
#include <string.h>
#include "ugui.h"
// To have less collisions TABLE_SIZE has to be larger than the cache size
#define CACHE_SIZE 265
#define TABLE_SIZE (CACHE_SIZE * 1.5f)
#define CACHE_NCYCLES (CACHE_SIZE * 2 / 3)
#define CACHE_BSIZE (((CACHE_SIZE + 0x3f) & (~0x3f)) >> 6)
#define CACHE_BRESET(b) \
for (int i = 0; i < CACHE_BSIZE; b[i++] = 0) \
;
#define CACHE_BSET(b, x) b[(x) >> 6] |= (uint64_t)1 << ((x)&63)
#define CACHE_BTEST(b, x) (b[(x) >> 6] & ((uint64_t)1 << ((x)&63)))
/* Hash Table Implementation ----------------------------------------------------- */
#define HASH_MAXSIZE 4096
// hash table (id -> cache index)
typedef struct {
UgId id;
uint32_t index;
} IdElem;
typedef struct _IdTable {
uint32_t items, size, exp;
IdElem bucket[];
} IdTable;
IdTable *table_create(uint32_t size)
{
if (!size || size > HASH_MAXSIZE) {
return NULL;
}
/* round to the greater power of two */
/* FIXME: check for intger overflow here */
uint32_t exp = 32 - __builtin_clz(size - 1);
size = 1 << (exp);
/* FIXME: check for intger overflow here */
IdTable *ht = malloc(sizeof(IdTable) + sizeof(IdElem) * size);
if (ht) {
ht->items = 0;
ht->size = size;
memset(ht->bucket, 0, sizeof(IdTable) * size);
}
return ht;
}
void table_destroy(IdTable *ht)
{
if (ht) {
free(ht);
}
}
// Find and return the element by pointer
IdElem *table_search(IdTable *ht, UgId id)
{
if (!ht) {
return NULL;
}
// In this case id is the hash
uint32_t idx = id % ht->size;
for (uint32_t x = 0, i; x < ht->size; x++) {
i = (idx + x) % ht->size;
if (ht->bucket[i].id == 0 || ht->bucket[i].id == id) {
return &(ht->bucket[i]);
}
}
return NULL;
}
// FIXME: this simply overrides the found item
IdElem *table_insert(IdTable *ht, IdElem entry)
{
IdElem *r = table_search(ht, entry.id);
if (r != NULL) {
if (r->id != 0) {
ht->items++;
}
*r = entry;
}
return r;
}
IdElem *table_remove(IdTable *ht, UgId id)
{
if (!ht) {
return NULL;
}
IdElem *r = table_search(ht, id);
if (r) {
r->id = 0;
}
return r;
}
/* Cache Implementation ---------------------------------------------------------- */
// Every CACHE_CYCLES operations mark not-present the unused elements
#define CACHE_CYCLE(c) \
do { \
if (++(c->cycles) > CACHE_NCYCLES) { \
for (int i = 0; i < CACHE_BSIZE; i++) { \
c->present[i] &= c->used[i]; \
c->used[i] = 0; \
} \
c->cycles = 0; \
} \
} while (0)
/* FIXME: check for allocation errors */
UgElemCache ug_cache_init(void)
{
IdTable *t = table_create(TABLE_SIZE);
UgElem *a = malloc(sizeof(UgElem) * CACHE_SIZE);
uint64_t *p = malloc(sizeof(uint64_t) * CACHE_BSIZE);
uint64_t *u = malloc(sizeof(uint64_t) * CACHE_BSIZE);
CACHE_BRESET(p);
CACHE_BRESET(u);
return (UgElemCache) {.table = t, .array = a, .present = p, .used = u, 0};
}
void ug_cache_free(UgElemCache *cache)
{
if (cache) {
table_destroy(cache->table);
free(cache->array);
free(cache->present);
free(cache->used);
}
}
UgElem *ug_cache_search(UgElemCache *cache, UgId id)
{
if (!cache) {
return NULL;
}
IdElem *r;
r = table_search(cache->table, id);
/* MISS */
if (!r || id != r->id) {
return NULL;
}
/* MISS, the data is not valid (not present) */
if (!CACHE_BTEST(cache->present, r->index)) {
return NULL;
}
/* HIT, set as recently used */
CACHE_BSET(cache->used, r->index);
return (&cache->array[r->index]);
}
/* Look for a free spot in the present bitmap and return its index */
/* If there is no free space left then just return the first position */
int ug_cache_get_free_spot(UgElemCache *cache)
{
if (!cache) {
return -1;
}
int x = 0;
for (int b = 0; b < CACHE_BSIZE; b++) {
if (cache->present[b] == 0) {
x = 64;
} else {
x = __builtin_clzll(cache->present[b]);
}
x = 64 - x;
if (!CACHE_BTEST(cache->present, x + 64 * b)) {
return x + 64 * b;
}
}
return 0;
}
UgElem *ug_cache_insert_at(UgElemCache *cache, const UgElem *g, uint32_t index)
{
if (!cache) {
return NULL;
}
UgElem *spot = NULL;
/* Set used and present */
CACHE_BSET(cache->present, index);
CACHE_BSET(cache->used, index);
CACHE_CYCLE(cache);
spot = &(cache->array[index]);
*spot = *g;
IdElem e = {.id = g->id, .index = index};
if (!table_insert(cache->table, e)) {
return NULL;
}
return spot;
}
// Insert an element in the cache
UgElem *ug_cache_insert_new(UgElemCache *cache, const UgElem *g, uint32_t *index)
{
*index = ug_cache_get_free_spot(cache);
return ug_cache_insert_at(cache, g, *index);
}

5
compile_flags.txt Normal file
View File

@ -0,0 +1,5 @@
-Iraylib/include
-Wall
-Wextra
-pedantic
-std=c11

1
fm/README Normal file
View File

@ -0,0 +1 @@
File Manager using ugui

193
fm/libconf.c Normal file
View File

@ -0,0 +1,193 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdarg.h>
#include <linux/limits.h>
#include <sys/stat.h>
#include <sys/types.h>
#define PATHS_NO 3
static char temp[PATH_MAX] = {0};
static int valid_paths = 0;
static char paths[PATHS_NO][PATH_MAX] = {0}; // 0: xdg, 1: fallback, 2: global
static const mode_t CONFDIR_MODE = S_IRWXU | S_IRWXU | S_IRWXU | S_IRWXU;
static void err(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "[libconf]: ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
// implements "mkdir -p"
static int mkdir_p(const char *path)
{
char tmp_path[PATH_MAX] = {0};
strncpy(tmp_path, path, PATH_MAX - 1);
struct stat st;
for (char *tk = strtok(tmp_path, "/"); tk != NULL; tk = strtok(NULL, "/")) {
if (tk != tmp_path) {
tk[-1] = '/';
}
if (stat(tmp_path, &st)) {
if (errno != ENOENT) {
err("could not stat() %s: %s",
tmp_path,
strerror(errno));
return 1;
}
if (mkdir(tmp_path, CONFDIR_MODE)) {
err("could not mkdir() %s: %s",
tmp_path,
strerror(errno));
return 1;
}
} else if (!S_ISDIR(st.st_mode)) {
err("could not create directory %s: file exists but it is "
"not a directory",
tmp_path);
return 1;
}
}
return 0;
}
// TODO: add a way to add a custom directory to the paths, for example via an
// environment variable
// TODO: maybe add a LIBCONF_PATH that overrides the defaults
/* default paths are:
* 1. ${XDG_CONFIG_HOME}/libconf/appid.d/
* 2. ${HOME}/.config/libconf/appid.d/
* 3. etc/libconf/appid.d/
*/
static int fill_paths(const char *id)
{
// TODO: verify id
if (id == NULL) {
err("must provide a valid app id");
return 1;
}
const char *xdg = getenv("XDG_CONFIG_HOME");
if (xdg != NULL) {
snprintf(paths[0], PATH_MAX, "%s/libconf/%s.d", xdg, id);
}
const char *home = getenv("HOME");
if (home) {
snprintf(temp, PATH_MAX, "%s/.config/libconf/%s.d", home, id);
if (mkdir_p(temp) == 0) {
strcpy(paths[1], temp);
}
}
// do not create global config path since most likely we don't have
// the necessary permissions to do so
snprintf(paths[2], PATH_MAX, "/etc/libconf/%s.d", id);
for (size_t i = 0; i < PATHS_NO; i++) {
printf("paths[%ld] = %s\n", i, paths[i]);
}
valid_paths = 1;
return 0;
}
// get config file path
const char *lcnf_get_conf_file(const char *id, const char *name, int global)
{
static char str[PATH_MAX] = {0};
if (id == NULL) {
return NULL;
}
if (!valid_paths) {
if (fill_paths(id)) {
return NULL;
}
}
// quick path for global config file
if (global && paths[2][0] != '\0') {
snprintf(str, PATH_MAX, "%s/%s", paths[2], name);
return str;
}
int found = 0;
for (size_t i = 0; i < PATHS_NO; i++) {
if (paths[i][0] == '\0') {
continue;
}
struct stat st;
snprintf(str, PATH_MAX, "%s/%s", paths[i], name);
if (stat(str, &st) && errno != ENOENT) {
err("could not stat %s: %s", str, strerror(errno));
}
if (S_ISREG(st.st_mode)) {
found = 1;
break;
}
}
return found ? str : NULL;
}
// get config file directory path
const char *lcnf_get_conf_dir(const char *id, int global)
{
if (id == NULL) {
return NULL;
}
if (global) {
return paths[2][0] != '\0' ? paths[2] : NULL;
}
if (paths[0][0] != '\0') {
return paths[0];
} else if (paths[1][0] != '\0') {
return paths[1];
} else {
return NULL;
}
}
// TODO: watch directory for file updates
int lcnf_watch_conf_dir(const char *id, int global);
// TODO: collect all files into a temporary one, useful for when you have multiple
// configuration files like 00-foo.conf, 01-bar.conf and 99-baz.conf
const char *lcnf_collect_conf_files(const char *id, int global);
#if 1
int main(void)
{
const char *p = lcnf_get_conf_file("pippo", "pippo.toml", 0);
if (p) {
printf("config found: %s\n", p);
} else {
printf("config not found\n");
}
return 0;
}
#endif

7
fm/libconf.h Normal file
View File

@ -0,0 +1,7 @@
#ifndef LIBCONF_H_
#define LIBCONF_H_
const char *lcnf_get_conf_file(const char *id, const char *name, int global);
const char *lcnf_get_conf_dir(const char *id, int global);
#endif

8
get_raylib.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
mkdir -p raylib
wget https://github.com/raysan5/raylib/releases/download/5.0/raylib-5.0_linux_amd64.tar.gz
tar -xvf raylib-5.0_linux_amd64.tar.gz
mv ./raylib-5.0_linux_amd64/* ./raylib/
rm -rf raylib-5.0_linux_amd64 raylib-5.0_linux_amd64.tar.gz

View File

View File

@ -1,11 +0,0 @@
{
"provides" : "ugui",
"sources" : [ "src/**" ],
"targets" : {
"linux-x64" : {
"link-args" : [],
"dependencies" : ["schrift"],
"linked-libraries" : []
}
}
}

View File

View File

@ -1,131 +0,0 @@
module ugui;
import std::io;
faultdef CANNOT_PLACE, INVALID_TYPE;
enum AtlasType {
ATLAS_GRAYSCALE,
ATLAS_R8G8B8A8,
}
// black and white atlas
struct Atlas {
AtlasType type;
Id id;
ushort width, height;
char[] buffer;
Point row;
ushort row_h;
}
// bytes per pixel
macro usz AtlasType.bpp(type)
{
switch (type) {
case ATLAS_GRAYSCALE: return 1;
case ATLAS_R8G8B8A8: return 4;
}
}
macro typeid AtlasType.underlying(type)
{
switch (type) {
case ATLAS_GRAYSCALE: return char;
case ATLAS_R8G8B8A8: return uint;
}
}
/*
// FIXME: in and out types are not always known at compile time
macro @pixel_convert(p, AtlasType $in, AtlasType $out)
{
$if $in == $out:
return p;
$else
$switch
$case $in == ATLAS_R8G8B8A8 && $out == ATLAS_GRAYSCALE:
var r = ((p >> 0) & 0xff);
var g = ((p >> 8) & 0xff);
var b = ((p >> 16) & 0xff);
var a = ((p >> 24) & 0xff);
if (a == 0) return (char)0;
return (ATLAS_GRAYSCALE.underlying())(((float)r+g+b) / 3.0f);
$case $in == ATLAS_GRAYSCALE && $out == ATLAS_R8G8B8A8:
var x = (char)(p/3.0);
return (ATLAS_R8G8B8A8.underlying())(x|(x<<8)|(x<<16)|(255<<24));
$default: $error "Unimplemented pixel format conversion";
$endswitch
$endif
}
*/
fn void? Atlas.new(&atlas, Id id, AtlasType type, ushort width, ushort height)
{
atlas.id = id;
atlas.type = type;
atlas.width = width;
atlas.height = height;
atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height*type.bpp());
}
fn void Atlas.free(&atlas)
{
free(atlas.buffer);
}
/*
* pixels -> +--------------+-----+
* | | | h
* | | | e
* | | | i
* | | | g
* | | | h
* | | | t
* +--------------+-----+
* |<--- width -->|
* |<----- stride ----->|
* bytes per pixels are inferred and have to be the same
* as the atlas type
*/
// place a rect inside the atlas
// uses a row first algorithm
// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/
fn Point? Atlas.place(&atlas, char[] pixels, ushort w, ushort h, ushort stride)
{
Point p;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
atlas.row.x = 0;
atlas.row.y = atlas.row.y + atlas.row_h;
atlas.row_h = 0;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
return CANNOT_PLACE?;
}
}
usz bpp = atlas.type.bpp();
for (usz y = 0; y < h; y++) {
for (usz x = 0; x < w; x++) {
char[] buf = atlas.buffer[(usz)(p.y+y)*atlas.width*bpp + (p.x+x)*bpp ..];
char[] pix = pixels[(usz)y*stride*bpp + x*bpp ..];
buf[0..bpp-1] = pix[0..bpp-1];
}
}
atlas.row.x += w;
if (h > atlas.row_h) {
atlas.row_h = h;
}
return p;
}

View File

@ -1,149 +0,0 @@
module cache{Key, Value, SIZE};
/* LRU Cache
* The cache uses a pool (array) to store all the elements, each element has
* a key (id) and a value. A HashMap correlates the ids to an index in the pool.
* To keep track of which items were recently used two bit arrays are kept, one
* stores the "used" flag for each index and anothe the "present" flag.
* Every NCYCLES operations the present and used arrays are updated to free up
* the elements that were not recently used.
*/
import std::core::mem;
import std::core::mem::allocator;
import std::collections::bitset;
import std::collections::map;
alias BitArr = bitset::BitSet{SIZE};
alias IdTable = map::HashMap{Key, usz};
alias IdTableEntry = map::Entry{Key, usz};
const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0);
struct Cache {
Allocator allocator;
BitArr present, used;
IdTable table;
Value[] pool;
usz cycle_count;
}
// Every CACHE_CYCLES operations mark as not-present the unused elements
macro Cache.cycle(&cache) @private {
cache.cycle_count++;
if (cache.cycle_count > CACHE_NCYCLES) {
for (usz i = 0; i < cache.present.data.len; i++) {
cache.present.data[i] &= cache.used.data[i];
cache.used.data[i] = 0;
}
cache.cycle_count = 0;
}
}
fn void? Cache.init(&cache, Allocator allocator)
{
cache.allocator = allocator;
cache.table.init(allocator, capacity: SIZE);
// FIXME: this shit is SLOW
foreach (idx, bit : cache.used) { cache.used[idx] = false; }
foreach (idx, bit : cache.present) { cache.present[idx] = false; }
cache.pool = allocator::new_array(allocator, Value, SIZE);
}
fn void Cache.free(&cache)
{
(void)cache.table.free();
(void)allocator::free(cache.allocator, cache.pool);
}
fn Value*? Cache.search(&cache, Key id)
{
// get_entry() faults on miss
IdTableEntry* entry = cache.table.get_entry(id)!;
/* MISS, wrong key */
if (entry.key != id) {
cache.table.remove(id)!;
return NOT_FOUND?;
}
/* MISS, the data is not valid (not present) */
if (!cache.present[entry.value]) {
// if the data is not present but it is still in the table, remove it
cache.table.remove(id)!;
return NOT_FOUND?;
}
/* HIT, set as recently used */
//io::printfn("HIT: %d [%d]", entry.value, entry.key);
cache.used[entry.value] = true;
return &(cache.pool[entry.value]);
}
fn void Cache.remove(&cache, Key id)
{
IdTableEntry*? entry = cache.table.get_entry(id);
if (catch entry) {
return;
}
// found, remove it
cache.present[entry.value] = false;
(void)cache.table.remove(id);
}
/* Look for a free spot in the present bitmap and return its index */
/* If there is no free space left then just return the first position */
fn usz Cache.get_free_spot(&cache) @private
{
// TODO: in the upgrade to c3 1.7.5 use @bitsof()
const BITS = $typeof(cache.present.data[0]).sizeof*8;
foreach (idx, d: cache.present.data) {
if (d != $typeof(d).max) {
usz spot = idx*BITS + BITS-d.clz();
if (cache.used[spot]) unreachable("free spot is not actually free: %d", spot);
return spot;
}
}
return 0;
}
fn Value*? Cache.insert_at(&cache, Value* g, Key id, usz index) @private
{
// TODO: verify index, g and id
Value* spot;
/* Set used and present */
cache.present.set(index);
cache.used.set(index);
cache.cycle();
spot = &(cache.pool[index]);
*spot = *g;
cache.table.set(id, index);
return spot;
}
// Insert an element in the cache, returns the index
fn Value*? Cache.insert_new(&cache, Value* g, Key id)
{
usz index = cache.get_free_spot();
return cache.insert_at(g, id, index);
}
fn Value*? Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null)
{
Value*? c = cache.search(id);
if (catch e = c) {
if (e != NOT_FOUND) {
return e?;
} else {
// if the element is new (inserted) set the is_new flag
if (is_new) *is_new = true;
return cache.insert_new(g, id);
}
} else {
if (is_new) *is_new = false;
return c;
}
}

View File

@ -1,217 +0,0 @@
module ugui;
import std::io;
import std::collections::list;
// command type
enum CmdType {
CMD_RECT,
CMD_UPDATE_ATLAS,
CMD_SPRITE,
CMD_SCISSOR,
CMD_REQ_SKIP_FRAME,
}
// command to draw a rect
struct CmdRect {
Rect rect;
ushort radius;
Color color;
}
struct CmdUpdateAtlas {
Id id;
char* raw_buffer;
short width, height, bpp;
}
struct CmdSprite {
Id texture_id;
SpriteType type;
Rect rect;
Rect texture_rect;
Color hue;
}
// if rect is zero Rect{0} then reset the scissor
struct CmdScissor {
Rect rect;
}
// command structure
struct Cmd (Printable) {
CmdType type;
int z_index;
union {
CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
CmdScissor scissor;
}
}
// command queue
alias CmdQueue = list::List{Cmd};
fn int Cmd.compare_to(Cmd a, Cmd b)
{
if (a.z_index == b.z_index) return 0;
return a.z_index > b.z_index ? 1 : -1;
}
// FIXME: This sorting method does not fully respect layer ordering for popups.
// Popups that start on the same layer but are enqueued at different times
// may still be rendered on top of each other, even if the first popup
// has elements in higher layers than the second. The current approach
// does not truly sort by layer; it only moves elements from higher layers
// to the end of the queue as they are encountered.
fn void? CmdQueue.sort(&queue)
{
CmdQueue stack;
stack.init(allocator::tmem);
for (isz i = queue.len()-1; i > 0; i--) {
Cmd cur = (*queue)[i];
Cmd next = (*queue)[i - 1];
// cur < next
if (cur.compare_to(next) < 0) {
stack.push(next);
queue.remove_at(i-1);
}
}
usz l = stack.len();
for (usz i; i < l; i++) {
queue.push(stack.pop())!;
}
}
// implement the Printable interface
fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
{
usz ret;
ret += f.printf("Cmd{ type: %s, z_index: %d, ", cmd.type, cmd.z_index)!;
switch (cmd.type) {
case CMD_RECT:
ret += f.print("CmdRect")!;
ret += io::struct_to_format(cmd.rect, f, false)!;
case CMD_SCISSOR:
ret += f.print("CmdScissor")!;
ret += io::struct_to_format(cmd.scissor, f, false)!;
case CMD_SPRITE:
ret += f.print("CmdSprite")!;
ret += io::struct_to_format(cmd.sprite, f, false)!;
case CMD_UPDATE_ATLAS:
ret += f.print("CmdUpdateAtlas")!;
ret += io::struct_to_format(cmd.update_atlas, f, false)!;
case CMD_REQ_SKIP_FRAME:
ret += f.print("Skip Frame Request")!;
}
ret += f.print("}")!;
return ret;
}
macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max})
{
bool no_area = rect.w <= 0 || rect.h <= 0;
return no_area || !rect.collides(clip);
}
// FIXME: this whole thing could be done at compile time, maybe
macro Ctx.push_cmd(&ctx, Cmd cmd, int z_index)
{
cmd.z_index = z_index;
Rect rect;
switch (cmd.type) {
case CMD_RECT: rect = cmd.rect.rect;
case CMD_SPRITE: rect = cmd.sprite.rect;
default: return ctx.cmd_queue.push(cmd);
}
if (cull_rect(rect, ctx.div_scissor)) {
// println("NOPE: ", cmd.rect.rect, cmd.z_index);
// unreachable();
return;
}
return ctx.cmd_queue.push(cmd);
}
fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
{
Cmd sc = {
.type = CMD_SCISSOR,
.scissor.rect = rect.intersection(ctx.div_scissor),
};
ctx.push_cmd(sc, z_index);
}
fn void? Ctx.reset_scissor(&ctx, int z_index) => ctx.push_cmd({.type=CMD_SCISSOR,.scissor.rect=ctx.div_scissor}, z_index);
fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
{
Rect border = style.border;
ushort radius = style.radius;
Color bg = style.bg;
Color border_color = style.secondary;
// FIXME: this implies that the border has to be uniform
if (!border.is_null()) {
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = rect,
.rect.color = border_color,
.rect.radius = radius + border.x,
};
ctx.push_cmd(cmd, z_index);
}
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = {
.x = rect.x + border.x,
.y = rect.y + border.y,
.w = rect.w - (border.x+border.w),
.h = rect.h - (border.y+border.h),
},
.rect.color = bg,
.rect.radius = radius,
};
ctx.push_cmd(cmd, z_index);
}
// TODO: accept a Sprite* instead of all this shit
fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL)
{
Cmd cmd = {
.type = CMD_SPRITE,
.sprite.type = type,
.sprite.rect = bounds,
.sprite.texture_rect = texture,
.sprite.texture_id = texture_id,
.sprite.hue = hue,
};
ctx.push_cmd(cmd, z_index);
}
fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
{
Cmd up = {
.type = CMD_UPDATE_ATLAS,
.update_atlas = {
.id = atlas.id,
.raw_buffer = atlas.buffer,
.width = atlas.width,
.height = atlas.height,
.bpp = (ushort)atlas.type.bpp(),
},
};
// update the atlases before everything else
ctx.push_cmd(up, -1);
}
macro Ctx.dbg_rect(&ctx, Rect r, uint c = 0xff000042u) => ctx.push_rect(r, int.max, &&(Style){.bg=c.to_rgba()})!!;

View File

@ -1,396 +0,0 @@
module ugui;
import mtree;
import cache;
import std::io;
import std::core::string;
import std::core::mem::allocator;
import std::collections::pair;
import std::sort;
macro println(...)
{
$for var $i = 0; $i < $vacount; $i++:
io::print($vaexpr[$i]);
$endfor
io::printn();
}
// element ids are just long ints
alias Id = uint;
enum ElemType {
ETYPE_NONE,
ETYPE_DIV,
ETYPE_BUTTON,
ETYPE_SLIDER,
ETYPE_TEXT,
ETYPE_SPRITE,
}
bitstruct ElemFlags : uint {
bool updated : 0;
bool is_new : 1; // element is new in the cache
bool shown : 2; // element has been shown (drawn) this frame
}
bitstruct ElemEvents : uint {
bool key_press : 0;
bool key_release : 1;
bool key_repeat : 2;
bool mouse_hover : 3;
bool mouse_press : 4;
bool mouse_release : 5;
bool mouse_hold : 6;
bool update : 7;
bool text_input : 8;
bool has_focus : 9;
}
// element structure
struct Elem {
Id id;
int tree_idx;
ElemFlags flags;
ElemEvents events;
Rect bounds;
Rect children_bounds;
ElemType type;
Layout layout;
int z_index;
union {
ElemDiv div;
ElemButton button;
ElemSlider slider;
ElemText text;
ElemSprite sprite;
}
}
const uint MAX_ELEMENTS = 256;
const uint MAX_COMMANDS = 2048;
const uint STACK_STEP = 10;
const uint ROOT_ID = 1;
const uint TEXT_MAX = 64;
// Tuple of Element pointers, used when it is useful to get both parent and child
alias PElemTuple = pair::Pair{Elem*, Elem*};
// relationships between elements are stored in a tree, it stores just the ids
alias IdTree = mtree::MTree{Id};
// elements themselves are kept in a cache
alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
struct InputData {
InputEvents events;
struct mouse {
Point pos, delta;
// mouse_down: bitmap of mouse buttons that are held
// mouse_updated: bitmap of mouse buttons that have been updated
// mouse_released = mouse_updated & ~mouse_down
// mouse_pressed = mouse_updated & mouse_down
MouseButtons down;
MouseButtons updated;
// scroll wheel
Point scroll;
}
struct keyboard {
char[TEXT_MAX] text;
usz text_len;
ModKeys modkeys;
}
}
struct Ctx {
IdTree tree;
ElemCache cache;
CmdQueue cmd_queue;
StyleMap styles;
// total size in pixels of the context
ushort width, height;
Font font;
SpriteAtlas sprite_atlas;
bool skip_frame;
bool has_focus;
InputData input, current_input;
Id hover_id;
Id focus_id;
Rect div_scissor; // the current div bounds used for scissor test
int active_div; // tree node indicating the current active div
}
// return a pointer to the parent of the current active div
fn Elem*? Ctx.get_parent(&ctx)
{
Id parent_id = ctx.tree[ctx.active_div]!;
Elem* parent = ctx.cache.search(parent_id)!;
return parent;
}
macro @bits(#a) => $typeof(#a).sizeof*8;
macro Id.rotate_left(id, uint $n) => (id << $n) | (id >> (@bits(id) - $n));
const uint GOLDEN_RATIO = 0x9E3779B9;
// generate an id combining the hashes of the parent id and the label
// with the Cantor pairing function
fn Id? Ctx.gen_id(&ctx, Id id2)
{
// FIXME: this is SHIT
Id id1 = ctx.tree.get(ctx.active_div)!;
// Mix the two IDs non-linearly
Id mixed = id1 ^ id2.rotate_left(13);
mixed ^= id1.rotate_left(7);
mixed += GOLDEN_RATIO;
return mixed;
}
// compute the id from arguments and the line of the call
macro Id @compute_id(...)
{
Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE);
$for var $i = 0; $i < $vacount; $i++:
id ^= (Id)$vaexpr[$i].hash();
$endfor
return id;
}
// get or push an element from the cache, return a pointer to it
// resets all flags except is_new which is set accordingly
fn PElemTuple? Ctx.get_elem(&ctx, Id id, ElemType type)
{
bool is_new;
Elem* parent;
Elem* elem;
parent = ctx.get_parent() ?? &&(Elem){};
elem = ctx.cache.get_or_insert(&&(Elem){}, id, &is_new)!;
elem.flags = (ElemFlags)0;
elem.flags.is_new = is_new;
elem.flags.shown = true;
elem.id = id;
elem.layout = {};
if (is_new == false && elem.type != type) {
return WRONG_ELEMENT_TYPE?;
} else {
elem.type = type;
}
elem.z_index = parent.z_index;
elem.tree_idx = ctx.tree.add(ctx.active_div, id)!;
return {elem, parent};
}
// find an element, does not allocate a new one in cache
// THIS HAS TO BE A MACRO SINCE IT RETURNS A POINTER TO A TEMPORARY VALUE
macro Elem* Ctx.find_elem(&ctx, Id id)
{
Elem*? elem;
elem = ctx.cache.search(id);
if (catch elem) {
return &&(Elem){};
}
return elem;
}
fn Elem*? Ctx.get_active_div(&ctx)
{
Id id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(id);
}
fn void? Ctx.init(&ctx, Allocator allocator)
{
ctx.tree.init(MAX_ELEMENTS, allocator);
defer catch { (void)ctx.tree.free(); }
ctx.cache.init(allocator)!;
defer catch { (void)ctx.cache.free(); }
ctx.cmd_queue.init(allocator::mem, MAX_COMMANDS);
defer catch { (void)ctx.cmd_queue.free(); }
ctx.styles.init(allocator);
ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default"));
defer catch { ctx.styles.free(); }
ctx.active_div = 0;
}
fn void Ctx.free(&ctx)
{
(void)ctx.tree.free();
(void)ctx.cache.free();
(void)ctx.cmd_queue.free();
(void)ctx.font.free();
(void)ctx.sprite_atlas.free();
(void)ctx.styles.free();
}
fn void? Ctx.frame_begin(&ctx)
{
// 1. Reset the active div
// 2. Get the root element from the cache and update it
ctx.active_div = 0;
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!.first;
ctx.active_div = elem.tree_idx;
// The root should have the updated flag only if the size of the window
// was changed between frasmes, this propagates an element size recalculation
// down the element tree
elem.flags.updated = ctx.input.events.resize;
// if the window has focus then the root element also has focus, no other
// computation needed, child elements need to check the mouse positon and
// other stuff
//elem.flags.has_focus = ctx.has_focus;
elem.bounds = {0, 0, ctx.width, ctx.height};
elem.z_index = 0;
elem.div.scroll_x.enabled = false;
elem.div.scroll_y.enabled = false;
elem.layout.dir = ROW;
elem.layout.anchor = TOP_LEFT;
elem.layout.w = @exact(ctx.width);
elem.layout.h = @exact(ctx.height);
ctx.div_scissor = elem.bounds;
ctx.skip_frame = false;
}
fn void? Ctx.frame_end(&ctx)
{
Elem* root = ctx.get_active_div()!;
if (root.id != ROOT_ID) {
return WRONG_ID?;
}
// 2. clear input fields
ctx.input = ctx.current_input;
ctx.current_input.events = {};
ctx.current_input.mouse.scroll = {};
ctx.current_input.mouse.updated = BTN_NONE;
ctx.current_input.keyboard.text_len = 0;
// DO THE LAYOUT
ctx.layout_element_tree()!;
foreach (idx, id : ctx.tree.elem_vec) {
if (!ctx.tree.is_used((int)idx)) continue;
Elem* c = ctx.find_elem(id);
// reset events
c.events = {};
// reset shown flag
// TODO: use shown_last_frame to avoid this loop entirely
c.flags.shown = false;
}
// Propagate input events to the right elements
ctx.set_elem_events(ctx.hover_id);
ctx.set_elem_events(ctx.focus_id);
// 1. clear the tree
ctx.tree.nuke();
// send atlas updates
if (ctx.font.should_update) {
ctx.push_update_atlas(&ctx.font.atlas)!;
ctx.font.should_update = false;
}
if (ctx.sprite_atlas.should_update) {
ctx.push_update_atlas(&ctx.sprite_atlas.atlas)!;
ctx.sprite_atlas.should_update = false;
}
// send skip frame request
if (ctx.skip_frame) {
ctx.cmd_queue.push({.type = CMD_REQ_SKIP_FRAME});
}
// sort the command buffer by the z-index
// FIXME: sorting the buffer fucks with scissor commands that have to be kept in place
// TODO: instead of sorting at the end perform ordered inserts into the command buffer
//sort::countingsort(ctx.cmd_queue, fn uint(Cmd c) => c.z_index+1);
ctx.cmd_queue.sort()!;
// debug
$if $feature(DEBUG_POINTER):
// draw mouse position
Cmd cmd = {
.type = CMD_RECT,
.z_index = int.max-1, // hopefully over everything else
.rect.rect = {
.x = ctx.input.mouse.pos.x - 2,
.y = ctx.input.mouse.pos.y - 2,
.w = 4,
.h = 4,
},
.rect.color = 0xff00ffffu.to_rgba()
};
ctx.cmd_queue.push(cmd);
$endif
}
macro bool Ctx.is_hovered(&ctx, Elem *elem) => ctx.input.mouse.pos.in_rect(elem.bounds);
// Check if the element is hovered and/or focused, if it is update the context ids.
// The order in which the elements are passed to this function is not relevant
fn void Ctx.update_hover_and_focus(&ctx, Elem* elem)
{
bool hover = ctx.is_hovered(elem);
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_ANY));
if (hover) {
Elem* prev_hover = ctx.find_elem(ctx.hover_id);
bool different = prev_hover.id != elem.id;
bool still_hovered = ctx.is_hovered(prev_hover);
bool shown = prev_hover.flags.shown;
bool above = prev_hover.z_index > elem.z_index;
hover = !(different && still_hovered && shown && above);
}
if (focus) {
Elem* prev_focus = ctx.find_elem(ctx.hover_id);
bool different = prev_focus.id != elem.id;
bool shown = prev_focus.flags.shown;
bool above = prev_focus.z_index > elem.z_index;
focus = !(different && shown && above);
}
if (hover) ctx.hover_id = elem.id;
if (focus) ctx.focus_id = elem.id;
}
// FIXME: this does not work with touch
fn void Ctx.set_elem_events(&ctx, Id id)
{
bool hover = id == ctx.hover_id;
bool focus = id == ctx.focus_id;
Elem* e = ctx.find_elem(id);
e.events = {
.has_focus = focus,
.mouse_hover = hover,
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
.mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY),
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
.key_press = focus && ctx.input.events.key_press,
.key_release = focus && ctx.input.events.key_release,
.key_repeat = focus && ctx.input.events.key_repeat,
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
};
}

View File

@ -1,227 +0,0 @@
module ugui::font;
import schrift;
import std::collections::map;
import std::core::mem;
import std::core::mem::allocator;
import std::io;
import std::ascii;
// ---------------------------------------------------------------------------------- //
// CODEPOINT //
// ---------------------------------------------------------------------------------- //
// unicode code point, different type for a different hash
alias Codepoint = uint;
//macro uint Codepoint.hash(self) => ((uint)self).hash();
// ---------------------------------------------------------------------------------- //
// FONT ATLAS //
// ---------------------------------------------------------------------------------- //
/* width and height of a glyph contain the kering advance
* (u,v)
* +-------------*---+ -
* | ^ | | ^
* | |oy | | |
* | v | | |
* | .ii. | | |
* | @@@@@@. | | |
* | V@Mio@@o | | |
* | :i. V@V | | h
* | :oM@@M | | |
* | :@@@MM@M | | |
* | @@o o@M | | |
* |<->:@@. M@M | | |
* |ox @@@o@@@@ | | |
* | :M@@V:@@.| | v
* +-------------*---+ -
* |<---- w ---->|
* |<------ adv ---->|
*/
struct Glyph {
Codepoint code;
ushort u, v;
ushort w, h;
short adv, ox, oy;
}
const uint FONT_CACHED = 255;
alias GlyphTable = map::HashMap{Codepoint, Glyph};
faultdef TTF_LOAD_FAILED, MISSING_GLYPH, BAD_GLYPH_METRICS, RENDER_ERROR;
struct Font {
schrift::Sft sft;
String path;
Id id; // font id, same as atlas id
GlyphTable table;
float size;
float ascender, descender, linegap; // Line Metrics
Atlas atlas;
bool should_update; // should send update_atlas command, resets at frame_end()
}
macro Rect Glyph.bounds(&g) => {.x = g.ox, .y = g.oy, .w = g.w, .h = g.h};
macro Rect Glyph.uv(&g) => {.x = g.u, .y = g.v, .w = g.w, .h = g.h};
<*
@param [&inout] font
@param [in] name
@param [&in] path
@require height > 0, scale > 0: "height and scale must be positive non-zero"
*>
fn void? Font.load(&font, Allocator allocator, String name, ZString path, uint height, float scale)
{
font.table.init(allocator, capacity: FONT_CACHED);
font.id = name.hash();
font.size = height*scale;
font.sft = {
.xScale = (double)font.size,
.yScale = (double)font.size,
.flags = schrift::SFT_DOWNWARD_Y,
};
font.sft.font = schrift::loadfile(path);
if (font.sft.font == null) {
font.table.free();
return TTF_LOAD_FAILED?;
}
schrift::SftLMetrics lmetrics;
schrift::lmetrics(&font.sft, &lmetrics);
font.ascender = (float)lmetrics.ascender;
font.descender = (float)lmetrics.descender;
font.linegap = (float)lmetrics.lineGap;
//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
// like the letter 'A'
ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED));
font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!;
// preallocate the ASCII range
for (char c = ' '; c < '~'; c++) {
// FIXME: without @inline, this crashes with O1 or greater
font.get_glyph((Codepoint)c) @inline!;
}
}
<*
@param [&inout] font
*>
fn Glyph*? Font.get_glyph(&font, Codepoint code)
{
Glyph*? gp;
gp = font.table.get_ref(code);
if (catch excuse = gp) {
if (excuse != NOT_FOUND) {
return excuse?;
}
} else {
return gp;
}
// missing glyph, render and place into an atlas
Glyph glyph;
schrift::SftGlyph gid;
schrift::SftGMetrics gmtx;
if (schrift::lookup(&font.sft, (SftUChar)code, &gid) < 0) {
return MISSING_GLYPH?;
}
if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) {
return BAD_GLYPH_METRICS?;
}
schrift::SftImage img = {
.width = gmtx.minWidth,
.height = gmtx.minHeight,
};
char[] pixels = mem::new_array(char, (usz)img.width * img.height);
img.pixels = pixels;
if (schrift::render(&font.sft, gid, img) < 0) {
return RENDER_ERROR?;
}
glyph.code = code;
glyph.w = (ushort)img.width;
glyph.h = (ushort)img.height;
glyph.ox = (short)gmtx.leftSideBearing;
glyph.oy = (short)gmtx.yOffset;
glyph.adv = (short)gmtx.advanceWidth;
//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
Point uv = font.atlas.place(pixels, glyph.w, glyph.h, (ushort)img.width)!;
glyph.u = uv.x;
glyph.v = uv.y;
mem::free(pixels);
font.table.set(code, glyph);
font.should_update = true;
return font.table.get_ref(code);
}
<*
@param [&inout] font
*>
fn void Font.free(&font)
{
font.atlas.free();
font.table.free();
schrift::freefont(font.sft.font);
}
// ---------------------------------------------------------------------------------- //
// FONT LOAD AND QUERY //
// ---------------------------------------------------------------------------------- //
module ugui;
<*
@param [&inout] ctx
@param [in] name
@param [&in] path
@require height > 0, scale > 0: "height and scale must be positive non-zero"
*>
fn void? Ctx.load_font(&ctx, Allocator allocator, String name, ZString path, uint height, float scale = 1.0)
{
return ctx.font.load(allocator, name, path, height, scale);
}
<*
@param [&in] ctx
@param [in] label
*>
// TODO: check if the font is present in the context
fn Id Ctx.get_font_id(&ctx, String label) => (Id)label.hash();
<*
@param [&in] ctx
@param [in] name
*>
fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
{
// TODO: use the font name, for now there is only one font
if (name.hash() != ctx.font.id) {
return WRONG_ID?;
}
return &ctx.font.atlas;
}
<* @param [&in] font *>
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);

View File

@ -1,237 +0,0 @@
module ugui;
import std::io;
import std::math;
import std::core::string;
bitstruct InputEvents : uint {
bool resize : 0; // window size was changed
bool change_focus : 1; // window focus changed
bool mouse_move : 2; // mouse was moved
bool mouse_btn : 3; // mouse button pressed or released
bool mouse_scroll : 4; // mouse scroll wheel. x or y
bool text_input : 5;
bool mod_key : 6;
bool key_press : 7;
bool key_release : 8;
bool key_repeat : 9;
}
bitstruct MouseButtons : uint {
bool btn_left : 0;
bool btn_middle : 1;
bool btn_right : 2;
bool btn_4 : 3;
bool btn_5 : 4;
}
// FIXME: all of these names were prefixed with key_ idk if this is better,
// if it is remove the prefix on MouseButtons as well
// Modifier Keys, intended as any key that is not text
bitstruct ModKeys : uint {
bool lshift : 0;
bool rshift : 1;
bool lctrl : 2;
bool rctrl : 3;
bool lalt : 4;
bool ralt : 5;
bool lgui : 6;
bool rgui : 7;
bool num : 8;
bool caps : 9;
bool mode : 10;
bool scroll : 11;
bool bkspc : 12;
bool del : 13;
bool home : 14;
bool end : 15;
// arrow keys
bool up : 16;
bool down : 17;
bool left : 18;
bool right : 19;
}
const ModKeys KMOD_CTRL = {.lctrl = true, .rctrl = true};
const ModKeys KMOD_SHIFT = {.lshift = true, .rshift = true};
const ModKeys KMOD_ALT = {.lalt = true, .ralt = true};
const ModKeys KMOD_GUI = {.lgui = true, .rgui = true};
const ModKeys KMOD_TXT = {.bkspc = true, .del = true}; // modkeys that act like text input
const ModKeys KMOD_NONE = {};
const ModKeys KMOD_ANY = (ModKeys)(ModKeys.inner.max);
const MouseButtons BTN_NONE = {};
const MouseButtons BTN_ANY = (MouseButtons)(MouseButtons.inner.max);
const MouseButtons BTN_LEFT = {.btn_left = true};
const MouseButtons BTN_MIDDLE = {.btn_middle = true};
const MouseButtons BTN_RIGHT = {.btn_right = true};
const MouseButtons BTN_4 = {.btn_4 = true};
const MouseButtons BTN_5 = {.btn_5 = true};
const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max);
<* @param [&inout] ctx *>
fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String ...keys)
{
bool is_mod = (bool)(ctx.current_input.keyboard.modkeys & mod);
bool is_keys = true;
String haystack = (String)ctx.get_keys();
foreach (needle: keys) {
is_keys = is_keys && haystack.contains(needle);
}
return is_mod && is_keys;
}
// Window size was changed
<* @param [&inout] ctx *>
fn void? Ctx.input_window_size(&ctx, short width, short height)
{
if (width <= 0 || height <= 0) {
return INVALID_SIZE?;
}
ctx.current_input.events.resize = ctx.width != width || ctx.height != height;
ctx.width = width;
ctx.height = height;
if (ctx.current_input.events.resize) ctx.skip_frame = true;
}
// Window gained/lost focus
<* @param [&inout] ctx *>
fn void Ctx.input_changefocus(&ctx, bool has_focus)
{
// FIXME: raylib only has an API to query the focus status so we have to
// update the input flag only if the focus changed
ctx.current_input.events.change_focus = ctx.has_focus != has_focus;
ctx.has_focus = has_focus;
}
// NOTE: all of these refer to the previous frame's data
macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down;
macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down;
macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down;
macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn) => (ctx.mouse_pressed() & btn) != BTN_NONE;
macro Ctx.is_mouse_released(&ctx, MouseButtons btn) => (ctx.mouse_released() & btn) != BTN_NONE;
macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE;
// Mouse Buttons down
<* @param [&inout] ctx *>
fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons)
{
ctx.current_input.mouse.updated = ctx.current_input.mouse.down ^ buttons;
ctx.current_input.mouse.down = buttons;
ctx.current_input.events.mouse_btn = ctx.current_input.mouse.down != BTN_NONE || ctx.current_input.mouse.updated != BTN_NONE;
}
// Mouse was moved, report absolute position
<* @param [&inout] ctx *>
fn void Ctx.input_mouse_abs(&ctx, short x, short y)
{
ctx.current_input.mouse.pos.x = math::clamp(x, (short)0, ctx.width);
ctx.current_input.mouse.pos.y = math::clamp(y, (short)0, ctx.height);
short dx, dy;
dx = x - ctx.current_input.mouse.pos.x;
dy = y - ctx.current_input.mouse.pos.y;
ctx.current_input.mouse.delta.x = dx;
ctx.current_input.mouse.delta.y = dy;
ctx.current_input.events.mouse_move = dx != 0 || dy != 0;
}
// Mouse was moved, report relative motion
<* @param [&inout] ctx *>
fn void Ctx.input_mouse_delta(&ctx, short dx, short dy)
{
ctx.current_input.mouse.delta.x = dx;
ctx.current_input.mouse.delta.y = dy;
short mx, my;
mx = ctx.current_input.mouse.pos.x + dx;
my = ctx.current_input.mouse.pos.y + dy;
ctx.current_input.mouse.pos.x = math::clamp(mx, (short)0, ctx.width);
ctx.current_input.mouse.pos.y = math::clamp(my, (short)0, ctx.height);
ctx.current_input.events.mouse_move = dx != 0 || dy != 0;
}
<* @param [&inout] ctx *>
fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0)
{
ctx.current_input.mouse.scroll.x = (short)((float)-x*scale);
ctx.current_input.mouse.scroll.y = (short)((float)-y*scale);
ctx.current_input.events.mouse_scroll = x !=0 || y != 0;
}
<* @param [&inout] ctx *>
fn void Ctx.input_key_press(&ctx)
{
ctx.current_input.events.key_press = true;
}
<* @param [&inout] ctx *>
fn void Ctx.input_key_release(&ctx)
{
ctx.current_input.events.key_release = true;
}
<* @param [&inout] ctx *>
fn void Ctx.input_key_repeat(&ctx)
{
ctx.current_input.events.key_repeat = true;
}
// append utf-8 encoded text to the context text input
<* @param [&inout] ctx *>
fn void Ctx.input_text_utf8(&ctx, char[] text)
{
if (text == "") return;
usz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len;
usz len = text.len > remaining ? remaining : text.len;
char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..];
s[..len-1] = text[..len-1];
ctx.current_input.keyboard.text_len += len;
ctx.current_input.events.text_input = true;
}
<*
@param [&inout] ctx
@param [in] text
*>
fn void? Ctx.input_text_unicode(&ctx, uint[] text)
{
if (text.len == 0) return;
usz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len;
char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..];
usz off = conv::utf32to8(text, s)!;
ctx.current_input.keyboard.text_len += off;
ctx.current_input.events.text_input = true;
}
<* @param [&inout] ctx *>
fn void Ctx.input_char(&ctx, char c)
{
char[1] b = {c};
ctx.input_text_utf8(b[..]);
}
fn String Ctx.get_keys(&ctx) => (String)ctx.input.keyboard.text[:ctx.input.keyboard.text_len];
fn ModKeys Ctx.get_mod(&ctx) => ctx.input.keyboard.modkeys;
// Modifier keys, like control or backspace
<* @param [&inout] ctx *>
fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys, bool set)
{
if (set) {
ctx.current_input.keyboard.modkeys |= modkeys;
} else {
ctx.current_input.keyboard.modkeys &= ~modkeys;
}
ctx.current_input.events.mod_key = (uint)ctx.current_input.keyboard.modkeys != 0;
}

View File

@ -1,353 +0,0 @@
module ugui;
import std::math;
import std::io;
enum LayoutDirection {
ROW,
COLUMN,
}
enum Anchor {
TOP_LEFT,
LEFT,
BOTTOM_LEFT,
BOTTOM,
BOTTOM_RIGHT,
RIGHT,
TOP_RIGHT,
TOP,
CENTER
}
struct Layout {
Size w, h; // size of the CONTENT, does not include margin, border and padding
struct children { // the children size includes the children's margin/border/pading
Size w, h;
}
TextSize text;
ushort grow_children;
short occupied;
Point origin;
Point scroll_offset;
// false: the element is laid out according to the parent
// true: the element is laid out separate from all other children and the relative position to
// the parent is the .origin field
bool absolute;
LayoutDirection dir; // the direction the children are laid out
Anchor anchor; // how the children are positioned
Rect content_offset; // combined effect of margin, border and padding
}
// Returns the width and height of a @FIT() element based on it's wanted size (min/max)
// and the content size, this function is used to both update the parent's children size and
// give the dimensions of a fit element
// TODO: test and cleanup this function
macro Point Layout.get_dimensions(&el)
{
Point dim;
// if the direction is ROW then the text is placed horizontally with the children
if (el.dir == ROW) {
Size content_width = el.children.w + el.text.width;
Size width = el.w.combine(content_width);
short final_width = width.greater();
short text_height;
if (el.text.area != 0) {
short text_width = (@exact(final_width) - el.children.w).combine(el.text.width).min;
text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
}
Size content_height = el.children.h.comb_max(@exact(text_height));
Size height = el.h.combine(content_height);
short final_height = height.greater();
dim = {
.x = final_width + el.content_offset.x + el.content_offset.w,
.y = final_height + el.content_offset.y + el.content_offset.h,
};
} else {
// if the direction is COLUMN the text and children are one on top of the other
Size content_width = el.children.w.comb_max(el.text.width);
Size width = el.w.combine(content_width);
short final_width = width.greater();
short text_height;
if (el.text.area != 0) {
short text_width = @exact(final_width).combine(el.text.width).min;
text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
}
Size content_height = el.children.h + @exact(text_height);
Size height = el.h.combine(content_height);
short final_height = height.greater();
dim = {
.x = final_width + el.content_offset.x + el.content_offset.w,
.y = final_height + el.content_offset.y + el.content_offset.h,
};
}
// GROSS HACK FOR EXACT DIMENSIONS
if (el.w.@is_exact()) dim.x = el.w.min + el.content_offset.x + el.content_offset.w;
if (el.h.@is_exact()) dim.y = el.h.min + el.content_offset.y + el.content_offset.h;
// GROSS HACK FOR GROW DIMENSIONS
// FIXME: does this always work?
if (el.w.@is_grow()) dim.x = 0;
if (el.h.@is_grow()) dim.y = 0;
return dim;
}
// The content space of the element
macro Point Elem.content_space(&e)
{
return {
.x = (short)max(e.bounds.w - e.layout.content_offset.x - e.layout.content_offset.w, 0),
.y = (short)max(e.bounds.h - e.layout.content_offset.y - e.layout.content_offset.h, 0),
};
}
// Update the parent element's children size
fn void update_parent_size(Elem* child, Elem* parent)
{
Layout* cl = &child.layout;
Layout* pl = &parent.layout;
// if the element has absolute position do not update the parent
if (cl.absolute) return;
Point child_size = cl.get_dimensions();
switch (pl.dir) {
case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds
pl.children.w += @exact(child_size.x);
pl.children.h = pl.children.h.comb_max(@exact(child_size.y));
if (child.layout.w.@is_grow()) parent.layout.grow_children++;
case COLUMN: // do the opposite on column
pl.children.w = pl.children.w.comb_max(@exact(child_size.x));
pl.children.h += @exact(child_size.y);
if (child.layout.h.@is_grow()) parent.layout.grow_children++;
}
}
macro Rect Elem.content_bounds(&elem) => elem.bounds.pad(elem.layout.content_offset);
// Assign the width and height of an element in the directions that it doesn't need to grow
fn void resolve_dimensions(Elem* e, Elem* p)
{
Layout* el = &e.layout;
Layout* pl = &p.layout;
Point elem_dimensions = el.get_dimensions();
e.bounds.w = elem_dimensions.x;
e.bounds.h = elem_dimensions.y;
// if the element has absolute position do not update the parent
if (el.absolute) return;
switch (pl.dir) {
case ROW:
if (!el.w.@is_grow()) pl.occupied += e.bounds.w;
case COLUMN:
if (!el.h.@is_grow()) pl.occupied += e.bounds.h;
}
}
fn void resolve_grow_elements(Elem* e, Elem* p)
{
// WIDTH
if (e.layout.w.@is_grow()) {
if (e.layout.absolute) { // absolute children do not need to share space
e.bounds.w = p.content_space().x;
} else if (p.layout.dir == ROW) { // grow along the axis, divide the parent size
short slot = (short)max(((p.content_space().x - p.layout.occupied) / p.layout.grow_children), 0);
e.bounds.w = slot;
p.layout.grow_children--;
p.layout.occupied += slot;
} else if (p.layout.dir == COLUMN) { // grow across the layout axis, inherit width of the parent
e.bounds.w = p.content_space().x;
}
}
// HEIGHT
if (e.layout.h.@is_grow()) {
if (e.layout.absolute) { // absolute children do not need to share space
e.bounds.h = p.content_space().y;
} else if (p.layout.dir == COLUMN) { // grow along the axis, divide the parent size
short slot = (short)max(((p.content_space().y - p.layout.occupied) / p.layout.grow_children), 0);
e.bounds.h = slot;
p.layout.grow_children--;
p.layout.occupied += slot;
} else if (p.layout.dir == ROW) { // grow across the layout axis, inherit width of the parent
e.bounds.h = p.content_space().y;
}
}
}
fn void resolve_placement(Elem* c, Elem* p)
{
Layout* pl = &p.layout;
Layout* cl = &c.layout;
Point off = {
.x = p.bounds.x + pl.origin.x + pl.content_offset.x,
.y = p.bounds.y + pl.origin.y + pl.content_offset.y,
};
// if the element has absolute position assign the origin and do not update the parent
if (cl.absolute) {
c.bounds.x = p.bounds.x + pl.content_offset.x + cl.origin.x;
c.bounds.y = p.bounds.y + pl.content_offset.y + cl.origin.y;
return;
}
switch (pl.anchor) {
case TOP_LEFT:
c.bounds.x = off.x;
c.bounds.y = off.y;
case LEFT:
c.bounds.x = off.x;
c.bounds.y = off.y + p.content_space().y/2;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied/2;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h/2;
}
case BOTTOM_LEFT:
c.bounds.x = off.x;
c.bounds.y = off.y + p.content_space().y ;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h;
}
case BOTTOM:
c.bounds.x = off.x + p.content_space().x/2;
c.bounds.y = off.y + p.content_space().y;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied;
c.bounds.x -= c.bounds.w/2;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h;
c.bounds.x -= pl.occupied/2;
}
case BOTTOM_RIGHT:
c.bounds.x = off.x + p.content_space().x;
c.bounds.y = off.y + p.content_space().y;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied;
c.bounds.x -= c.bounds.w;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h;
c.bounds.x -= pl.occupied;
}
case RIGHT:
c.bounds.x = off.x + p.content_space().x;
c.bounds.y = off.y + p.content_space().y/2;
if (pl.dir == COLUMN) {
c.bounds.y -= pl.occupied/2;
c.bounds.x -= c.bounds.w;
} else if (pl.dir == ROW) {
c.bounds.y -= c.bounds.h/2;
c.bounds.x -= pl.occupied;
}
case TOP_RIGHT:
c.bounds.x = off.x + p.content_space().x;
c.bounds.y = off.y;
if (pl.dir == COLUMN) {
c.bounds.x -= c.bounds.w;
} else if (pl.dir == ROW) {
c.bounds.x -= pl.occupied;
}
case TOP:
c.bounds.x = off.x + p.content_space().x/2;
c.bounds.y = off.y;
if (pl.dir == COLUMN) {
c.bounds.x -= c.bounds.w/2;
} else if (pl.dir == ROW) {
c.bounds.x -= pl.occupied/2;
}
case CENTER:
c.bounds.x = off.x + p.content_space().x/2;
c.bounds.y = off.y + p.content_space().y/2;
if (pl.dir == COLUMN) {
c.bounds.x -= c.bounds.w/2;
c.bounds.y -= pl.occupied/2;
} else if (pl.dir == ROW) {
c.bounds.x -= pl.occupied/2;
c.bounds.y -= c.bounds.h/2;
}
break;
}
switch (pl.dir) {
case ROW:
pl.origin.x += c.bounds.w;
case COLUMN:
pl.origin.y += c.bounds.h;
default: unreachable("unknown layout direction");
}
// update the parent children_bounds
// FIXME: this causes scollbars to flicker in/out during resize because the current frames children_bounds are updated
// with the previous' frame child bounds. It would be better to implement another pass during layout.
// FIXME: this long way around to compute the children bounds works and reduces flickering, but it is very ugly
Rect ncb = c.children_bounds;
ncb.x = c.bounds.x;
ncb.y = c.bounds.y;
Rect cb = containing_rect(c.bounds, ncb);
Point o = p.layout.scroll_offset;
p.children_bounds = containing_rect(cb + o, p.children_bounds);
// reset the children bounds
c.children_bounds = {
.x = c.bounds.x,
.y = c.bounds.y
};
}
fn void? Ctx.layout_element_tree(&ctx)
{
int current;
for (int n; (current = ctx.tree.level_order_it(0, n)) >= 0; n++) {
Elem* p = ctx.find_elem(ctx.tree.get(current))!;
p.layout.origin = -p.layout.scroll_offset;
int ch;
// RESOLVE KNOWN DIMENSIONS
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
Elem* c = ctx.find_elem(ctx.tree.get(ch))!;
if (ctx.tree.is_root(ch)) {
resolve_dimensions(p, &&{});
} else {
resolve_dimensions(c, p);
}
}
// RESOLVE GROW CHILDREN
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
Elem* c = ctx.find_elem(ctx.tree.get(ch))!;
if (ctx.tree.is_root(ch)) {
resolve_grow_elements(p, &&{});
} else {
resolve_grow_elements(c, p);
}
}
// RESOLVE CHILDREN PLACEMENT
for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) {
Elem* c = ctx.find_elem(ctx.tree.get(ch))!;
if (ctx.tree.is_root(ch)) {
resolve_placement(p, &&{});
} else {
resolve_placement(c, p);
}
// FIXME: this stuff would be better elsewhere but we are already iteraring through all
// elements so here it fits really well
ctx.update_hover_and_focus(c);
}
}
}

View File

@ -1,367 +0,0 @@
module mtree{Type};
/* ================================================================================================
* MTree, Bitmap-based tree
* ================================================================================================
*
* Overview
* --------
* The MTree is a bitmap-based tree structure composed of three core elements:
* - Element Vector: Stores user data.
* - Reference Node Vector: Manages node relationships.
* - Bitmap: Marks used indices.
*
* The name "MTree" originates from "Matrix Tree," where the vector is divided into
* sectors of power-of-two sizes. Each node's bitmap marks the positions of its
* children within the same sector.
*
* If a parent and its children are in different sectors, a new node is created.
* The parent's "next" field points to this new node, forming a chain that must
* be traversed during iteration.
*
*
* Example (sector size = 8)
* -------------------------
*
* _________________________________
* |__ __ _______________________ |
* | | | | _ |
* | v v vv |v
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* refs_vec:| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|13|14|15|16|...
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* \__________ __________/ \__________ __________/ \__
* V V
* sector 0 sector 1
*
*
* Node Relationships:
* -------------------
* - Root (Element 0) has three direct children: 1, 2, and 10.
* - Node 10 is in a different sector than the root, so root.next points to Node 11.
* - Node 11 has Node 10 as a direct child and Node 0 (root) as its parent.
*
* Bitmap Representation:
* ---------------------
*
* root = {
* .parent = -1; // Root has no parent
* .next = 11; // Points to Node 11
* .children = 0b00000110; // [0|1|1|0|0|0|0|0] (Children: 1, 2)
* }
*
* node11 = {
* .parent = 0; // Parent is root (Node 0)
* .next = -1; // Last in the chain
* .children = 0b00000100; // [0|0|1|0|0|0|0|0] (Child: 10)
* }
*
* ================================================================================================
*/
import std::core::mem;
import std::core::mem::allocator;
import std::io;
import std::bits;
import std::collections::list;
alias Bitmap = ulong;
const BITS = Bitmap.sizeof*8;
alias IdxList = List{int};
// next: if positive it contains the index of the next node that contains the children information
struct RefNode {
int next;
int parent;
Bitmap children;
}
struct MTree {
usz elements;
Allocator allocator;
IdxList queue;
Bitmap[] used;
Type[] elem_vec; // element vector
RefNode[] refs_vec; // relationship vector
}
<* @param [&inout] tree *>
fn void MTree.init(&tree, usz size, Allocator allocator = mem)
{
// round size to the nearest multiple of BITS
size = size + size%BITS;
tree.elements = 0;
tree.allocator = allocator;
tree.queue.init(tree.allocator, size);
tree.used = allocator::new_array(tree.allocator, Bitmap, size/BITS);
tree.elem_vec = allocator::new_array(tree.allocator, Type, size);
tree.refs_vec = allocator::new_array(tree.allocator, RefNode, size);
foreach (&r: tree.refs_vec) {
r.next = -1;
}
}
<* @param [&inout] tree *>
fn void MTree.free(&tree)
{
tree.elements = 0;
tree.queue.free();
(void)allocator::free(tree.allocator, tree.used);
(void)allocator::free(tree.allocator, tree.elem_vec);
(void)allocator::free(tree.allocator, tree.refs_vec);
}
<* @param [&inout] tree *>
fn int? MTree.get_free_spot(&tree)
{
foreach (idx, d: tree.used) {
if (d != $typeof(d).max) {
int spot = (int)idx*BITS + BITS-(int)d.clz();
return spot;
}
}
return CAPACITY_EXCEEDED?;
}
<* @require idx >= 0 *>
macro void MTree.set_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
tree.used[q] |= (1l << r);
}
<* @require idx >= 0 *>
macro void MTree.unset_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
tree.used[q] &= ~(1l << r);
}
<* @require idx >= 0 *>
macro bool MTree.is_used(&tree, int idx)
{
int r = idx % BITS;
int q = idx / BITS;
return !!(tree.used[q] & (1l << r));
}
// get the last node in the "next" chain
<* @require tree.is_used(parent) == true *>
fn int MTree.last_node(&tree, int parent)
{
while(tree.refs_vec[parent].next >= 0) {
parent = tree.refs_vec[parent].next;
}
return parent;
}
<*
@require tree.elements == 0 || tree.is_used(parent) == true
@param [&inout] tree
*>
fn int? MTree.add(&tree, int parent, Type t)
{
int idx = tree.get_free_spot()!;
int subtree = idx / BITS;
tree.set_used(idx);
tree.elem_vec[idx] = t;
tree.refs_vec[idx] = (RefNode){
.parent = parent,
.next = -1,
};
tree.elements++;
// root element, has no parent
if (tree.elements == 1) {
tree.refs_vec[idx].parent = -1;
return idx;
}
// if the parent already has a node in the same subtree as the child then update that node's
// children bitmap
bool done;
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
int ps = p/BITS;
if (ps == subtree) {
tree.refs_vec[p].children |= (1l << (idx%BITS));
done = true;
break;
}
}
// on fail we need to create another parent node
if (!done) {
int new_next = tree.get_free_spot()!;
// if the new node does not land in the same subtree as the child we cannot do
// anything since the references are immutable
if (new_next/BITS != subtree) {
return CAPACITY_EXCEEDED?;
}
tree.set_used(new_next);
tree.elements++;
// update the "next" chain
int last_link = tree.last_node(parent);
tree.refs_vec[last_link].next = new_next;
tree.refs_vec[new_next].next = -1;
tree.refs_vec[new_next].children |= (long)(1 << (idx%BITS));
tree.refs_vec[new_next].parent = last_link;
// FIXME: the elem_vec is not updated, do we need to?
}
return idx;
}
// get the index of the n-th children of parent, -1 otherwise
// usage: for (int i, c; (c = tree.children_it(parent, i)) >= 0; i++) { ... }
<* @param [&in] tree *>
fn int MTree.children_it(&tree, int parent, int n)
{
int tot_children;
int child;
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
int cn = (int)tree.refs_vec[p].children.popcount();
tot_children += cn;
// we are in the right subtree
if (tot_children > n) {
child = (p/BITS) * BITS; // start at the parent's subtree index
int j = cn - (tot_children - n); // we need the j-th children of this node
Bitmap u = tree.refs_vec[p].children;
child += j; // add the children number
do {
child += (int)u.ctz(); // increment by the skipped zeroes
u >>= u.ctz() + 1;
j--;
} while (j >= 0);
return child;
}
}
return -1;
}
<* @param [&in] tree *>
fn int MTree.children_num(&tree, int parent)
{
int n;
for (int p = parent; p >= 0; p = tree.refs_vec[p].next) {
n += (int)tree.refs_vec[p].children.popcount();
}
return n;
}
<* @param [&in] tree *>
fn int MTree.subtree_size(&tree, int parent)
{
int x = tree.children_num(parent);
int c;
for (int n; (c = tree.children_it(parent, n)) >= 0; n++) {
x += tree.subtree_size(c);
}
return x;
}
<* @param [&inout] tree *>
fn int MTree.level_order_it(&tree, int parent, int i)
{
if (i == 0) {
tree.queue.clear();
tree.queue.push(parent);
}
if (tree.queue.len() == 0) return -1;
int p = tree.queue.pop_first()!!;
int c;
for (int n; (c = tree.children_it(p, n)) >= 0; n++) {
tree.queue.push(c);
}
return p;
}
<* @param [&inout] tree *>
fn void MTree.prune(&tree, int parent)
{
if (!tree.is_used(parent)) return;
int c;
for (int i = 0; (c = tree.children_it(parent, i)) >= 0; i++) {
tree.prune(c); // prune the subtree
// delete all children including their next chain
for (int p = c; p >= 0;) {
int next = tree.refs_vec[p].next;
tree.unset_used(p);
tree.refs_vec[p] = {.next = -1};
p = next;
}
}
// finally delete the parent
for (int p = parent; p >= 0;) {
int next = tree.refs_vec[p].next;
tree.unset_used(p);
tree.elements--;
tree.refs_vec[p] = {.next = -1};
p = next;
}
}
<*
@require ref >= 0 , ref < tree.elem_vec.len
@param [&inout] tree
*>
fn Type? MTree.get(&tree, int ref) @operator([])
{
if (tree.is_used(ref)) return tree.elem_vec[ref];
return NOT_FOUND?;
}
<* @param [&in] tree *>
fn Type? MTree.parentof(&tree, int ref)
{
if (!tree.is_used(ref)) return NOT_FOUND?;
return tree.refs_vec[ref].parent;
}
<* @param [&inout] tree *>
fn void MTree.nuke(&tree)
{
foreach (idx, &b: tree.used) {
*b = 0;
tree.refs_vec[idx] = {.next = -1};
}
tree.elements = 0;
}
<* @param [&in] t *>
macro bool MTree.is_root(&t, int i) => t.is_used(i) && t.refs_vec[i].parent == -1;
<* @param [&in] tree *>
fn void MTree.print(&tree)
{
foreach (idx, c: tree.elem_vec) {
if (tree.is_used((int)idx)) {
io::printfn("[%d](%s) parent:%d next:%d children:%b",
idx, c, tree.refs_vec[idx].parent, tree.refs_vec[idx].next,
tree.refs_vec[idx].children
);
}
}
}

View File

@ -1,268 +0,0 @@
module ugui;
// ---------------------------------------------------------------------------------- //
// RECTANGLE //
// ---------------------------------------------------------------------------------- //
// Rect and it's methods
struct Rect {
short x, y, w, h;
}
// TODO: find another name
const Rect RECT_MAX = {0, 0, short.max, short.max};
// return true if rect a contains b
macro bool Rect.contains(Rect a, Rect b)
{
return (a.x <= b.x && a.y <= b.y && a.x+a.w >= b.x+b.w && a.y+a.h >= b.y+b.h);
}
// returns the intersection of a and b
macro Rect Rect.intersection(Rect a, Rect b)
{
return {
.x = (short)max(a.x, b.x),
.y = (short)max(a.y, b.y),
.w = (short)min(a.x+a.w, b.x+b.w) - (short)max(a.x, b.x),
.h = (short)min(a.y+a.h, b.y+b.h) - (short)max(a.y, b.y),
};
}
// returns true if the intersection not null
macro bool Rect.collides(Rect a, Rect b)
{
return !(a.x > b.x+b.w || a.x+a.w < b.x || a.y > b.y+b.h || a.y+a.h < b.y);
}
// return a rect that contains both rects, a bounding box of both
macro Rect containing_rect(Rect a, Rect b)
{
short min_x = (short)min(a.x, b.x);
short min_y = (short)min(a.y, b.y);
short max_x = (short)max(a.x + a.w, b.x + b.w);
short max_y = (short)max(a.y + a.h, b.y + b.h);
return {
.x = min_x,
.y = min_y,
.w = (short)(max_x - min_x),
.h = (short)(max_y - min_y)
};
}
// check for empty rect
macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0;
// returns the element-wise addition of r1 and r2
macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
{
return {
.x = r1.x + r2.x,
.y = r1.y + r2.y,
.w = r1.w + r2.w,
.h = r1.h + r2.h,
};
}
// returns the element-wise subtraction of r1 and r2
macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
{
return {
.x = r1.x - r2.x,
.y = r1.y - r2.y,
.w = r1.w - r2.w,
.h = r1.h - r2.h,
};
}
// returns the element-wise multiplication of r1 and r2
macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*)
{
return {
.x = r1.x * r2.x,
.y = r1.y * r2.y,
.w = r1.w * r2.w,
.h = r1.h * r2.h,
};
}
macro Point Rect.position(Rect r)
{
return {
.x = r.x,
.y = r.y,
};
}
macro Point Rect.size(Rect r)
{
return {
.x = r.w,
.y = r.h,
};
}
macro Rect Rect.max(Rect a, Rect b)
{
return {
.x = max(a.x, b.x),
.y = max(a.y, b.y),
.w = max(a.w, b.w),
.h = max(a.h, b.h),
};
}
macro Rect Rect.min(Rect a, Rect b)
{
return {
.x = min(a.x, b.x),
.y = min(a.y, b.y),
.w = min(a.w, b.w),
.h = min(a.h, b.h),
};
}
// Offset a rect by a point
macro Rect Rect.off(Rect r, Point p) @operator_s(+)
{
return {
.x = r.x + p.x,
.y = r.y + p.y,
.w = r.w,
.h = r.h,
};
}
// Resize a rect width and height
macro Rect Rect.grow(Rect r, Point p)
{
return {
.x = r.x,
.y = r.y,
.w = r.w + p.x,
.h = r.h + p.y,
};
}
// Return the bottom-right corner of a rectangle
macro Point Rect.bottom_right(Rect r)
{
return {
.x = r.x + r.w,
.y = r.y + r.h,
};
}
macro Rect Rect.center_to(Rect a, Rect b)
{
return {
.x = b.x + (b.w - a.w)/2,
.y = b.y + (b.h - a.h)/2,
.w = a.w,
.h = a.h,
};
}
macro Rect Rect.pad(Rect a, Rect b)
{
return {
.x = a.x + b.x,
.y = a.y + b.y,
.w = a.w - b.x - b.w,
.h = a.h - b.y - b.h,
};
}
macro Rect Rect.expand(Rect a, Rect b)
{
return {
.x = a.x - b.x,
.y = a.y - b.y,
.w = a.w + b.x + b.w,
.h = a.h + b.y + b.h,
};
}
// ---------------------------------------------------------------------------------- //
// POINT //
// ---------------------------------------------------------------------------------- //
struct Point {
short x, y;
}
// returns true if a point is inside the rectangle
macro bool Point.in_rect(Point p, Rect r)
{
return (p.x >= r.x && p.x <= r.x + r.w) && (p.y >= r.y && p.y <= r.y + r.h);
}
macro bool Point.outside(Point p, Rect r) => !p.in_rect(r);
macro Point Point.add(Point a, Point b) @operator_s(+) => {.x = a.x+b.x, .y = a.y+b.y};
macro Point Point.sub(Point a, Point b) @operator_s(-) => {.x = a.x-b.x, .y = a.y-b.y};
macro Point Point.neg(Point p) @operator_s(-) => {-p.x, -p.y};
macro Point Point.max(Point a, Point b) => {.x = max(a.x, b.x), .y = max(a.y, b.y)};
macro Point Point.min(Point a, Point b) => {.x = min(a.x, b.x), .y = min(a.y, b.y)};
macro bool Point.equals(Point a, Point b) @operator_s(==) => a.x == b.x && a.y == b.y;
// ---------------------------------------------------------------------------------- //
// COLOR //
// ---------------------------------------------------------------------------------- //
struct Color{
char r, g, b, a;
}
macro Color uint.to_rgba(u)
{
return {
.r = (char)((u >> 24) & 0xff),
.g = (char)((u >> 16) & 0xff),
.b = (char)((u >> 8) & 0xff),
.a = (char)((u >> 0) & 0xff)
};
}
macro Color uint.@to_rgba($u)
{
return {
.r = (char)(($u >> 24) & 0xff),
.g = (char)(($u >> 16) & 0xff),
.b = (char)(($u >> 8) & 0xff),
.a = (char)(($u >> 0) & 0xff)
};
}
macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
// ---------------------------------------------------------------------------------- //
// SIZE //
// ---------------------------------------------------------------------------------- //
macro short short.add_no_of(short a, short b) => (short)max(min((int)a + (int)b, short.max), short.min) @inline;
struct Size {
short min, max;
}
macro Size @grow() => {.min = 0, .max = 0};
macro Size @exact(short s) => {.min = s, .max = s};
macro Size @fit(short min = 0, short max = short.max) => {.min = min, .max = max};
macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0);
macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0);
macro bool Size.@is_fit(s) => (s.min != s.max);
macro Size Size.add(a, Size b) @operator_s(+) => {.min = a.min.add_no_of(b.min), .max = a.max.add_no_of(b.max)};
macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min.add_no_of(-b.min), .max = a.max.add_no_of(-b.max)};
macro Size Size.combine(a, Size b) => {.min = max(a.min, b.min), .max = min(a.max, b.max)};
macro Size Size.comb_max(a, Size b) => {.min = max(a.min, b.min), .max = max(a.max, b.max)};
macro Size Size.comb_min(a, Size b) => {.min = min(a.min, b.min), .max = min(a.max, b.max)};
macro short Size.greater(a) => a.min > a.max ? a.min : a.max;

View File

@ -1,107 +0,0 @@
module ugui;
import std::core::mem::allocator;
import std::collections::map;
import std::io;
import std::compression::qoi;
const usz SRITES_PER_ATLAS = 64;
enum SpriteType {
SPRITE_NORMAL,
SPRITE_SDF,
SPRITE_MSDF,
SPRITE_ANIMATED,
}
struct Sprite {
Id id;
SpriteType type;
ushort u, v;
ushort w, h;
}
alias SpriteMap = map::HashMap{Id, Sprite};
struct SpriteAtlas {
Id id;
Atlas atlas;
SpriteMap sprites;
bool should_update;
}
// name: some examples are "icons" or "images"
fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, ushort height)
{
// FIXME: for now only R8G8B8A8 format is supported
if (type != ATLAS_R8G8B8A8) {
return INVALID_TYPE?;
}
this.id = name.hash();
this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!;
this.sprites.init(allocator::mem, capacity: SRITES_PER_ATLAS);
this.should_update = false;
}
fn void? SpriteAtlas.free(&this)
{
this.atlas.free();
this.sprites.free();
}
// FIXME: this should throw an error when a different pixel format than the atlas' is used
// or convert from the source's pixel format to the atlas'
fn Sprite*? SpriteAtlas.insert(&this, String name, SpriteType type, char[] pixels, ushort w, ushort h, ushort stride)
{
Sprite s;
s.id = name.hash();
s.type = type;
Point uv = this.atlas.place(pixels, w, h, stride)!;
s.w = w;
s.h = h;
s.u = uv.x;
s.v = uv.y;
this.sprites.set(s.id, s);
this.should_update = true;
return this.sprites.get_ref(s.id);
}
fn Sprite*? SpriteAtlas.get(&this, String name)
{
Id id = name.hash();
return this.sprites.get_ref(id);
}
fn Sprite*? SpriteAtlas.get_by_id(&this, Id id)
{
return this.sprites.get_ref(id);
}
macro Rect Sprite.rect(s) => {0,0,s.w,s.h};
macro Rect Sprite.uv(s) => {s.u,s.v,s.w,s.h};
fn void? Ctx.sprite_atlas_create(&ctx, String name, AtlasType type, ushort w, ushort h)
{
ctx.sprite_atlas.init(name, type, w, h)!;
}
fn Id Ctx.get_sprite_atlas_id(&ctx, String name)
{
return name.hash();
}
fn void? Ctx.import_sprite_memory(&ctx, String name, char[] pixels, ushort w, ushort h, ushort stride, SpriteType type = SPRITE_NORMAL)
{
ctx.sprite_atlas.insert(name, type, pixels, w, h, stride)!;
}
fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType type = SPRITE_NORMAL)
{
QOIDesc desc;
char[] pixels = qoi::read(allocator::mem, path, &desc, QOIChannels.RGBA)!;
defer mem::free(pixels);
ctx.sprite_atlas.insert(name, type, pixels, (ushort)desc.width, (ushort)desc.height, (ushort)desc.width)!;
}

View File

@ -1,588 +0,0 @@
module ugui;
import std::collections::list;
import std::core::string;
struct LineInfo @local {
usz start, end;
short width, height;
short first_off; // first character offset
}
macro usz LineInfo.len(li) => li.end-li.start;
alias LineStack @local = list::List{LineInfo};
fn short Rect.y_off(Rect bounds, short height, Anchor anchor) @local
{
short off;
switch (anchor) {
case TOP_LEFT: nextcase;
case TOP: nextcase;
case TOP_RIGHT:
off = 0;
case LEFT: nextcase;
case CENTER: nextcase;
case RIGHT:
off = (short)(bounds.h - height)/2;
case BOTTOM_LEFT: nextcase;
case BOTTOM: nextcase;
case BOTTOM_RIGHT:
off = (short)(bounds.h - height);
}
return off;
}
fn short Rect.x_off(Rect bounds, short width, Anchor anchor) @local
{
short off;
switch (anchor) {
case TOP_LEFT: nextcase;
case LEFT: nextcase;
case BOTTOM_LEFT:
off = 0;
case TOP: nextcase;
case CENTER: nextcase;
case BOTTOM:
off = (short)(bounds.w - width)/2;
case TOP_RIGHT: nextcase;
case RIGHT: nextcase;
case BOTTOM_RIGHT:
off = (short)(bounds.w - width);
}
return off;
}
// ---------------------------------------------------------------------------------- //
// STRING LAYOUT //
// ---------------------------------------------------------------------------------- //
struct GlyphIterator {
short baseline;
short line_height;
short line_gap;
short space_width;
short tab_width;
Rect bounds;
Anchor anchor;
bool reflow;
Font* font;
LineStack lines;
usz line_off, line_idx;
String text;
Codepoint cp;
Glyph* gp;
short adv; // prev advance
Point o;
Rect str_bounds;
}
<*
@param [&inout] self
@param [in] text
@param [&inout] font
*>
fn void? GlyphIterator.init(&self, Allocator allocator, String text, Rect bounds, Font* font, Anchor anchor, bool reflow, uint tab_size)
{
self.font = font;
self.line_height = (short)font.line_height();
self.baseline = (short)font.ascender;
self.line_gap = (short)font.linegap;
self.space_width = font.get_glyph(' ').adv!;
self.tab_width = self.space_width * (short)tab_size;
self.bounds = bounds;
self.o = bounds.position();
self.reflow = reflow;
self.text = text;
self.anchor = anchor;
// if the anchor is top_left we can skip dividing the string line by line, in GlyphIterator.next
// this has to be accounted for
if (anchor != TOP_LEFT) {
self.lines.init(allocator, 4);
self.populate_lines_stack()!;
self.line_off = 0;
self.line_idx = 0;
if (self.lines.len() > 0) {
self.o.y += bounds.y_off(self.str_bounds.h, anchor);
self.o.x += bounds.x_off(self.lines[0].width, anchor) - self.lines[0].first_off;
}
}
}
fn void? GlyphIterator.populate_lines_stack(&self)
{
usz line_start;
LineInfo li;
Point o = self.o;
StringIterator ti = self.text.iterator();
usz prev_off;
for (Codepoint cp; ti.has_next();) {
cp = ti.next()!;
usz off = ti.current;
bool push = false;
li.height = self.line_height;
switch {
case cp == '\n':
push = true;
case cp == '\t':
o.x += self.tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Glyph* gp = self.font.get_glyph(cp)!;
if (off == line_start) {
li.first_off = gp.ox;
o.x -= gp.ox;
}
Rect b = gp.bounds().off(o);
b.y += self.baseline;
if (self.reflow && b.x + b.w > self.bounds.x + self.bounds.w) {
push = true;
// roll back this character since it is on the next line
ti.current = prev_off;
off = prev_off;
} else {
o.x += gp.adv;
li.width += gp.adv;
}
}
if (push) {
li.start = line_start;
li.end = off;
self.lines.push(li);
self.str_bounds.w = max(self.str_bounds.w, li.width);
self.str_bounds.h += li.height;
o.x = self.bounds.x;
o.y += self.line_height;
line_start = off;
li.height = 0;
li.width = 0;
}
prev_off = off;
}
if (line_start != ti.current) {
// FIXME: crap, can we not repeat this code?
li.start = line_start;
li.end = ti.current;
self.lines.push(li);
self.str_bounds.w = max(self.str_bounds.w, li.width);
self.str_bounds.h += li.height;
}
self.str_bounds.h += (short)(self.lines.len()-1) * self.line_gap;
}
fn String GlyphIterator.current_line(&self)
{
LineInfo li = self.lines[self.line_idx];
return self.text[li.start:li.len()];
}
fn Rect? GlyphIterator.next(&self)
{
// check if there is a next glyph and maybe update the line and offset indices
if (self.anchor != TOP_LEFT) {
if (self.line_idx >= self.lines.len()) {
return NO_MORE_ELEMENT?;
}
LineInfo li = self.lines[self.line_idx];
if (self.line_off >= li.len()) {
self.line_idx++;
if (self.line_idx >= self.lines.len()) {
return NO_MORE_ELEMENT?;
}
self.line_off = 0;
li = self.lines[self.line_idx];
self.o.y += self.line_height + self.line_gap;
self.o.x = self.bounds.x + self.bounds.x_off(li.width, self.anchor) - li.first_off;
}
} else if (self.line_off >= self.text.len) {
return NO_MORE_ELEMENT?;
}
String t;
if (self.anchor != TOP_LEFT) {
t = self.current_line()[self.line_off..];
} else {
t = self.text[self.line_off..];
}
usz read = t.len < 4 ? t.len : 4;
self.cp = conv::utf8_to_char32(&t[0], &read)!;
self.line_off += read;
self.gp = self.font.get_glyph(self.cp)!;
Rect b = {.x = self.o.x, .y = self.o.y};
Point prev_o = self.o;
self.adv = 0;
switch {
case self.cp == '\n':
if (self.anchor == TOP_LEFT) {
self.o.x = self.bounds.x;
self.o.y += self.line_height + self.line_gap;
self.line_idx++;
}
break;
case self.cp == '\t':
self.o.x += self.tab_width;
case ascii::is_cntrl((char)self.cp):
break;
default:
b = self.gp.bounds().off(self.o);
b.y += self.baseline;
if (self.anchor == TOP_LEFT) {
//if (self.o.x == self.bounds.x) self.bounds.x -= self.gp.ox;
if (self.reflow && b.bottom_right().x > self.bounds.bottom_right().x) {
self.o.x = self.bounds.x - self.gp.ox;
self.o.y += self.line_height + self.line_gap;
self.line_idx++;
b = self.gp.bounds().off(self.o);
b.y += self.baseline;
}
}
self.o.x += self.gp.adv;
self.adv = self.o.x - prev_o.x;
}
return b;
}
fn bool GlyphIterator.has_next(&self)
{
if (self.anchor == TOP_LEFT) {
return self.line_off < self.text.len;
}
if (self.line_idx >= self.lines.len()) {
return false;
}
LineInfo li = self.lines[self.line_idx];
if (self.line_idx == self.lines.len() - 1 && self.line_off >= li.len()) {
return false;
}
return true;
}
fn usz GlyphIterator.current_offset(&self)
{
if (self.anchor == TOP_LEFT) return self.line_off;
return self.lines[self.line_idx].start + self.line_off;
}
// layout a string inside a bounding box, following the given alignment (anchor).
<*
@param [&in] ctx
@param [in] text
*>
fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false)
{
if (text == "") return;
if (bounds.w <= 0 || bounds.h <= 0) return;
ctx.push_scissor(bounds, z_index)!;
Font* font = &ctx.font;
Id texture_id = font.id;
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
while (gi.has_next()) {
Rect b = gi.next()!;
Rect uv = gi.gp.uv();
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
}
ctx.reset_scissor(z_index)!;
// ctx.dbg_rect(str_bounds.off(bounds.position()));
}
// ---------------------------------------------------------------------------------- //
// CURSOR AND MOUSE //
// ---------------------------------------------------------------------------------- //
fn Rect? Ctx.get_cursor_position(&ctx, String text, Rect bounds, Anchor anchor, usz cursor, bool reflow = false)
{
if (bounds.w <= 0 || bounds.h <= 0) return {};
Font* font = &ctx.font;
Id texture_id = font.id;
if (text == "") text = "\f";
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
Rect cursor_rect;
cursor_rect.x = gi.o.x;
cursor_rect.y = gi.o.y;
cursor_rect.h = (short)font.line_height();
if (cursor == 0) return cursor_rect;
while (gi.has_next()) {
Rect b = gi.next()!;
if (gi.current_offset() == cursor) {
if (gi.cp == '\n') {
if (!gi.has_next()) {
cursor_rect.x = bounds.x + bounds.x_off(0, anchor);
cursor_rect.y = b.y + gi.line_height + gi.line_gap;
} else {
gi.next()!;
cursor_rect.x = gi.o.x - gi.gp.adv;
cursor_rect.y = gi.o.y;
}
} else {
// Use the updated origin position instead of glyph bounds
cursor_rect.x = gi.o.x;
cursor_rect.y = gi.o.y;
}
return cursor_rect;
}
}
return {};
}
<*
@param [&in] ctx
@param [in] text
*>
fn usz? Ctx.hit_test_string(&ctx, String text, Rect bounds, Anchor anchor, Point p, bool reflow = false)
{
if (text == "") return 0;
if (bounds.w <= 0 || bounds.h <= 0) return 0;
Font* font = &ctx.font;
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
usz prev_offset = 0;
Point prev_o = gi.o;
while (gi.has_next()) {
Point o_before = gi.o;
usz offset_before = gi.current_offset();
Rect b = gi.next()!;
switch {
case gi.cp == '\n':
// Check if point is on this line before the newline
Rect line_rect = {
.x = prev_o.x,
.y = prev_o.y,
.w = (short)(o_before.x - prev_o.x),
.h = gi.line_height
};
if (p.in_rect(line_rect)) return offset_before;
prev_o = gi.o;
break;
case gi.cp == '\t':
// Check if point is in the tab space
Rect tab_rect = {
.x = o_before.x,
.y = o_before.y,
.w = gi.tab_width,
.h = gi.line_height
};
if (p.in_rect(tab_rect)) return offset_before;
break;
case ascii::is_cntrl((char)gi.cp):
break;
default:
// Create a hit test rect for this character
Rect hit_rect = {
.x = o_before.x,
.y = o_before.y,
.w = gi.gp.adv,
.h = gi.line_height
};
if (p.in_rect(hit_rect)) {
// Check if cursor should be before or after this character
// by checking which half of the character was clicked
short mid_x = o_before.x + gi.gp.adv / 2;
if (p.x < mid_x) {
return offset_before;
} else {
return gi.current_offset();
}
}
}
prev_offset = gi.current_offset();
}
// Point is after all text
return text.len;
}
// TODO: implement a function `layout_string_with_selection` to avoid iterating over the string twice
fn void? Ctx.draw_string_selection(&ctx, String text, Rect bounds, Anchor anchor, usz start, usz end, int z_index, Color hue, bool reflow = false)
{
if (text == "") return;
if (bounds.w <= 0 || bounds.h <= 0) return;
if (start > end) @swap(start, end);
// Ensure start < end
if (start > end) {
usz temp = start;
start = end;
end = temp;
}
ctx.push_scissor(bounds, z_index)!;
Font* font = &ctx.font;
GlyphIterator gi;
gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!;
Rect sel_rect = { .h = gi.line_height }; // selection rect
isz sel_line = -1; // selection line
while (gi.has_next()) {
Rect b = gi.next()!;
usz off = gi.current_offset()-1;
isz line = gi.line_idx;
bool in_selection = start <= off && off <= end;
if (in_selection && line != sel_line) {
if (sel_line != -1) {
ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
}
sel_rect = {.x = gi.o.x - gi.adv, .y = gi.o.y, .w = 0, .h = gi.line_height};
sel_line = line;
}
if (in_selection) {
sel_rect.w = gi.o.x - sel_rect.x;
if (gi.cp == '\n') sel_rect.w += gi.space_width;
}
if (off > end) break;
}
ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!;
ctx.reset_scissor(z_index)!;
}
// ---------------------------------------------------------------------------------- //
// TEXT MEASUREMENT //
// ---------------------------------------------------------------------------------- //
const uint TAB_SIZE = 4;
struct TextSize {
Size width, height;
int area;
}
// Measeure the size of a string.
// width.min: as if each word is broken up by a new line
// width.max: the width of the string left as-is
// height.min: the height of the string left as-is
// height.max: the height of the string with each word broken up by a new line
<*
@param [&in] ctx
@param [in] text
*>
fn TextSize? Ctx.measure_string(&ctx, String text)
{
if (text == "") return (TextSize){};
Font* font = &ctx.font;
short baseline = (short)font.ascender;
short line_height = (short)font.line_height();
short line_gap = (short)font.linegap;
short space_width = font.get_glyph(' ').adv!;
short tab_width = space_width * TAB_SIZE;
isz off;
usz x;
TextSize ts;
short word_width;
short words = 1;
Rect bounds; // unaltered text bounds;
Point origin;
StringIterator it = text.iterator();
for (Codepoint cp; it.has_next();) {
cp = it.next()!;
Glyph* gp = font.get_glyph(cp)!;
// update the text bounds
switch {
case cp == '\n':
origin.x = 0;
origin.y += line_height + line_gap;
case cp == '\t':
origin.x += tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Rect b = gp.bounds().off(origin);
b.y += baseline;
bounds = containing_rect(bounds, b);
origin.x += gp.adv;
}
// update the word width
switch {
case ascii::is_space((char)cp):
if (word_width > ts.width.min) ts.width.min = word_width;
word_width = 0;
words++;
default:
//word_width += gp.w + gp.ox;
if (off < text.len) {
word_width += gp.adv;
} else {
word_width += gp.w + gp.ox;
}
}
}
// end of string is also end of word
if (word_width > ts.width.min) ts.width.min = word_width;
ts.width.max = bounds.w;
ts.height.min = bounds.h;
ts.height.max = words * line_height + line_gap * (words-1);
ts.area = bounds.w * bounds.h;
return ts;
}

View File

@ -1,461 +0,0 @@
module ugui;
import std::collections::map;
import std::core::mem::allocator;
import std::io;
// global style, similar to the css box model
struct Style { // css box model
Rect padding;
Rect border;
Rect margin;
Color bg; // background color
Color fg; // foreground color
Color primary; // primary color
Color secondary; // secondary color
Color accent; // accent color
ushort radius;
short size;
}
const Style DEFAULT_STYLE = {
.margin = {2, 2, 2, 2},
.border = {2, 2, 2, 2},
.padding = {1, 1, 1, 1},
.radius = 0,
.size = 16,
.bg = 0x282828ffu.@to_rgba(),
.fg = 0xfbf1c7ffu.@to_rgba(),
.primary = 0xcc241dffu.@to_rgba(),
.secondary = 0x458588ffu.@to_rgba(),
.accent = 0xfabd2fffu.@to_rgba(),
};
// style is stored in a hashmap, each style has an Id that can be generated by a string or whatever
alias StyleMap = map::HashMap{Id, Style};
// push or update a new style into the map
fn void StyleMap.register_style(&map, Style* style, Id id)
{
if (style == null) return;
map.set(id, *style);
}
// get a style from the map, if the style is not found then use a default style.
fn Style* StyleMap.get_style(&map, Id id)
{
Style*? s = map.get_ref(id);
if (catch s) {
// io::eprintfn("WARNING: style %x not found, using default style", id);
return &DEFAULT_STYLE;
}
return s;
}
fn int StyleMap.import_style_string(&map, String text)
{
Parser p;
p.lex.text = text;
int added;
while (p.parse_style() == true) {
added++;
// set the default style correctly
map.register_style(&p.style, p.style_id);
if (p.lex.peep_token().type == EOF) break;
}
return added;
}
fn int Ctx.import_style_from_string(&ctx, String text) => ctx.styles.import_style_string(text);
fn int Ctx.import_style_from_file(&ctx, String path)
{
char[] text;
usz size = file::get_size(path)!!;
text = mem::new_array(char, size);
file::load_buffer(path, text)!!;
defer mem::free(text);
int added = ctx.import_style_from_string((String)text);
return added;
}
/*
* Style can be serialized and deserialized with a subset of CSS
* <style name> {
* padding: left right top bottom;
* border: left right top bottom;
* margin: left right top bottom;
* radius: uint;
* size: uint;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* Color: #RRGGBBAA;
* }
* The field "style name" will be hashed and the hash used as the id int the style map.
* Fields may be excluded, each excluded field is set to zero.
* The default unit is pixels, but millimeters is also available. The parser function accepts a scale
* factor that has to be obtained with the window manager functions.
*/
// TODO: implement <style name> : <another style> to easily inherit all properties
// of a previously defined style
module ugui::css;
import std::ascii;
import std::io;
// CSS parser module
enum TokenType {
INVALID,
IDENTIFIER,
RCURLY,
LCURLY,
SEMICOLON,
COLON,
NUMBER,
COLOR,
EOF,
}
enum Unit {
PIXELS,
MILLIMETERS
}
struct Token {
TokenType type;
usz line, col, off;
String text;
union {
struct {
float value;
Unit unit;
}
Color color;
}
}
fn short Token.to_px(&t, float mm_to_px)
{
if (t.type != NUMBER) {
unreachable("WFT you cannot convert to pixels a non-number");
}
if (t.unit == PIXELS) return (short)(t.value);
return (short)(t.value * mm_to_px);
}
// ---------------------------------------------------------------------------------- //
// LEXER //
// ---------------------------------------------------------------------------------- //
struct Lexer {
String text;
usz line, col, off;
}
macro char Lexer.peep(&lex) => lex.text[lex.off];
fn char Lexer.advance(&lex)
{
if (lex.off >= lex.text.len) return '\0';
char c = lex.text[lex.off];
if (c == '\n') {
lex.col = 0;
lex.line++;
} else {
lex.col++;
}
lex.off++;
return c;
}
fn Token Lexer.next_token(&lex)
{
Token t;
t.type = INVALID;
t.off = lex.off;
t.col = lex.col;
t.line = lex.line;
if (lex.off >= lex.text.len) {
return {.type = EOF};
}
// skip whitespace
while (ascii::is_space_m(lex.peep())) {
if (lex.advance() == 0) return {.type = EOF};
if (lex.off >= lex.text.len) return {.type = EOF};
}
t.off = lex.off;
switch (true) {
case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
t.text = lex.text[lex.off:1];
if (lex.advance() == 0) { t.type = INVALID; break; }
switch (t.text[0]) {
case ':': t.type = COLON;
case ';': t.type = SEMICOLON;
case '{': t.type = LCURLY;
case '}': t.type = RCURLY;
default: t.type = INVALID;
}
case lex.peep() == '#': // color
t.type = COLOR;
if (lex.advance() == 0) { t.type = INVALID; break; }
usz hex_start = t.off+1;
while (ascii::is_alnum_m(lex.peep())) {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
usz h_len = lex.off - hex_start;
if (h_len != 8 && h_len != 6) {
io::eprintfn("CSS lexing error at %d:%d: the only suppported color formats are #RRGGBBAA or #RRGGBB", t.line, t.col);
t.type = INVALID;
break;
}
char[10] hex_str = (char[])"0x";
hex_str[2:h_len] = lex.text[hex_start:h_len];
if (h_len == 6) hex_str[8..9] = "ff"[..];
uint? color_hex = ((String)hex_str[..]).to_uint();
if (catch color_hex) {
t.type = INVALID;
break;
}
t.color = color_hex.to_rgba();
case ascii::is_alpha_m(lex.peep()): // identifier
t.type = IDENTIFIER;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '-' || lex.peep() == '_') {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
t.text = lex.text[t.off..lex.off-1];
case ascii::is_digit_m(lex.peep()): // number
t.type = NUMBER;
t.unit = PIXELS;
// find the end of the number
usz end;
while (ascii::is_alnum_m(lex.peep()) || lex.peep() == '+' || lex.peep() == '-' || lex.peep() == '.') {
if (lex.advance() == 0) { t.type = INVALID; break; }
}
end = lex.off;
if (end - t.off > 2) {
if (lex.text[end-2:2] == "px") {
t.unit = PIXELS;
end -= 2;
} else if (lex.text[end-2:2] == "mm") {
t.unit = MILLIMETERS;
end -= 2;
} else if (lex.text[end-2:2] == "pt" || lex.text[end-2:2] == "em") {
io::eprintn("units 'em' or 'pt' are not supported at the moment");
t.type = INVALID;
break;
}
}
String number_str = lex.text[t.off..end-1];
float? value = number_str.to_float();
if (catch value) { t.type = INVALID; break; }
t.value = value;
t.text = lex.text[t.off..lex.off-1];
}
if (t.type == INVALID) {
io::eprintfn("CSS Lexing ERROR at %d:%d: '%s' is not a valid token", t.line, t.col, lex.text[t.off..lex.off]);
}
return t;
}
fn Token Lexer.peep_token(&lex)
{
Lexer start_state = *lex;
Token t;
t = lex.next_token();
*lex = start_state;
return t;
}
// ---------------------------------------------------------------------------------- //
// PARSER //
// ---------------------------------------------------------------------------------- //
struct Parser {
Lexer lex;
Style style;
Id style_id;
float mm_to_px;
}
macro bool Parser.expect(&p, Token* t, TokenType type)
{
*t = p.lex.next_token();
if (t.type == type) return true;
io::eprintfn("CSS parsing error at %d:%d: expected %s but got %s", t.line, t.col, type, t.type);
return false;
}
// style := ( IDENTIFIER "{" property_list "}" )
// property_list := property property_list
fn bool Parser.parse_style(&p)
{
Token t;
p.style = {};
p.style_id = 0;
// style name
if (p.expect(&t, IDENTIFIER) == false) return false;
p.style_id = t.text.hash();
// style body
if (p.expect(&t, LCURLY) == false) return false;
while (true) {
if (p.parse_property() == false) return false;
t = p.lex.peep_token();
if (t.type != IDENTIFIER) break;
}
if (p.expect(&t, RCURLY) == false) return false;
return true;
}
// property := ( number_property | color_property | size_property ) ";"
// number_property := ( "radius" | "size" ) "=" number
// color_property := ( "bg" | "fg" | "primary" | "secondary" | "accent" ) "=" color
// size_property := ( "padding" | "border" | "margin" ) "=" size
fn bool Parser.parse_property(&p)
{
Token t, prop;
if (p.expect(&prop, IDENTIFIER) == false) return false;
if (p.expect(&t, COLON) == false) return false;
switch (prop.text) {
case "padding":
Rect padding;
if (p.parse_size(&padding) == false) return false;
p.style.padding = padding;
case "border":
Rect border;
if (p.parse_size(&border) == false) return false;
p.style.border = border;
case "margin":
Rect margin;
if (p.parse_size(&margin) == false) return false;
p.style.margin = margin;
case "bg":
Color bg;
if (p.parse_color(&bg) == false) return false;
p.style.bg = bg;
case "fg":
Color fg;
if (p.parse_color(&fg) == false) return false;
p.style.fg = fg;
case "primary":
Color primary;
if (p.parse_color(&primary) == false) return false;
p.style.primary = primary;
case "secondary":
Color secondary;
if (p.parse_color(&secondary) == false) return false;
p.style.secondary = secondary;
case "accent":
Color accent;
if (p.parse_color(&accent) == false) return false;
p.style.accent = accent;
case "radius":
short r;
if (p.parse_number(&r) == false) return false;
if (r < 0) {
io::eprintfn("CSS parsing error at %d:%d: 'radius' must be a positive number, got %d", t.line, t.col, r);
return false;
}
p.style.radius = (ushort)r;
case "size":
short s;
if (p.parse_number(&s) == false) return false;
if (s < 0) {
io::eprintfn("CSS parsing error at %d:%d: 'size' must be a positive number, got %d", t.line, t.col, s);
return false;
}
p.style.size = (ushort)s;
default:
io::eprintfn("CSS parsing error at %d:%d: '%s' is not a valid property", prop.line, prop.col, prop.text);
return false;
}
if (p.expect(&t, SEMICOLON) == false) return false;
return true;
}
// number := NUMBER
fn bool Parser.parse_number(&p, short* n)
{
Token t;
if (p.expect(&t, NUMBER) == false) return false;
*n = t.to_px(p.mm_to_px);
return true;
}
// color := COLOR
// FIXME: since '#' is punctuation this cannot be done in parsing but it has to be done in lexing
fn bool Parser.parse_color(&p, Color* c)
{
Token t;
if (p.expect(&t, COLOR) == false) return false;
*c = t.color;
return true;
}
// size := number | number_list
// number_list := number number number number
fn bool Parser.parse_size(&p, Rect* r)
{
short x;
Token t;
if (p.parse_number(&x) == false) return false;
t = p.lex.peep_token();
if (t.type == NUMBER) {
// we got another number so we expect three more
r.x = x;
if (p.parse_number(&x) == false) return false;
r.y = x;
if (p.parse_number(&x) == false) return false;
r.w = x;
if (p.parse_number(&x) == false) return false;
r.h = x;
return true;
} else if (t.type == SEMICOLON) {
// just one number, all dimensions are the same
r.x = r.y = r.w = r.h = x;
return true;
}
return false;
}

View File

@ -1,288 +0,0 @@
module ugui;
import ugui::textedit::te;
struct TextEdit {
char[] buffer;
usz chars;
usz cursor;
isz sel_len;
}
fn String TextEdit.to_string(&te) => (String)te.buffer[:te.chars];
fn String TextEdit.until_cursor(&te) => (String)te.buffer[..te.cursor];
fn String TextEdit.from_cursor(&te) => (String)te.buffer[te.cursor..te.chars];
fn String TextEdit.until(&te, usz off) => (String)te.buffer[..min(te.chars, off)];
fn String TextEdit.from(&te, usz off) => (String)te.buffer[off..te.chars];
// implement text editing operations on the buffer
// returns true if the buffer is full
fn bool Ctx.text_edit(&ctx, TextEdit* te)
{
String in = ctx.get_keys();
ModKeys mod = ctx.get_mod();
te.insert_utf8(in);
bool select = !!(mod & KMOD_SHIFT);
bool ctrl = !!(mod & KMOD_CTRL);
// handle backspace and delete
if (mod.bkspc) {
if (ctrl) {
te.remove_word(false);
} else {
te.remove_character(false);
}
}
if (mod.del) {
if (ctrl) {
te.remove_word(true);
} else {
te.remove_character(true);
}
}
// handle arrow keys
if (mod.left) {
if (ctrl) {
te.move_cursor_word(false, select);
} else {
te.move_cursor(false, select);
}
}
if (mod.right) {
if (ctrl) {
te.move_cursor_word(true, select);
} else {
te.move_cursor(true, select);
}
}
if (mod.home) {
te.set_cursor(te.search_lines(false).first, select);
}
if (mod.end) {
te.set_cursor(te.search_lines(true).first, select);
}
// TODO: up, down
return te.chars < te.buffer.len;
}
module ugui::textedit::te;
import std::core::string;
import std::ascii;
import std::collections::pair;
alias OffPair = pair::Pair{usz, usz};
// returns the offset of the next codepoint in the buffer from the cursor
fn usz TextEdit.next_char_off(&te)
{
usz len = min(te.chars - te.cursor, 4);
if (len == 0) return len;
conv::utf8_to_char32(&te.buffer[te.cursor], &len)!!;
return len;
}
// returns the offset of the previous codepoint in the buffer from the cursor
fn usz TextEdit.prev_char_off(&te)
{
if (te.cursor == 0) return 0;
String b = (String)te.buffer[..te.cursor];
usz len;
foreach_r (off, c: b) {
if (c & 0xC0 == 0x80) continue;
len = b.len - off;
break;
}
// verify the utf8 character
conv::utf8_to_char32(&b[te.cursor - len], &len)!!;
return len;
}
// moves the cursor forwards or backwards by one codepoint without exiting the bounds, if select is
// true also change the selection width
fn void TextEdit.move_cursor(&te, bool forward, bool select)
{
// in selection but trying to move without selecting, snap the cursor and reset selection
if (te.sel_len != 0 && !select) {
if (te.sel_len > 0 && forward) {
// selection is in front of the cursor and trying to move right, snap the cursor to the
// end of selection
te.cursor += te.sel_len;
} else if (te.sel_len < 0 && !forward) {
// selection is behind the cursor and trying to move left, snap the cursor to the start
// of selection
te.cursor += te.sel_len;
}
te.sel_len = 0;
} else {
isz off = forward ? te.next_char_off() : -te.prev_char_off();
if (te.cursor + off < 0 || te.cursor + off > te.chars) return;
te.cursor += off;
// if trying to select increment selection width
if (select) {
te.sel_len -= off;
}
}
}
// set the cursor to the exact offset provided, the selection flags controls wether the selection has
// expanded or rese
fn void TextEdit.set_cursor(&te, usz cur, bool select)
{
if (!select) te.sel_len = 0;
if (cur == te.cursor) return;
usz prev_cur = te.cursor;
te.cursor = cur;
if (select) {
te.sel_len += prev_cur - cur;
}
}
fn void TextEdit.move_cursor_word(&te, bool forward, bool select)
{
// moving out of selection, snap to the end
if (!select && te.sel_len != 0) {
te.move_cursor(forward, select);
return;
}
usz prev_cur = te.cursor;
while (te.cursor <= te.chars) {
char c;
if (forward) {
if (te.cursor == te.chars) break;
c = te.buffer[te.cursor];
} else {
if (te.cursor == 0) break;
c = te.buffer[te.cursor-1];
}
if (ascii::is_space(c) || ascii::is_punct(c)) break;
te.move_cursor(forward, select);
}
// move at least one character
if (prev_cur == te.cursor) te.move_cursor(forward, select);
}
// get the offset of the current line start and the previous line start from the cursor
fn OffPair TextEdit.search_lines(&te, bool forward)
{
// look for the current line start
OffPair p;
if (forward) {
// if searching forwards look for the end of the current line and the end of the next line
p.first = te.cursor + (te.from_cursor().index_of_char('\n') ?? (te.chars - te.cursor));
if (p.first < te.chars) {
p.second = p.first + 1 + (te.from(p.first + 1).index_of_char('\n') ?? (te.chars - p.first - 1));
} else {
p.second = te.chars;
}
} else {
// if searching backwards, look for the start of the current and previous line
if (te.cursor > 0) {
p.first = te.until(te.cursor-1).rindex_of_char('\n') ?? 0;
if (p.first != 0 && te.chars) p.first++;
}
p.second = te.until(p.first).rindex_of_char('\n') ?? 0;
}
return p;
}
fn void TextEdit.delete_selection(&te)
{
if (te.sel_len == 0) return;
usz start = te.sel_len > 0 ? te.cursor : te.cursor + te.sel_len;
usz end = te.sel_len > 0 ? te.cursor + te.sel_len : te.cursor;
usz len = te.chars - end;
te.buffer[start:len] = te.buffer[end:len];
te.cursor -= te.sel_len < 0 ? -te.sel_len : 0;
te.chars -= te.sel_len < 0 ? -te.sel_len : te.sel_len;
te.sel_len = 0;
}
// removes the character before or after the cursor
fn void TextEdit.remove_character(&te, bool forward)
{
// if there is a selection active then delete that selection
if (te.sel_len) {
te.delete_selection();
return;
}
if (te.chars == 0) return;
if (forward) {
usz rem = te.chars - te.cursor;
if (rem > 0) {
usz len = te.next_char_off();
te.buffer[te.cursor:rem-len] = te.buffer[te.cursor+len:rem-len];
te.chars -= len;
}
} else {
if (te.cursor > 0) {
usz len = te.prev_char_off();
te.buffer[te.cursor-len:te.chars-te.cursor] = te.buffer[te.cursor:te.chars-te.cursor];
te.chars -= len;
te.cursor -= len;
}
}
}
// remove the word before or after the cursor up until the next punctuation or space
fn void TextEdit.remove_word(&te, bool forward)
{
// if there is a selection active then delete that selection
if (te.sel_len) {
te.delete_selection();
return;
}
usz prev_cur = te.cursor;
while (te.chars > 0) {
char c;
if (forward) {
if (te.cursor == te.chars) break;
c = te.buffer[te.cursor];
} else {
if (te.cursor == 0) break;
c = te.buffer[te.cursor-1];
}
if (ascii::is_space(c) || ascii::is_punct(c)) break;
te.remove_character(forward);
}
// delete at least one character
if (prev_cur == te.cursor) te.remove_character(forward);
}
// insert a character at the cursor and update the cursor
fn void TextEdit.insert_character(&te, uint cp)
{
char[4] b;
usz len = conv::char32_to_utf8(cp, b[..])!!;
te.insert_utf8((String)b[:len]);
}
fn void TextEdit.insert_utf8(&te, String s)
{
if (s.len == 0) return;
if (s.len + te.chars > te.buffer.len) return;
te.delete_selection();
te.buffer[te.cursor+s.len : te.chars-te.cursor] = te.buffer[te.cursor : te.chars-te.cursor];
te.buffer[te.cursor : s.len] = s[..];
te.chars += s.len;
te.cursor += s.len;
}

View File

@ -1,226 +0,0 @@
module ugui;
import std::io;
// button element
struct ElemButton {
int filler;
}
macro Ctx.button(&ctx, String label = "", String icon = "", ...)
=> ctx.button_id(@compute_id($vasplat), label, icon);
fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_BUTTON)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("button"));
Sprite* sprite = icon != "" ? ctx.sprite_atlas.get(icon)! : &&(Sprite){};
Rect icon_size = sprite.rect();
ushort min_size = style.size;
ushort half_lh = (ushort)(ctx.font.line_height() / 2);
ushort inner_pad = label != "" && icon != "" ? half_lh : 0;
/*
* +--------------------------------------+
* | +--------+ |
* | | | +-----------------+ |
* | | icon | | label | |
* | | | +-----------------+ |
* | +--------+<->| |
* +-------------^------------------------+
* |inner_pad
*/
Point content_size = {
.x = icon_size.w + inner_pad, // text sizing is handled differently
.y = icon_size.h + inner_pad,
};
elem.layout.w = @fit(min_size);
elem.layout.h = @fit(min_size);
elem.layout.children.w = @exact(content_size.x);
elem.layout.children.h = @exact(content_size.y);
elem.layout.text = ctx.measure_string(label)!;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
//elem.events = ctx.get_elem_events(elem);
// if (ctx.input.z_index == elem.z_index) println("true ", elem.z_index);
Rect content_bounds = elem.content_bounds();
Rect icon_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = icon_size.w,
.h = icon_size.h
};
icon_bounds = icon_size.center_to(icon_bounds);
Rect text_bounds = {
.x = content_bounds.x + icon_bounds.w + inner_pad,
.y = content_bounds.y,
.w = content_bounds.w - icon_bounds.w - inner_pad,
.h = content_bounds.h,
};
//text_bounds = text_size.center_to(text_bounds);
bool is_active = elem.events.has_focus || elem.events.mouse_hover;
Style s = *style;
if (is_active) {
s.secondary = s.primary;
s.bg = s.accent;
}
ctx.push_rect(elem.bounds.pad(style.margin), parent.z_index, &s)!;
if (icon != "") {
ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.z_index, type: sprite.type)!;
}
if (label != "") {
ctx.layout_string(label, text_bounds, CENTER, parent.z_index, style.fg)!;
}
return elem.events;
}
macro Ctx.checkbox(&ctx, String desc, bool* active, String tick_sprite = "", ...)
=> ctx.checkbox_id(@compute_id($vasplat), desc, active, tick_sprite);
fn void? Ctx.checkbox_id(&ctx, Id id, String description, bool* active, String tick_sprite)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_BUTTON)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("checkbox"));
short inner_pad = description != "" ? style.size/2 : 0;
/*
* |< >| style.size/2
* +---------------------|---|-----------+
* | | .-----. ---|--
* | +-----------------+ ' ### ' | ^
* | | description | | ##### | | style.size
* | +-----------------+ . ### . | v
* | '-----' ---|--
* +-------------------------|-------|---+
* |<----->| style.size
*/
elem.layout.w = @fit(style.size);
elem.layout.h = @fit(style.size);
elem.layout.children.w = @exact(style.size + inner_pad);
elem.layout.children.h = @exact(style.size);
elem.layout.text = ctx.measure_string(description)!;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
//elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
Rect text_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = content_bounds.w - inner_pad - style.size,
.h = content_bounds.h
};
Rect check_bounds = {
.x = content_bounds.x + text_bounds.w + inner_pad,
.y = content_bounds.y + (content_bounds.h - style.size)/2,
.w = style.size,
.h = style.size,
};
Style s;
s.bg = style.bg;
s.secondary = style.secondary;
s.border = style.border;
s.radius = style.radius;
ctx.layout_string(description, text_bounds, CENTER, parent.z_index, style.fg)!;
if (tick_sprite != "") {
ctx.push_rect(check_bounds, parent.z_index, &s)!;
if (*active) {
Sprite* sprite = ctx.sprite_atlas.get(tick_sprite)!;
Id tex_id = ctx.sprite_atlas.id;
ctx.push_sprite(check_bounds, sprite.uv(), tex_id, parent.z_index, type: sprite.type)!;
}
} else {
if (*active) {
s.bg = style.primary;
ctx.push_rect(check_bounds, parent.z_index, &s)!;
} else {
ctx.push_rect(check_bounds, parent.z_index, &s)!;
}
}
}
macro Ctx.toggle(&ctx, String desc, bool* active)
=> ctx.toggle_id(@compute_id($vasplat), desc, active);
fn void? Ctx.toggle_id(&ctx, Id id, String description, bool* active)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_BUTTON)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("toggle"));
short inner_pad = description != "" ? style.size/2 : 0;
/*
* |< >| style.size/2
* +---------------------|---|-----------------+
* | | .-----------. ---|--
* | +-----------------+ ' ##### ' | ^
* | | description | | ##### | | style.size
* | +-----------------+ . ##### . | v
* | '-----------' ---|--
* +-------------------------|-------------|---+
* |<----->| style.size*2
*/
elem.layout.w = @fit(style.size*2);
elem.layout.h = @fit(style.size);
elem.layout.children.w = @exact(style.size*2 + inner_pad);
elem.layout.children.h = @exact(style.size);
elem.layout.text = ctx.measure_string(description)!;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
//elem.events = ctx.get_elem_events(elem);
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
Rect text_bounds = {
.x = content_bounds.x,
.y = content_bounds.y,
.w = content_bounds.w - inner_pad - style.size*2,
.h = content_bounds.h
};
Rect toggle_bounds = {
.x = content_bounds.x + text_bounds.w + inner_pad,
.y = content_bounds.y + (content_bounds.h - style.size)/2,
.w = style.size*2,
.h = style.size,
};
Rect toggle = {
.x = toggle_bounds.x + (*active ? style.size : 0),
.y = toggle_bounds.y,
.w = style.size,
.h = style.size
};
Style s;
s.bg = style.bg;
s.secondary = style.secondary;
s.border = style.border;
s.radius = style.radius;
ctx.layout_string(description, text_bounds, CENTER, parent.z_index, style.fg)!;
ctx.push_rect(toggle_bounds, parent.z_index, &s)!;
s.bg = style.primary;
s.border = {};
ctx.push_rect(toggle.pad(style.border), parent.z_index, &s)!;
}

View File

@ -1,252 +0,0 @@
module ugui;
import std::io;
import std::math;
// div element
struct ElemDiv {
struct scroll_x {
bool enabled;
bool on;
float value;
}
struct scroll_y {
bool enabled;
bool on;
float value;
}
}
macro Ctx.@center(&ctx, LayoutDirection dir = ROW, ...; @body())
{
return ctx.@div(@grow(), @grow(), dir, CENTER) {
@body();
}!;
}
macro Ctx.@row(&ctx, Anchor anchor = TOP_LEFT, ...; @body())
{
return ctx.@div(@fit(), @fit(), ROW, anchor: anchor) {
@body();
}!;
}
macro Ctx.@column(&ctx, Anchor anchor = TOP_LEFT, ...; @body())
{
return ctx.@div(@fit(), @fit(), COLUMN, anchor: anchor) {
@body();
}!;
}
// useful macro to start and end a div, capturing the trailing block
macro Ctx.@div(&ctx,
Size width = @grow, Size height = @grow,
LayoutDirection dir = ROW,
Anchor anchor = TOP_LEFT,
bool absolute = false, Point off = {},
bool scroll_x = false, bool scroll_y = false,
...;
@body()
)
{
ctx.div_begin(width, height, dir, anchor, absolute, off, scroll_x, scroll_y, $vasplat)!;
@body();
return ctx.div_end()!;
}
macro Ctx.div_begin(&ctx,
Size width = @grow(), Size height = @grow(),
LayoutDirection dir = ROW,
Anchor anchor = TOP_LEFT,
bool absolute = false, Point off = {},
bool scroll_x = false, bool scroll_y = false,
...
)
{
return ctx.div_begin_id(@compute_id($vasplat), width, height, dir, anchor, absolute, off, scroll_x, scroll_y);
}
fn void? Ctx.div_begin_id(&ctx,
Id id,
Size width, Size height,
LayoutDirection dir,
Anchor anchor,
bool absolute, Point off,
bool scroll_x, bool scroll_y
)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_DIV)!.unpack(&elem, &parent);
ctx.active_div = elem.tree_idx;
Style* style = ctx.styles.get_style(@str_hash("div"));
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
// update layout with correct info
elem.layout = {
.w = width,
.h = height,
.dir = dir,
.anchor = anchor,
.content_offset = style.margin + style.border + style.padding,
.absolute = absolute,
};
if (absolute) {
elem.layout.origin.x = off.x;
elem.layout.origin.y = off.y;
}
ctx.push_rect(elem.bounds.pad(style.margin), elem.z_index, style)!;
// update the ctx scissor, it HAS to be after drawing the background
ctx.div_scissor = elem.bounds.pad(elem.layout.content_offset).max({0,0,0,0});
ctx.push_scissor(ctx.div_scissor, elem.z_index)!;
//elem.events = ctx.get_elem_events(elem);
// TODO: check active
// TODO: check resizeable
}
fn Id? Ctx.div_end(&ctx)
{
Elem* elem = ctx.get_active_div()!;
Style* style = ctx.styles.get_style(@str_hash("div"));
Rect bounds = elem.bounds.pad(style.margin + style.border);
// set the scrollbar flag, is used in layout
Point cbc = elem.children_bounds.bottom_right();
Point bc = elem.bounds.bottom_right();
// horizontal overflow
elem.div.scroll_x.on = cbc.x > bc.x && elem.div.scroll_x.enabled;
// vertical overflow
elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled;
Id hsid_raw = @str_hash("div_scrollbar_horizontal");
Id vsid_raw = @str_hash("div_scrollbar_vertical");
Id hsid_real = ctx.gen_id(hsid_raw)!;
Id vsid_real = ctx.gen_id(vsid_raw)!;
if (elem.div.scroll_y.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id && !(ctx.get_mod() & KMOD_SHIFT)) {
elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f;
elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f);
}
ctx.scrollbar(vsid_raw, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
elem.layout.scroll_offset.y = (short)(elem.div.scroll_y.value*(float)(elem.children_bounds.h-elem.bounds.h));
} else {
elem.div.scroll_y.value = 0;
}
if (elem.div.scroll_x.on) {
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
if (ctx.get_mod() & KMOD_SHIFT) { // horizontal scroll with shift
elem.div.scroll_x.value += ctx.input.mouse.scroll.y * 0.07f;
elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
} else {
elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f;
elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
}
}
ctx.scrollbar(hsid_raw, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15), false)!;
elem.layout.scroll_offset.x = (short)(elem.div.scroll_x.value*(float)(elem.children_bounds.w-elem.bounds.w));
} else {
elem.div.scroll_x.value = 0;
}
// the active_div returns to the parent of the current one
ctx.active_div = ctx.tree.parentof(ctx.active_div)!;
Elem* parent = ctx.get_parent()!;
ctx.div_scissor = parent.bounds.pad(parent.layout.content_offset).max({0,0,0,0});
ctx.reset_scissor(elem.z_index)!;
update_parent_size(elem, parent);
return elem.id;
}
<* @param[&inout] state *>
macro Ctx.@popup(&ctx, bool* state,
Point pos,
Size width, Size height,
LayoutDirection dir = ROW, Anchor anchor = TOP_LEFT,
bool scroll_x = false, bool scroll_y = false,
...; @body())
{
if (*state) {
*state = ctx.popup_begin(pos, width, height, dir, anchor, scroll_x, scroll_y)!;
@body();
ctx.div_end()!;
}
}
macro bool? Ctx.popup_begin(&ctx, Point pos,
Size width, Size height,
LayoutDirection dir = ROW, Anchor anchor = TOP_LEFT,
bool scroll_x = false, bool scroll_y = false,
...
)
=> ctx.popup_begin_id(@compute_id($vasplat), pos, width, height, dir, anchor, scroll_x, scroll_y);
fn bool? Ctx.popup_begin_id(&ctx,
Id id,
Point pos,
Size width, Size height,
LayoutDirection dir, Anchor anchor,
bool scroll_x, bool scroll_y
)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_DIV)!.unpack(&elem, &parent);
ctx.active_div = elem.tree_idx;
Style* style = ctx.styles.get_style(@str_hash("popup"));
elem.div.scroll_x.enabled = scroll_x;
elem.div.scroll_y.enabled = scroll_y;
elem.z_index++;
// update layout with correct info
elem.layout = {
.w = width,
.h = height,
.dir = dir,
.anchor = anchor,
.content_offset = style.margin + style.border + style.padding,
.absolute = true,
.origin.x = pos.x - parent.bounds.x,
.origin.y = pos.y - parent.bounds.y,
};
// if the position is not the one expected then request a frame-skip to not flash appear in the
// wrong position for one frame
if (elem.bounds.position() != pos) {
ctx.skip_frame = true;
return true;
}
ctx.push_rect(elem.bounds.pad(style.margin), elem.z_index, style)!;
// update the ctx scissor, it HAS to be after drawing the background
ctx.div_scissor = elem.bounds.pad(elem.layout.content_offset).max({0,0,0,0});
ctx.push_scissor(ctx.div_scissor, elem.z_index)!;
//elem.events = ctx.get_elem_events(elem);
// check close condition, mouse release anywhere outside the div bounds
if ((ctx.mouse_released() & BTN_ANY) && ctx.input.mouse.pos.outside(elem.bounds)) {
return false;
}
// TODO: check active
// TODO: check resizeable
return true;
}

View File

@ -1,56 +0,0 @@
module ugui;
macro Ctx.separator(&ctx, Size width, Size height, ...)
=> ctx.separator_id(@compute_id($vasplat), width, height);
fn void? Ctx.separator_id(&ctx, Id id, Size width, Size height)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_NONE)!.unpack(&elem, &parent);
elem.layout.w = width;
elem.layout.h = height;
update_parent_size(elem, parent);
}
macro Ctx.hor_line(&ctx, ...)
=> ctx.hor_line_id(@compute_id($vasplat));
fn void? Ctx.hor_line_id(&ctx, Id id)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_NONE)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("separator"));
elem.layout.w = @grow();
elem.layout.h = @exact(style.size);
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
Rect r = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(r, elem.z_index, style)!;
}
macro Ctx.ver_line(&ctx, ...)
=> ctx.ver_line_id(@compute_id($vasplat));
fn void? Ctx.ver_line_id(&ctx, Id id)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_NONE)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("separator"));
elem.layout.w = @exact(style.size);
elem.layout.h = @grow();
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
Rect r = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(r, elem.z_index, style)!;
}

View File

@ -1,185 +0,0 @@
module ugui;
import std::io;
import std::math;
// slider element
struct ElemSlider {
Rect handle;
}
/* handle
* +----+-----+---------------------+
* | |#####| |
* +----+-----+---------------------+
*/
macro Ctx.slider_hor(&ctx, Size w, Size h, float* value, float hpercent = 0.25, ...)
=> ctx.slider_hor_id(@compute_id($vasplat), w, h, value, hpercent);
<*
@require value != null
*>
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Size w, Size h, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SLIDER)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("slider"));
elem.layout.w = w;
elem.layout.h = h;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect content_bounds = elem.bounds.pad(style.margin + style.border + style.padding);
// handle width
short hw = (short)(content_bounds.w * hpercent);
Rect handle = {
.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value),
.y = content_bounds.y,
.w = hw,
.h = content_bounds.h,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
//elem.events = ctx.get_elem_events(elem);
if (elem.events.has_focus && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(content_bounds.x, m.x, content_bounds.w, hw);
elem.slider.handle.x = calc_slider(content_bounds.x, content_bounds.w-hw, *value);
elem.events.update = true;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(bg_bounds, parent.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.z_index, &s)!;
return elem.events;
}
/*
* +--+
* | |
* | |
* +--+
* |##| handle
* |##|
* +--+
* | |
* | |
* +--+
*/
macro Ctx.slider_ver(&ctx, Size w, Size h, float* value, float hpercent = 0.25, ...)
=> ctx.slider_ver_id(@compute_id($vasplat), w, h, value, hpercent);
fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Size w, Size h, float* value, float hpercent = 0.25)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SLIDER)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("slider"));
elem.layout.w = w;
elem.layout.h = h;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
// 2. Layout
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect content_bounds = elem.bounds.pad(style.margin + style.border + style.padding);
// handle height
short hh = (short)(content_bounds.h * hpercent);
Rect handle = {
.x = content_bounds.x,
.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value),
.w = content_bounds.w,
.h = hh,
};
elem.slider.handle = handle;
Point m = ctx.input.mouse.pos;
//elem.events = ctx.get_elem_events(elem);
if (elem.events.has_focus && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(content_bounds.y, m.y, content_bounds.h, hh);
elem.slider.handle.y = calc_slider(content_bounds.y, content_bounds.h-hh, *value);
elem.events.update = true;
}
// Draw the slider background and handle
Style s = *style;
Rect padding = s.padding;
s.padding = {};
ctx.push_rect(bg_bounds, parent.z_index, &s)!;
s.bg = s.primary;
s.padding = padding;
s.border = {};
ctx.push_rect(elem.slider.handle, parent.z_index, &s)!;
return elem.events;
}
fn void? Ctx.scrollbar(&ctx, Id id, float *value, float handle_percent, bool vertical = true)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SLIDER)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("scrollbar"));
Rect pb = parent.bounds.pad(parent.layout.content_offset);
if (vertical) {
elem.layout.w = @exact(style.size);
elem.layout.h = @grow();
elem.layout.origin.x = pb.w - style.size;
elem.layout.origin.y = 0;
} else {
elem.layout.w = @grow();
elem.layout.h = @exact(style.size);
elem.layout.origin.x = 0;
elem.layout.origin.y = pb.h - style.size;
}
elem.layout.content_offset = style.margin + style.border + style.padding;
elem.layout.absolute = true;
update_parent_size(elem, parent);
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
short o = vertical ? content_bounds.y : content_bounds.x;
short m = vertical ? ctx.input.mouse.pos.y : ctx.input.mouse.pos.x;
short s = vertical ? content_bounds.h : content_bounds.w;
short h = (short)((float)s * handle_percent);
if (elem.events.has_focus && ctx.is_mouse_down(BTN_LEFT)) {
*value = calc_value(o, m, s, h);
elem.events.update = true;
}
short handle_pos = calc_slider(o, s-h, *value);
elem.slider.handle = {
.x = vertical ? content_bounds.x : handle_pos,
.y = vertical ? handle_pos : content_bounds.y,
.w = vertical ? content_bounds.w : h,
.h = vertical ? h : content_bounds.h,
};
Rect bg_bounds = elem.bounds.pad(style.margin);
ctx.push_rect(bg_bounds, parent.z_index, style)!;
ctx.push_rect(elem.slider.handle, parent.z_index, &&(Style){.bg = style.primary, .radius = style.radius})!;
}
macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value);
macro float calc_value(short off, short mouse, short dim, short slider)
=> math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f);

View File

@ -1,41 +0,0 @@
module ugui;
struct ElemSprite {
Id id;
}
macro Ctx.sprite(&ctx, String name, short size = 0, ...)
=> ctx.sprite_id(@compute_id($vasplat), name, size);
fn void? Ctx.sprite_id(&ctx, Id id, String name, short size = 0)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_SPRITE)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("sprite"));
Sprite* sprite = ctx.sprite_atlas.get(name)!;
elem.sprite.id = ctx.get_sprite_atlas_id(name);
// scale the sprite so that the biggest dimension becomes "size"
short width = sprite.w;
short height = sprite.h;
if (size > 0) {
if (sprite.w >= sprite.h) {
height = (short)(size * (float)height/width);
width = size;
} else {
width = (short)(size * (float)width/height);
height = size;
}
}
elem.layout.w = elem.layout.children.w = @exact(width);
elem.layout.h = elem.layout.children.h = @exact(height);
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
Id tex_id = ctx.sprite_atlas.id;
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
return ctx.push_sprite(content_bounds, sprite.uv(), tex_id, parent.z_index, type: sprite.type)!;
}

View File

@ -1,97 +0,0 @@
module ugui;
import std::io;
struct ElemText {
Id hash;
TextSize size;
TextEdit* te;
}
/* Layout some text without bounds.
* There is a limitation where the current frame bounds are based on the last frame, this is usually
* not a problem but it is in the situation where the text changes almost all frames.
*/
macro Ctx.text(&ctx, String text, ...)
=> ctx.text_id(@compute_id($vasplat), text);
fn void? Ctx.text_id(&ctx, Id id, String text)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_TEXT)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("text"));
Id text_hash = text.hash();
if (elem.flags.is_new || elem.text.hash != text_hash) {
elem.text.size = ctx.measure_string(text)!;
}
elem.text.hash = text_hash;
elem.layout.w = @fit(style.size);
elem.layout.h = @fit(style.size);
elem.layout.text = elem.text.size;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
ctx.layout_string(text, elem.bounds.pad(elem.layout.content_offset), TOP_LEFT, parent.z_index, style.fg)!;
}
macro Ctx.text_box(&ctx, Size w, Size h, TextEdit* te, Anchor text_alignment = TOP_LEFT, bool reflow = true, ...)
=> ctx.text_box_id(@compute_id($vasplat), w, h, te, text_alignment, reflow);
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te, Anchor text_alignment, bool reflow)
{
id = ctx.gen_id(id)!;
Elem* parent, elem;
ctx.get_elem(id, ETYPE_TEXT)!.unpack(&elem, &parent);
Style* style = ctx.styles.get_style(@str_hash("text-box"));
elem.text.te = te;
Id text_hash = te.to_string().hash();
if (elem.flags.is_new || elem.text.hash != text_hash) {
elem.text.size = ctx.measure_string(te.to_string())!;
}
elem.text.hash = text_hash;
elem.layout.w = w;
elem.layout.h = h;
elem.layout.text = elem.text.size;
elem.layout.content_offset = style.margin + style.border + style.padding;
update_parent_size(elem, parent);
// check input and update the text
//elem.events = ctx.get_elem_events(elem);
if (elem.events.text_input || elem.events.key_press) {
ctx.text_edit(elem.text.te);
}
Rect bg_bounds = elem.bounds.pad(style.margin);
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
ctx.push_rect(bg_bounds, parent.z_index, style)!;
String s = elem.text.te.to_string();
if (te.sel_len) {
usz start = te.sel_len > 0 ? te.cursor : te.cursor + te.sel_len;
usz end = (te.sel_len > 0 ? te.cursor + te.sel_len : te.cursor) - 1;
ctx.draw_string_selection(s, text_bounds, text_alignment, start, end, parent.z_index, style.accent, reflow)!;
}
ctx.layout_string(s, text_bounds, text_alignment, parent.z_index, style.fg, reflow)!;
// draw the cursor if the element has focus
if (elem.events.has_focus) {
if (elem.events.mouse_press || elem.events.mouse_hold) {
usz cur = ctx.hit_test_string(s, text_bounds, text_alignment, ctx.input.mouse.pos, reflow)!;
bool select = (elem.events.mouse_hold && !elem.events.mouse_press) || (ctx.get_mod() & KMOD_SHIFT);
te.set_cursor(cur, select);
}
Rect cur = ctx.get_cursor_position(s, text_bounds, text_alignment, te.cursor, reflow)!;
cur.w = 2;
ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!;
}
return elem.events;
}

137
stuff/generic_hash.h Normal file
View File

@ -0,0 +1,137 @@
#ifndef _HASH_GENERIC
#define _HASH_GENERIC
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
// FIXME: change the api to just one HASH_DECL to HASH_PROTO and HASH_DEFINE
#define HASH_MAXSIZE 4096
// for fibonacci hashing, 2^{32,64}/<golden ratio>
#define HASH_RATIO32 ((uint64_t)2654435769u)
#define HASH_RATIO64 ((uint64_t)11400714819322457583u)
// salt for string hashing
#define HASH_SALT ((uint64_t)0xbabb0cac)
/* Ready-made compares */
static inline int hash_cmp_u32(uint32_t a, uint32_t b) { return a == b; }
static inline int hash_cmp_u64(uint64_t a, uint64_t b) { return a == b; }
static inline int hash_cmp_str(const char *a, const char *b) { return strcmp(a, b) == 0; }
/* Ready-made hashes */
static inline uint32_t hash_u64(uint64_t c)
{
return (uint64_t)((((uint64_t)c+HASH_SALT)*HASH_RATIO64)>>32);
}
static inline uint32_t hash_u32(uint32_t c)
{
return (uint32_t)((((uint64_t)c<<31)*HASH_RATIO64)>>32);
}
static inline uint32_t hash_str(const char *s)
{
uint32_t h = HASH_SALT;
const uint8_t *v = (const uint8_t *)(s);
for (int x = *s; x; x--) {
h += v[x-1];
h += h << 10;
h ^= h >> 6;
}
h += h << 3;
h ^= h >> 11;
h += h << 15;
return h;
}
#define HASH_DECL(htname, codetype, datatype, hashfn, cmpfn) \
struct htname##_entry { \
codetype code; \
datatype data; \
}; \
\
struct htname##_ref { \
uint32_t items, size, exp; \
struct htname##_entry bucket[]; \
}; \
\
\
struct htname##_ref * htname##_create(uint32_t size) \
{ \
if (!size || size > HASH_MAXSIZE) \
return NULL; \
/* round to the greater power of two */ \
/* FIXME: check for intger overflow here */ \
uint32_t exp = 32-__builtin_clz(size-1); \
size = 1<<(exp); \
/* FIXME: check for intger overflow here */ \
struct htname##_ref *ht = malloc(sizeof(struct htname##_ref)+sizeof(struct htname##_entry)*size); \
if (ht) { \
ht->items = 0; \
ht->size = size; \
ht->exp = exp; \
memset(ht->bucket, 0, sizeof(struct htname##_entry)*size); \
} \
return ht; \
} \
\
\
void htname##_destroy(struct htname##_ref *ht) \
{ \
if (ht) free(ht); \
} \
\
\
static uint32_t htname##_lookup(struct htname##_ref *ht, uint32_t hash, uint32_t idx) \
{ \
if (!ht) return 0; \
uint32_t mask = ht->size-1; \
uint32_t step = (hash >> (32 - ht->exp)) | 1; \
return (idx + step) & mask; \
} \
\
\
/* Find and return the element by code */ \
struct htname##_entry * htname##_search(struct htname##_ref *ht, codetype code)\
{ \
if (!ht) return NULL; \
uint32_t h = hashfn(code); \
for (uint32_t i=h, x=0; ; x++) { \
i = htname##_lookup(ht, h, i); \
if (x > (ht->size<<1) || \
!ht->bucket[i].code || \
cmpfn(ht->bucket[i].code, code) \
) { \
return &(ht->bucket[i]); \
} \
} \
return NULL; \
} \
\
\
/* FIXME: this simply overrides the found item */ \
struct htname##_entry * htname##_insert(struct htname##_ref *ht, struct htname##_entry *entry) \
{ \
struct htname##_entry *r = htname##_search(ht, entry->code); \
if (r) { \
if (!r->code) \
ht->items++; \
*r = *entry; \
} \
return r; \
} \
\
\
struct htname##_entry * htname##_remove(struct htname##_ref *ht, codetype code)\
{ \
if (!ht) return NULL; \
struct htname##_entry *r = htname##_search(ht, code); \
if (r) r->code = 0; \
return r; \
} \
#endif

118
stuff/generic_stack.h Normal file
View File

@ -0,0 +1,118 @@
#ifndef _STACK_GENERIC_H
#define _STACK_GENERIC_H
#define STACK_STEP 8
#define STACK_SALT 0xbabb0cac
// FIXME: find a way to not re-hash the whole stack when removing one item
// incremental hash for every grow
#if STACK_DISABLE_HASH
#define STACK_HASH(p, s, h) {}
#else
#define STACK_HASH(p, s, h) \
{ \
unsigned char *v = (unsigned char *)(p); \
for (int x = (s); x; x--) { \
(h) += v[x-1]; \
(h) += (h) << 10; \
(h) ^= (h) >> 6; \
} \
(h) += (h) << 3; \
(h) ^= (h) >> 11; \
(h) += (h) << 15; \
}
#endif
// TODO: add a rolling hash
#define STACK_DECL(stackname, type) \
struct stackname { \
type *items; \
int size, idx, old_idx; \
unsigned int hash, old_hash; \
}; \
\
\
struct stackname stackname##_init(void) \
{ \
return (struct stackname){0, .hash = STACK_SALT}; \
} \
\
\
int stackname##_grow(struct stackname *stack, int step) \
{ \
if (!stack) \
return -1; \
stack->items = realloc(stack->items, (stack->size+step)*sizeof(type)); \
if(!stack->items) \
return -1; \
memset(&(stack->items[stack->size]), 0, step*sizeof(*(stack->items))); \
stack->size += step; \
return 0; \
} \
\
\
int stackname##_push(struct stackname *stack, type *e) \
{ \
if (!stack || !e) \
return -1; \
if (stack->idx >= stack->size) \
if (stackname##_grow(stack, STACK_STEP)) \
return -1; \
stack->items[stack->idx++] = *e; \
STACK_HASH(e, sizeof(type), stack->hash); \
return 0; \
} \
\
\
type stackname##_pop(struct stackname *stack) \
{ \
if (!stack || stack->idx == 0 || stack->size == 0) \
return (type){0}; \
stack->hash = STACK_SALT; \
STACK_HASH(stack->items, sizeof(type)*(stack->idx-1), stack->hash); \
return stack->items[stack->idx--]; \
} \
\
\
int stackname##_clear(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
stack->old_idx = stack->idx; \
stack->old_hash = stack->hash; \
stack->hash = STACK_SALT; \
stack->idx = 0; \
return 0; \
} \
\
\
int stackname##_changed(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
return stack->hash != stack->old_hash; \
} \
\
\
int stackname##_size_changed(struct stackname *stack) \
{ \
if (!stack) \
return -1; \
return stack->size != stack->old_idx; \
} \
\
\
int stackname##_free(struct stackname *stack) \
{ \
if (stack) { \
stackname##_clear(stack); \
if (stack->items) \
free(stack->items); \
} \
return 0; \
} \
#endif

214
stuff/main.c Executable file
View File

@ -0,0 +1,214 @@
//#!/usr/bin/tcc -run
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HASH_SALT (0xbabb0cac)
#define HASH_RATIO64 ((unsigned long int)11400714819322457583u)
struct ntree_node
{
unsigned int id;
const char *name;
int children_no, children_size;
struct ntree_node *children, *parent;
};
static char tmp_name_buffer[16] = {"node:"};
unsigned int hash_str_to_uint(const char *s)
{
unsigned int h = HASH_SALT;
const unsigned char *v = (const unsigned char *)(s);
for (int x = *s; x; x--) {
h += v[x-1];
h += h << 10;
h ^= h >> 6;
}
h += h << 3;
h ^= h >> 11;
h += h << 15;
return h;
}
unsigned int hash_u32(unsigned int c)
{
return (unsigned int)((((unsigned long int)c<<31)*HASH_RATIO64)>>32);
}
const char * generate_new_name(void)
{
static int count = 0;
unsigned int h = hash_u32(count++);
snprintf(tmp_name_buffer+sizeof("node:"), 16-sizeof("node:"), "%x", h);
return tmp_name_buffer;
}
int tree_prune(struct ntree_node *node)
{
if (node == NULL)
return 0;
if ((node->children_no == 0 && node->children != NULL) ||
(node->children_no != 0 && node->children == NULL)) {
printf("ERR: (%x %s) Inconsistent node children", node->id, node->name);
} else for (int i = 0; i < node->children_no; i++) {
tree_prune(&(node->children[i]));
}
if (node->children != NULL) {
free(node->children);
}
free((void *)node->name);
if (node->parent != NULL)
node->parent->children_no--;
*node = (struct ntree_node){0};
return 0;
}
struct ntree_node * tree_append(struct ntree_node *parent, const char *name)
{
if (name == NULL)
return NULL;
if (strlen(name) == 0)
name = generate_new_name();
// generate the children information
unsigned int id = hash_str_to_uint(name);
char *str = malloc(strlen(name)+1);
if (str == NULL)
return NULL;
strcpy(str, name);
// grow the parent buffer if necessary
if (parent->children_no >= parent->children_size) {
struct ntree_node *temp = NULL;
temp = realloc(parent->children, (parent->children_size+1)*sizeof(struct ntree_node));
if (temp == NULL) {
free(str);
return NULL;
}
parent->children = temp;
parent->children[parent->children_size] = (struct ntree_node){0};
parent->children_size++;
}
// find an open spot for the child
struct ntree_node *child = NULL;
for (int i = 0; i < parent->children_size; i++) {
if (parent->children[i].id == 0)
child = &(parent->children[i]);
}
if (child == NULL)
return NULL;
child->name = str;
child->id = id;
child->children = NULL;
child->children_no = 0;
child->parent = parent;
parent->children_no++;
//printf("append to %s, children: %d\n", parent->name, parent->children_no);
return child;
}
struct ntree_node * tree_find_id(struct ntree_node *root, unsigned int id)
{
if (id == 0 || root == NULL)
return NULL;
if (root->id == id)
return root;
else for (int i = 0; i < root->children_size; i++) {
if (root->children[i].id != 0 && tree_find_id(&(root->children[i]), id) != NULL)
return &(root->children[i]);
}
return NULL;
}
// TODO: add a foreach_child function
struct ntree_node * tree_find(struct ntree_node *root, const char *name)
{
if (name == NULL || strlen(name) == 0 || root == NULL)
return NULL;
unsigned int id = hash_str_to_uint(name);
return tree_find_id(root, id);
}
int tree_size(struct ntree_node *root)
{
if (root == NULL)
return 0;
int count = root->children_no;
if (count > 0) {
for (int i = 0; i < root->children_size; i++) {
if (root->children[i].id != 0)
count += tree_size(&(root->children[i]));
}
}
return count;
}
static int tree_print_ind(struct ntree_node *node, int dd)
{
for (int i = 0; i < dd; i++) printf(" ");
printf("[%s]\n", node->name);
for (int i = 0; i < node->children_size; i++) {
if (node->children[i].id == 0) continue;
tree_print_ind(&(node->children[i]), dd+1);
}
return node->children_no;
}
int tree_print(struct ntree_node *root)
{
if (root == NULL)
return 1;
tree_print_ind(root, 0);
return 0;
}
int main(void)
{
char *root_name = malloc(sizeof("root"));
strcpy(root_name, "root");
struct ntree_node root = {.name = root_name};
struct ntree_node *n;
n = tree_append(&root, "node 0:0");
tree_append(n, "node 0:0:0");
tree_append(&root, "node 0:1");
tree_append(&root, "node 0:2");
printf("Number of nodes %d\n", tree_size(&root));
tree_print(&root);
tree_prune(tree_find(&root, "node 0:0"));
printf("Number of nodes %d\n", tree_size(&root));
tree_prune(&root);
printf("Number of nodes %d\n", tree_size(&root));
return 0;
}

348
stuff/vectree.h Normal file
View File

@ -0,0 +1,348 @@
#ifndef _VECTREE_H
#define _VECTREE_H
#ifdef VTREE_DTYPE
typedef struct {
int size, elements;
VTREE_DTYPE *vector;
int *refs;
} Vtree;
int vtree_init(Vtree *tree, unsigned int size);
int vtree_pack(Vtree *tree);
int vtree_resize(Vtree *tree, unsigned int newsize);
int vtree_add(Vtree *tree, VTREE_DTYPE elem, int parent);
int vtree_prune(Vtree *tree, int ref);
int vtree_subtree_size(Vtree *tree, int ref);
int vtree_children_it(Vtree *tree, int parent, int *cursor);
int vtree_level_order_it(Vtree *tree, int ref, int **queue_p, int *cursor);
int vtree_destroy(Vtree *tree);
#ifdef VTREE_IMPL
#define IS_VALID_REF(t, r) ((r) >= 0 && (r) < (t)->size)
#define REF_IS_PRESENT(t, r) ((t)->refs[r] >= 0)
int vtree_init(Vtree *tree, unsigned int size)
{
if (tree == NULL) {
return -1;
}
tree->vector = malloc(sizeof(VTREE_DTYPE) * size);
if (tree->vector == NULL) {
return -1;
}
tree->refs = malloc(sizeof(int) * size);
if (tree->refs == NULL) {
free(tree->vector);
return -1;
}
// set all refs to -1, meaning invalid (free) element
for (unsigned int i = 0; i < size; i++) {
tree->refs[i] = -1;
}
// fill vector with zeroes
memset(tree->vector, 0, size * sizeof(VTREE_DTYPE));
tree->size = size;
tree->elements = 0;
return 0;
}
int vtree_destroy(Vtree *tree)
{
if (tree == NULL) {
return -1;
}
free(tree->vector);
free(tree->refs);
return 0;
}
int vtree_pack(Vtree *tree)
{
if (tree == NULL) {
return -1;
}
// TODO: add a PACKED flag to skip this
int free_spot = -1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == -1) {
free_spot = i;
continue;
}
// find a item that can be packed
if (free_spot >= 0 && tree->refs[i] >= 0) {
int old_ref = i;
// move the item
tree->vector[free_spot] = tree->vector[i];
tree->refs[free_spot] = tree->refs[i];
tree->vector[i] = (VTREE_DTYPE){0};
tree->refs[i] = -1;
// and move all references
for (int x = 0; x < tree->size; x++) {
if (tree->refs[x] == old_ref) {
tree->refs[x] = free_spot;
}
}
// mark the free spot as used
free_spot = -1;
}
}
return 0;
}
int vtree_resize(Vtree *tree, unsigned int newsize)
{
if (tree == NULL) {
return -1;
}
// return error when shrinking with too many elements
if ((int)newsize < tree->elements) {
return -1;
}
// pack the vector when shrinking to avoid data loss
if ((int)newsize < tree->size) {
//if (vtree_pack(tree) < 0) {
// return -1;
//}
// TODO: allow shrinking, since packing destroys all references
return -1;
}
VTREE_DTYPE *newvec = realloc(tree->vector, newsize * sizeof(VTREE_DTYPE));
if (newvec == NULL) {
return -1;
}
int *newrefs = realloc(tree->refs, newsize * sizeof(int));
if (newrefs == NULL) {
return -1;
}
tree->vector = newvec;
tree->refs = newrefs;
if ((int)newsize > tree->size) {
for (int i = tree->size; i < (int)newsize; i++) {
tree->vector[i] = (VTREE_DTYPE){0};
tree->refs[i] = -1;
}
}
tree->size = newsize;
return 0;
}
// add an element to the tree, return it's ref
int vtree_add(Vtree *tree, VTREE_DTYPE elem, int parent)
{
if (tree == NULL) {
return -1;
}
// invalid parent
if (!IS_VALID_REF(tree, parent)) {
return -1;
}
// no space left
if (tree->elements >= tree->size) {
return -1;
}
// check if the parent exists
// if there are no elements in the tree the first add will set the root
if (!REF_IS_PRESENT(tree, parent) && tree->elements != 0) {
return -1;
}
// get the first free spot
int free_spot = -1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == -1) {
free_spot = i;
break;
}
}
if (free_spot < 0) {
return -1;
}
// finally add the element
tree->vector[free_spot] = elem;
tree->refs[free_spot] = parent;
tree->elements++;
return free_spot;
}
// prune the tree starting from the ref
// returns the number of pruned elements
int vtree_prune(Vtree *tree, int ref)
{
if (tree == NULL) {
return -1;
}
if (!IS_VALID_REF(tree, ref)) {
return -1;
}
if (!REF_IS_PRESENT(tree, ref)) {
return 0;
}
tree->vector[ref] = (VTREE_DTYPE){0};
tree->refs[ref] = -1;
int count = 1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == ref) {
count += vtree_prune(tree, i);
}
}
return count;
}
// find the size of the subtree starting from ref
int vtree_subtree_size(Vtree *tree, int ref)
{
if (tree == NULL) {
return -1;
}
if (!IS_VALID_REF(tree, ref)) {
return -1;
}
if (!REF_IS_PRESENT(tree, ref)) {
return 0;
}
int count = 1;
for (int i = 0; i < tree->size; i++) {
// only root has the reference to itself
if (tree->refs[i] == ref && ref != i) {
count += vtree_subtree_size(tree, i);
}
}
return count;
}
// iterate through the first level children, use a cursor like strtok_r
int vtree_children_it(Vtree *tree, int parent, int *cursor)
{
if (tree == NULL || cursor == NULL) {
return -1;
}
// if the cursor is out of bounds then we are done for sure
if (!IS_VALID_REF(tree, *cursor)) {
return -1;
}
// same for the parent, if it's invalid it can't have children
if (!IS_VALID_REF(tree, parent) || !REF_IS_PRESENT(tree, parent)) {
return -1;
}
// find the first child, update the cursor and return the ref
for (int i = *cursor; i < tree->size; i++) {
if (tree->refs[i] == parent) {
*cursor = i + 1;
return i;
}
}
// if no children are found return -1
*cursor = -1;
return -1;
}
/* iterates trough every leaf of the subtree in the following manner
* node [x], x: visit order
* [0]
* / | \
* / [2] [3]
* [1] |
* / \ [6]
* [4] [5]
*/
int vtree_level_order_it(Vtree *tree, int ref, int **queue_p, int *cursor)
{
if (tree == NULL || queue_p == NULL || cursor == NULL) {
return -1;
}
int *queue = *queue_p;
// TODO: this could also be done when adding or removing elements
// first call, create a ref array ordered like we desire
if (queue == NULL) {
*cursor = 0;
// create a queue of invalid refs, size is the worst case
queue = malloc(sizeof(int) * tree->size);
if (queue == NULL) {
return -1;
}
for (int i = 0; i < tree->size; i++) {
queue[i] = -1;
}
*queue_p = queue;
// iterate through the queue appending found children
int pos = 0, off = 0;
do {
//printf ("ref=%d\n", ref);
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == ref) {
queue[pos++] = i;
}
}
for (;ref == queue[off] && off < tree->size; off++);
ref = queue[off];
} while (IS_VALID_REF(tree, ref));
}
//PRINT_ARR(queue, tree->size);
//return -1;
// on successive calls just iterate through the queue until we find an
// invalid ref
int ret = queue[(*cursor)++];
if (!IS_VALID_REF(tree, ret)) {
free(queue);
}
return ret;
}
#endif // VTREE_IMPL
#endif // VTREE_DTYPE
#endif

69
timer.c Normal file
View File

@ -0,0 +1,69 @@
#define _POSIX_C_SOURCE 200809l
#include <time.h>
#include "timer.h"
const clockid_t CLOCK_ID = CLOCK_PROCESS_CPUTIME_ID;
struct _timer {
struct timespec start, stop;
struct timespec times[TIMER_MAX_PARTIAL];
} timer = {0};
int timer_start(void) { return clock_gettime(CLOCK_ID, &timer.start); }
int timer_reset(void)
{
timer = (struct _timer) {0};
return 0;
}
int timer_stop(void) { return clock_gettime(CLOCK_ID, &timer.stop); }
// partial clocks also update the stop time
int timer_partial(int idx)
{
if (idx > TIMER_MAX_PARTIAL || idx < 0) {
return -1;
}
clock_gettime(CLOCK_ID, &timer.stop);
return clock_gettime(CLOCK_ID, &(timer.times[idx]));
}
size_t timer_get_us(int idx)
{
if (idx > TIMER_MAX_PARTIAL) {
return -1;
}
struct timespec ts = {0};
if (idx < 0) {
ts = timer.stop;
} else {
ts = timer.times[idx];
}
ts.tv_sec -= timer.start.tv_sec;
ts.tv_nsec -= timer.start.tv_nsec;
// FIXME: check overflow
return (ts.tv_nsec / 1000) + (ts.tv_sec * 1000000);
}
double timer_get_sec(int idx)
{
if (idx > TIMER_MAX_PARTIAL) {
return -1;
}
struct timespec ts = {0};
if (idx < 0) {
ts = timer.stop;
} else {
ts = timer.times[idx];
}
ts.tv_sec -= timer.start.tv_sec;
ts.tv_nsec -= timer.start.tv_nsec;
return (double)ts.tv_sec + ((double)ts.tv_nsec / 1e9);
}

16
timer.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef UG_TIMER_H_
#define UG_TIMER_H_
#include <stdlib.h>
#define TIMER_MAX_PARTIAL 10
int timer_start(void);
int timer_stop(void);
int timer_reset(void);
int timer_partial(int idx);
size_t timer_get_us(int idx);
double timer_get_sec(int idx);
#endif

1061
ugui.c Normal file

File diff suppressed because it is too large Load Diff

105
ugui.h Normal file
View File

@ -0,0 +1,105 @@
#ifndef _UGUI_H
#define _UGUI_H
#include <stdint.h>
typedef struct {
int32_t x, y, w, h;
} UgRect;
typedef struct {
int32_t x, y;
} UgPoint;
typedef struct {
uint8_t r, g, b, a;
} UgColor;
typedef uint64_t UgId;
typedef enum {
ETYPE_NONE = 0,
ETYPE_DIV,
ETYPE_BUTTON,
} UgElemType;
enum UgElemFlags {
ELEM_UPDATED = 1 << 0,
ELEM_HASFOCUS = 1 << 1,
};
enum UgElemEvent {
EVENT_KEY_PRESS = 1 << 0,
EVENT_KEY_RELEASE = 1 << 1,
EVENT_KEY_HOLD = 1 << 2,
EVENT_MOUSE_HOVER = 1 << 3,
EVENT_MOUSE_PRESS = 1 << 4,
EVENT_MOUSE_RELEASE = 1 << 5,
EVENT_MOUSE_HOLD = 1 << 6,
};
typedef struct {
UgId id;
uint32_t flags;
uint32_t event;
UgRect rect;
UgElemType type;
// type-specific fields
union {
struct UgDiv {
enum {
DIV_LAYOUT_ROW = 0,
DIV_LAYOUT_COLUMN,
DIV_LAYOUT_FLOATING,
} layout;
UgPoint origin_r, origin_c;
UgColor color_bg;
} div; // Div
};
} UgElem;
// TODO: add a packed flag
// TODO: add a fill index to skip some searching for free spots
typedef struct {
int size, elements;
UgId *vector; // vector of element ids
int *refs, *ordered_refs;
} UgTree;
typedef struct {
struct _IdTable *table;
UgElem *array;
uint64_t *present, *used;
int cycles;
} UgElemCache;
typedef struct _UgCtx UgCtx;
// tree implementation
int ug_tree_init(UgTree *tree, unsigned int size);
int ug_tree_pack(UgTree *tree);
int ug_tree_resize(UgTree *tree, unsigned int newsize);
int ug_tree_add(UgTree *tree, UgId elem, int parent);
int ug_tree_prune(UgTree *tree, int ref);
int ug_tree_subtree_size(UgTree *tree, int ref);
int ug_tree_children_it(UgTree *tree, int parent, int *cursor);
int ug_tree_level_order_it(UgTree *tree, int ref, int *cursor);
int ug_tree_parentof(UgTree *tree, int node);
int ug_tree_destroy(UgTree *tree);
UgId ug_tree_get(UgTree *tree, int node);
// cache implementation
UgElemCache ug_cache_init(void);
void ug_cache_free(UgElemCache *cache);
UgElem *ug_cache_search(UgElemCache *cache, UgId id);
UgElem *ug_cache_insert_new(UgElemCache *cache, const UgElem *g, uint32_t *index);
int ug_init(UgCtx *ctx);
int ug_destroy(UgCtx *ctx);
int ug_frame_begin(UgCtx *ctx);
int ug_frame_end(UgCtx *ctx);
#endif // _UGUI_H

355
vectree.c Normal file
View File

@ -0,0 +1,355 @@
#include <stdlib.h>
#include <string.h>
#include "ugui.h"
#define IS_VALID_REF(t, r) ((r) >= 0 && (r) < (t)->size)
#define REF_IS_PRESENT(t, r) ((t)->refs[r] >= 0)
int ug_tree_init(UgTree *tree, unsigned int size)
{
if (tree == NULL) {
return -1;
}
tree->vector = malloc(sizeof(UgId) * size);
if (tree->vector == NULL) {
return -1;
}
tree->refs = malloc(sizeof(int) * size);
if (tree->refs == NULL) {
free(tree->vector);
return -1;
}
// ordered refs are used in the iterators
tree->ordered_refs = malloc(sizeof(int) * (size + 1));
if (tree->ordered_refs == NULL) {
free(tree->vector);
free(tree->refs);
return -1;
}
// set all refs to -1, meaning invalid (free) element
for (unsigned int i = 0; i < size; i++) {
tree->refs[i] = -1;
}
// fill vector with zeroes
memset(tree->vector, 0, size * sizeof(UgId));
tree->size = size;
tree->elements = 0;
return 0;
}
int ug_tree_destroy(UgTree *tree)
{
if (tree == NULL) {
return -1;
}
free(tree->vector);
free(tree->refs);
return 0;
}
int ug_tree_pack(UgTree *tree)
{
if (tree == NULL) {
return -1;
}
// TODO: add a PACKED flag to skip this
int free_spot = -1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == -1) {
free_spot = i;
continue;
}
// find a item that can be packed
if (free_spot >= 0 && tree->refs[i] >= 0) {
int old_ref = i;
// move the item
tree->vector[free_spot] = tree->vector[i];
tree->refs[free_spot] = tree->refs[i];
tree->vector[i] = 0;
tree->refs[i] = -1;
// and move all references
for (int x = 0; x < tree->size; x++) {
if (tree->refs[x] == old_ref) {
tree->refs[x] = free_spot;
}
}
// mark the free spot as used
free_spot = -1;
}
}
return 0;
}
int ug_tree_resize(UgTree *tree, unsigned int newsize)
{
if (tree == NULL) {
return -1;
}
// return error when shrinking with too many elements
if ((int)newsize < tree->elements) {
return -1;
}
// pack the vector when shrinking to avoid data loss
if ((int)newsize < tree->size) {
// if (ug_tree_pack(tree) < 0) {
// return -1;
// }
// TODO: allow shrinking, since packing destroys all references
return -1;
}
UgId *newvec = realloc(tree->vector, newsize * sizeof(UgId));
if (newvec == NULL) {
return -1;
}
int *newrefs = realloc(tree->refs, newsize * sizeof(int));
if (newrefs == NULL) {
return -1;
}
int *neworrefs = realloc(tree->ordered_refs, (newsize + 1) * sizeof(int));
if (neworrefs == NULL) {
return -1;
}
tree->vector = newvec;
tree->refs = newrefs;
tree->ordered_refs = neworrefs;
if ((int)newsize > tree->size) {
for (int i = tree->size; i < (int)newsize; i++) {
tree->vector[i] = 0;
tree->refs[i] = -1;
}
}
tree->size = newsize;
return 0;
}
// add an element to the tree, return it's ref
int ug_tree_add(UgTree *tree, UgId elem, int parent)
{
if (tree == NULL) {
return -1;
}
// invalid parent
if (!IS_VALID_REF(tree, parent)) {
return -1;
}
// no space left
if (tree->elements >= tree->size) {
return -1;
}
// check if the parent exists
// if there are no elements in the tree the first add will set the root
if (!REF_IS_PRESENT(tree, parent) && tree->elements != 0) {
return -1;
}
// get the first free spot
int free_spot = -1;
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == -1) {
free_spot = i;
break;
}
}
if (free_spot < 0) {
return -1;
}
// finally add the element
tree->vector[free_spot] = elem;
tree->refs[free_spot] = parent;
tree->elements++;
return free_spot;
}
// prune the tree starting from the ref
// returns the number of pruned elements
int ug_tree_prune(UgTree *tree, int ref)
{
if (tree == NULL) {
return -1;
}
if (!IS_VALID_REF(tree, ref)) {
return -1;
}
if (!REF_IS_PRESENT(tree, ref)) {
return 0;
}
tree->vector[ref] = 0;
tree->refs[ref] = -1;
tree->elements--;
int count = 1;
for (int i = 0; tree->elements > 0 && i < tree->size; i++) {
if (tree->refs[i] == ref) {
count += ug_tree_prune(tree, i);
}
}
return count;
}
// find the size of the subtree starting from ref
int ug_tree_subtree_size(UgTree *tree, int ref)
{
if (tree == NULL) {
return -1;
}
if (!IS_VALID_REF(tree, ref)) {
return -1;
}
if (!REF_IS_PRESENT(tree, ref)) {
return 0;
}
int count = 1;
for (int i = 0; i < tree->size; i++) {
// only root has the reference to itself
if (tree->refs[i] == ref && ref != i) {
count += ug_tree_subtree_size(tree, i);
}
}
return count;
}
// iterate through the first level children, use a cursor like strtok_r
int ug_tree_children_it(UgTree *tree, int parent, int *cursor)
{
if (tree == NULL || cursor == NULL) {
return -1;
}
// if the cursor is out of bounds then we are done for sure
if (!IS_VALID_REF(tree, *cursor)) {
return -1;
}
// same for the parent, if it's invalid it can't have children
if (!IS_VALID_REF(tree, parent) || !REF_IS_PRESENT(tree, parent)) {
return -1;
}
// find the first child, update the cursor and return the ref
for (int i = *cursor; i < tree->size; i++) {
if (tree->refs[i] == parent) {
*cursor = i + 1;
return i;
}
}
// if no children are found return -1
*cursor = -1;
return -1;
}
/* iterates trough every leaf of the subtree in the following manner
* node [x], x: visit order
* [0]
* / | \
* / [2] [3]
* [1] |
* / \ [6]
* [4] [5]
*/
int ug_tree_level_order_it(UgTree *tree, int ref, int *cursor)
{
if (tree == NULL || cursor == NULL) {
return -1;
}
int *queue = tree->ordered_refs;
// TODO: this could also be done when adding or removing elements
// first call, create a ref array ordered like we desire
if (*cursor == -1) {
*cursor = 0;
for (int i = 0; i < tree->size; i++) {
queue[i] = -1;
}
// iterate through the queue appending found children
int pos = 0, off = 0;
do {
// printf ("ref=%d\n", ref);
for (int i = 0; i < tree->size; i++) {
if (tree->refs[i] == ref) {
queue[pos++] = i;
}
}
for (; ref == queue[off] && off < tree->size; off++)
;
ref = queue[off];
} while (IS_VALID_REF(tree, ref));
// This line is why tree->ordered_refs has to be size+1
queue[off + 1] = -1;
}
// PRINT_ARR(queue, tree->size);
// return -1;
// on successive calls just iterate through the queue until we find an
// invalid ref, if the user set the cursor to -1 it means it has found what
// he needed, so free
if (*cursor < 0) {
return -1;
} else if (IS_VALID_REF(tree, *cursor)) {
return queue[(*cursor)++];
}
return -1;
}
int ug_tree_parentof(UgTree *tree, int node)
{
if (tree == NULL || !IS_VALID_REF(tree, node) ||
!REF_IS_PRESENT(tree, node)) {
return -1;
}
return tree->refs[node];
}
UgId ug_tree_get(UgTree *tree, int node)
{
if (tree == NULL || !IS_VALID_REF(tree, node)) {
return 0;
}
return tree->vector[node];
}