Compare commits
4 Commits
c3
...
correct-pa
| Author | SHA1 | Date | |
|---|---|---|---|
| f409a67130 | |||
| d0a8e8a1ff | |||
| fa334ed154 | |||
| b35bb427a7 |
43
.ecode/project_build.json
Normal file
43
.ecode/project_build.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"Debug": {
|
||||||
|
"build": [
|
||||||
|
{
|
||||||
|
"args": "-C resources/shaders",
|
||||||
|
"command": "make",
|
||||||
|
"working_dir": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": "build -g",
|
||||||
|
"command": "c3c",
|
||||||
|
"working_dir": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build_types": [],
|
||||||
|
"clean": [
|
||||||
|
{
|
||||||
|
"args": "clean",
|
||||||
|
"command": "c3c",
|
||||||
|
"working_dir": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"clear_sys_env": false
|
||||||
|
},
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"output_parser": {
|
||||||
|
"config": {
|
||||||
|
"relative_file_paths": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"run": [
|
||||||
|
{
|
||||||
|
"args": "",
|
||||||
|
"command": "build/ugui",
|
||||||
|
"name": "Custom Executable",
|
||||||
|
"working_dir": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
16
.gitmodules
vendored
16
.gitmodules
vendored
@ -0,0 +1,16 @@
|
|||||||
|
[submodule "lib/grapheme.c3l/thirdparty/libgrapheme"]
|
||||||
|
path = lib/grapheme.c3l/thirdparty/libgrapheme
|
||||||
|
url = git://git.suckless.org/libgrapheme
|
||||||
|
ignore = dirty
|
||||||
|
[submodule "lib/schrift.c3l/thirdparty/libschrift"]
|
||||||
|
path = lib/schrift.c3l/thirdparty/libschrift
|
||||||
|
url = https://github.com/tomolt/libschrift
|
||||||
|
ignore = dirty
|
||||||
|
[submodule "lib/sdl3.c3l"]
|
||||||
|
path = lib/sdl3.c3l
|
||||||
|
url = https://git.alemauri.eu/alema/sdl3.c3l
|
||||||
|
ignore = dirty
|
||||||
|
[submodule "lib/vendor"]
|
||||||
|
path = lib/vendor
|
||||||
|
url = https://github.com/c3lang/vendor
|
||||||
|
ignore = dirty
|
||||||
184
ARCHITECTURE.md
Normal file
184
ARCHITECTURE.md
Normal 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
243
LAYOUT
@ -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
165
LICENSE
@ -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.
|
|
||||||
3
Makefile
Normal file
3
Makefile
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
test_renderer: test_renderer.c3 src/renderer.c3 resources/shaders/source/*
|
||||||
|
scripts/compile_shaders.sh
|
||||||
|
c3c compile -g -O0 test_renderer.c3 src/renderer.c3 --libdir lib --lib sdl3 --lib ugui
|
||||||
72
TODO
72
TODO
@ -4,22 +4,21 @@
|
|||||||
[x] Implement div.view and scrollbars
|
[x] Implement div.view and scrollbars
|
||||||
[x] Port font system from C to C3 (rewrite1)
|
[x] Port font system from C to C3 (rewrite1)
|
||||||
[ ] Update ARCHITECTURE.md
|
[ ] Update ARCHITECTURE.md
|
||||||
[x] Write a README.md
|
[ ] Write a README.md
|
||||||
[x] Use an arena allocator for cache
|
[ ] Use an arena allocator for cache
|
||||||
[ ] Do not redraw if there was no update (no layout and no draw)
|
[ ] 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)
|
[ ] 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] 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
|
[x] Clip element bounds to parent div, specifically text
|
||||||
[ ] Resizeable divs
|
[ ] Resizeable divs
|
||||||
[x] Implement a z index and sort command buffer based on that
|
[x] Implement a z index and sort command buffer based on that
|
||||||
[ ] Ctx.set_z_index()
|
[ ] Ctx.set_z_index()
|
||||||
[x] Sort command buffer on insertion
|
[x] Sort command buffer on insertion
|
||||||
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot
|
[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot of boiler plate and reuse it
|
||||||
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
|
||||||
[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
|
[ ] Animations, somehow
|
||||||
[x] Maybe cache codepoint converted strings
|
[ ] Maybe cache codepoint converted strings
|
||||||
[x] Fix scroll wheel when div is scrolled
|
[x] Fix scroll wheel when div is scrolled
|
||||||
[ ] Be consistent with the initialization methods some are foo.new() and some are foo.init()
|
[ ] 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
|
[ ] Implement image loading (.bmp, .ff, .qoi and .png), in the future even lossy images like .jpg
|
||||||
@ -29,52 +28,31 @@
|
|||||||
[ ] .png
|
[ ] .png
|
||||||
[ ] .jpg
|
[ ] .jpg
|
||||||
[ ] gif support?
|
[ ] gif support?
|
||||||
[x] layout_set_max_rows() and layout_set_max_columns()
|
[ ] layout_set_max_rows() and layout_set_max_columns()
|
||||||
[x] Maybe SDF sprites??
|
[x] Maybe SDF sprites??
|
||||||
[x] Stylesheets and stylesheet import
|
[x] Stylesheets and stylesheet import
|
||||||
[x] use SDF to draw anti-aliased rounded rectangles https://zed.dev/blog/videogame
|
[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
|
[ ] Subdivide modules into ugui::ug for exported functions and ugui::core for
|
||||||
internal use functions (used to create widgets)
|
internal use functions (used to create widgets)
|
||||||
[x] The render loop RAPES the gpu, valve pls fix
|
[x] The render loop RAPES the gpu, valve pls fix
|
||||||
[x] The way the element structures are implemented wastes a lot of memory since
|
[ ] 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
|
each struct Elem, struct Cmd, etc. is as big as the largest element. It would
|
||||||
be better to use a different allcation strategy.
|
be better to use a different allcation strategy.
|
||||||
[ ] Add a way to handle time events like double clicks
|
[ ] 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
|
[ ] Border and padding do not go well together if the library issues two rect commands, the visible
|
||||||
content, the background color is applied starting from the border. Right now push_rect() offsets
|
border is effectively the border size plus the padding since there is a gap between the border
|
||||||
the background rect by both border and padding
|
rect and the internal rect. A better solution is to leave it up to the renderer to draw the rect
|
||||||
[x] Investigate why the debug pointer (cyan rectangle) disappears...
|
correctly
|
||||||
[ ] Selectable divs
|
|
||||||
[ ] Selectable text
|
|
||||||
[ ] Copy buffer
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
[x] Flexbox
|
[x] Flexbox
|
||||||
[x] Center elements to the row/column
|
[ ] For some reason padding is not correct, look at the sliders, they have 2px per side when the
|
||||||
[x] Text wrapping / reflow
|
theme specifies 4px per side
|
||||||
[x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct
|
[ ] Center elements to the row/column
|
||||||
[x] Correct whitespace handling in text (\t \r etc)
|
[ ] Text wrapping / reflow
|
||||||
[x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
|
[ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
|
||||||
instead of the curren multi-frame approach.
|
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
|
|
||||||
[ ] Grids
|
|
||||||
|
|
||||||
## Input
|
## Input
|
||||||
|
|
||||||
@ -82,9 +60,7 @@
|
|||||||
[x] Mouse scroll wheel
|
[x] Mouse scroll wheel
|
||||||
[ ] Touch input
|
[ ] Touch input
|
||||||
[x] Do not set input event to true if the movement was zero (like no mouse movement)
|
[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
|
[ ] 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
|
## Commands
|
||||||
|
|
||||||
@ -92,10 +68,9 @@
|
|||||||
- border width
|
- border width
|
||||||
- border radius
|
- border radius
|
||||||
[x] add a command to update an atlas
|
[x] add a command to update an atlas
|
||||||
[x] New window command, useful for popups
|
[ ] New window command, useful for popups
|
||||||
[x] Text command returns the text bounds, this way we can avoid the pattern
|
[ ] 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) -> ...
|
draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ...
|
||||||
[ ] Rounded rectangle with different radius for each corner
|
|
||||||
|
|
||||||
## Atlas
|
## Atlas
|
||||||
|
|
||||||
@ -118,6 +93,11 @@
|
|||||||
[x] Checkbox
|
[x] Checkbox
|
||||||
[ ] Selectable text box
|
[ ] Selectable text box
|
||||||
|
|
||||||
|
## Main / exaple
|
||||||
|
|
||||||
|
[ ] Create maps from ids to textures and images instead of hardcoding them
|
||||||
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
[ ] Introduce a Layout structure that specifies the positioning of elements inside
|
[ ] Introduce a Layout structure that specifies the positioning of elements inside
|
||||||
|
|||||||
0
lib/.gitkeep
Normal file
0
lib/.gitkeep
Normal file
4
lib/grapheme.c3l/Makefile
Normal file
4
lib/grapheme.c3l/Makefile
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
all:
|
||||||
|
make -C thirdparty/libgrapheme
|
||||||
|
mkdir -p linux-x64
|
||||||
|
cp thirdparty/libgrapheme/libgrapheme.a linux-x64/libgrapheme.a
|
||||||
46
lib/grapheme.c3l/libgrapheme.c3i
Normal file
46
lib/grapheme.c3l/libgrapheme.c3i
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
module grapheme;
|
||||||
|
|
||||||
|
const uint GRAPHEME_INVALID_CODEPOINT = 0xFFFD;
|
||||||
|
|
||||||
|
enum BidirectionalDirection {
|
||||||
|
GRAPHEME_BIDIRECTIONAL_DIRECTION_NEUTRAL,
|
||||||
|
GRAPHEME_BIDIRECTIONAL_DIRECTION_LTR,
|
||||||
|
GRAPHEME_BIDIRECTIONAL_DIRECTION_RTL,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isz bidirectional_get_line_embedding_levels(uint *, isz, ichar *, isz) @extern("grapheme_bidirectional_get_line_embedding_levels");
|
||||||
|
|
||||||
|
fn isz bidirectional_preprocess_paragraph(uint *, isz, BidirectionalDirection, uint *, isz, BidirectionalDirection *) @extern("grapheme_bidirectional_preprocess_paragraph");
|
||||||
|
|
||||||
|
fn isz bidirectional_reorder_line(uint *, uint *, isz, uint *, isz) @extern("grapheme_bidirectional_reorder_line");
|
||||||
|
|
||||||
|
fn isz decode_utf8(char *, isz, uint *) @extern("grapheme_decode_utf8");
|
||||||
|
fn isz encode_utf8(uint, char *, isz) @extern("grapheme_encode_utf8");
|
||||||
|
|
||||||
|
fn bool is_character_break(uint, uint, ushort *) @extern("grapheme_is_character_break");
|
||||||
|
|
||||||
|
fn bool is_lowercase(uint *, isz, isz *) @extern("grapheme_is_lowercase");
|
||||||
|
fn bool is_titlecase(uint *, isz, isz *) @extern("grapheme_is_titlecase");
|
||||||
|
fn bool is_uppercase(uint *, isz, isz *) @extern("grapheme_is_uppercase");
|
||||||
|
|
||||||
|
fn bool is_lowercase_utf8(char *, isz, isz *) @extern("grapheme_is_lowercase_utf8");
|
||||||
|
fn bool is_titlecase_utf8(char *, isz, isz *) @extern("grapheme_is_titlecase_utf8");
|
||||||
|
fn bool is_uppercase_utf8(char *, isz, isz *) @extern("grapheme_is_uppercase_utf8");
|
||||||
|
|
||||||
|
fn isz next_character_break(uint *, isz) @extern("grapheme_next_character_break");
|
||||||
|
fn isz next_line_break(uint *, isz) @extern("grapheme_next_line_break");
|
||||||
|
fn isz next_sentence_break(uint *, isz) @extern("grapheme_next_sentence_break");
|
||||||
|
fn isz next_word_break(uint *, isz) @extern("grapheme_next_word_break");
|
||||||
|
|
||||||
|
fn isz next_character_break_utf8(char *, isz) @extern("grapheme_next_character_break_utf8");
|
||||||
|
fn isz next_line_break_utf8(char *, isz) @extern("grapheme_next_line_break_utf8");
|
||||||
|
fn isz next_sentence_break_utf8(char *, isz) @extern("grapheme_next_sentence_break_utf8");
|
||||||
|
fn isz next_word_break_utf8(char *, isz) @extern("grapheme_next_word_break_utf8");
|
||||||
|
|
||||||
|
fn isz to_lowercase(uint *, isz, uint *, isz) @extern("grapheme_to_lowercase");
|
||||||
|
fn isz to_titlecase(uint *, isz, uint *, isz) @extern("grapheme_to_titlecase");
|
||||||
|
fn isz to_uppercase(uint *, isz, uint *, isz) @extern("grapheme_to_uppercase");
|
||||||
|
|
||||||
|
fn isz to_lowercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_lowercase_utf8");
|
||||||
|
fn isz to_titlecase_utf8(char *, isz, char *, isz) @extern("grapheme_to_titlecase_utf8");
|
||||||
|
fn isz to_uppercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_uppercase_utf8");
|
||||||
9
lib/grapheme.c3l/manifest.json
Normal file
9
lib/grapheme.c3l/manifest.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"provides": "grapheme",
|
||||||
|
"targets": {
|
||||||
|
"linux-x64": {
|
||||||
|
"dependencies": [],
|
||||||
|
"linked-libraries": ["grapheme", "c"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/grapheme.c3l/project.json
Normal file
14
lib/grapheme.c3l/project.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"langrev": "1",
|
||||||
|
"warnings": ["no-unused"],
|
||||||
|
"dependency-search-paths": [".."],
|
||||||
|
"dependencies": ["grapheme"],
|
||||||
|
"authors": ["Alessandro Mauri <alemauri001@gmail.com>", "Laslo Hunhold <dev@frign.de>"],
|
||||||
|
"version": "0.1.0",
|
||||||
|
"sources": [],
|
||||||
|
"output": "build",
|
||||||
|
"target": "linux-x64",
|
||||||
|
"features": [],
|
||||||
|
"cpu": "generic",
|
||||||
|
"opt": "O0"
|
||||||
|
}
|
||||||
1
lib/grapheme.c3l/thirdparty/libgrapheme
vendored
Submodule
1
lib/grapheme.c3l/thirdparty/libgrapheme
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 65b354f0fcb1d925f4340dbb4415ea06e8af2bec
|
||||||
4
lib/schrift.c3l/Makefile
Normal file
4
lib/schrift.c3l/Makefile
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
all:
|
||||||
|
make -C thirdparty/libschrift
|
||||||
|
mkdir -p linux-x64
|
||||||
|
cp thirdparty/libschrift/libschrift.a linux-x64/libschrift.a
|
||||||
58
lib/schrift.c3l/libschrift.c3
Normal file
58
lib/schrift.c3l/libschrift.c3
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
module schrift;
|
||||||
|
|
||||||
|
alias SftFont = void*;
|
||||||
|
alias SftUChar = uint;
|
||||||
|
alias SftGlyph = uint;
|
||||||
|
|
||||||
|
const int SFT_DOWNWARD_Y = 0x01;
|
||||||
|
|
||||||
|
struct Sft
|
||||||
|
{
|
||||||
|
SftFont font;
|
||||||
|
double xScale;
|
||||||
|
double yScale;
|
||||||
|
double xOffset;
|
||||||
|
double yOffset;
|
||||||
|
int flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SftLMetrics
|
||||||
|
{
|
||||||
|
double ascender;
|
||||||
|
double descender;
|
||||||
|
double lineGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SftGMetrics
|
||||||
|
{
|
||||||
|
double advanceWidth;
|
||||||
|
double leftSideBearing;
|
||||||
|
int yOffset;
|
||||||
|
int minWidth;
|
||||||
|
int minHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SftKerning
|
||||||
|
{
|
||||||
|
double xShift;
|
||||||
|
double yShift;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SftImage
|
||||||
|
{
|
||||||
|
void *pixels;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern fn ZString sft_version() @extern("sft_version");
|
||||||
|
|
||||||
|
extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem");
|
||||||
|
extern fn SftFont loadfile(ZString filename) @extern("sft_loadfile");
|
||||||
|
extern fn void freefont(SftFont font) @extern("sft_freefont");
|
||||||
|
|
||||||
|
extern fn int lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics");
|
||||||
|
extern fn int lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup");
|
||||||
|
extern fn int gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics");
|
||||||
|
extern fn int kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning");
|
||||||
|
extern fn int render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render");
|
||||||
9
lib/schrift.c3l/manifest.json
Normal file
9
lib/schrift.c3l/manifest.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"provides" : "schrift",
|
||||||
|
"targets" : {
|
||||||
|
"linux-x64" : {
|
||||||
|
"dependencies" : [],
|
||||||
|
"linked-libraries" : ["schrift", "c"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/schrift.c3l/project.json
Normal file
14
lib/schrift.c3l/project.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"langrev": "1",
|
||||||
|
"warnings": [ "no-unused" ],
|
||||||
|
"dependency-search-paths": [ ".." ],
|
||||||
|
"dependencies": [ "schrift" ],
|
||||||
|
"authors": [ "Alessandro Mauri <alemauri001@gmail.com>", "Thomas Oltmann <thomas.oltmann.hhg@gmail.com>" ],
|
||||||
|
"version": "0.1.0",
|
||||||
|
"sources": [ ],
|
||||||
|
"output": "build",
|
||||||
|
"target": "linux-x64",
|
||||||
|
"features": [],
|
||||||
|
"cpu": "generic",
|
||||||
|
"opt": "O0",
|
||||||
|
}
|
||||||
1
lib/schrift.c3l/thirdparty/libschrift
vendored
Submodule
1
lib/schrift.c3l/thirdparty/libschrift
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 24737d2922b23df4a5692014f5ba03da0c296112
|
||||||
1
lib/sdl3.c3l
Submodule
1
lib/sdl3.c3l
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d
|
||||||
0
lib/ugui.c3l/LICENSE
Normal file
0
lib/ugui.c3l/LICENSE
Normal file
1
lib/ugui.c3l/README.md
Normal file
1
lib/ugui.c3l/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Welcome to the ugui library.
|
||||||
0
lib/ugui.c3l/linux-x64/.gitkeep
Normal file
0
lib/ugui.c3l/linux-x64/.gitkeep
Normal file
@ -4,7 +4,7 @@
|
|||||||
"targets" : {
|
"targets" : {
|
||||||
"linux-x64" : {
|
"linux-x64" : {
|
||||||
"link-args" : [],
|
"link-args" : [],
|
||||||
"dependencies" : ["schrift"],
|
"dependencies" : ["schrift", "grapheme"],
|
||||||
"linked-libraries" : []
|
"linked-libraries" : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0
lib/ugui.c3l/scripts/.gitkeep
Normal file
0
lib/ugui.c3l/scripts/.gitkeep
Normal file
@ -9,6 +9,10 @@ module cache{Key, Value, SIZE};
|
|||||||
* the elements that were not recently used.
|
* the elements that were not recently used.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// FIXME: this module should really allocate all resources on an arena or temp
|
||||||
|
// allocator, since all memory allocations are connected and freeing
|
||||||
|
// happens at the same time
|
||||||
|
|
||||||
import std::core::mem;
|
import std::core::mem;
|
||||||
import std::core::mem::allocator;
|
import std::core::mem::allocator;
|
||||||
import std::collections::bitset;
|
import std::collections::bitset;
|
||||||
@ -21,7 +25,6 @@ alias IdTableEntry = map::Entry{Key, usz};
|
|||||||
const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0);
|
const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0);
|
||||||
|
|
||||||
struct Cache {
|
struct Cache {
|
||||||
Allocator allocator;
|
|
||||||
BitArr present, used;
|
BitArr present, used;
|
||||||
IdTable table;
|
IdTable table;
|
||||||
Value[] pool;
|
Value[] pool;
|
||||||
@ -40,20 +43,19 @@ macro Cache.cycle(&cache) @private {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn void? Cache.init(&cache, Allocator allocator)
|
fn void? Cache.init(&cache)
|
||||||
{
|
{
|
||||||
cache.allocator = allocator;
|
cache.table.init(allocator::heap(), capacity: SIZE);
|
||||||
cache.table.init(allocator, capacity: SIZE);
|
|
||||||
// FIXME: this shit is SLOW
|
// FIXME: this shit is SLOW
|
||||||
foreach (idx, bit : cache.used) { cache.used[idx] = false; }
|
foreach (idx, bit : cache.used) { cache.used[idx] = false; }
|
||||||
foreach (idx, bit : cache.present) { cache.present[idx] = false; }
|
foreach (idx, bit : cache.present) { cache.present[idx] = false; }
|
||||||
cache.pool = allocator::new_array(allocator, Value, SIZE);
|
cache.pool = mem::new_array(Value, SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn void Cache.free(&cache)
|
fn void Cache.free(&cache)
|
||||||
{
|
{
|
||||||
(void)cache.table.free();
|
(void)cache.table.free();
|
||||||
(void)allocator::free(cache.allocator, cache.pool);
|
(void)mem::free(cache.pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Value*? Cache.search(&cache, Key id)
|
fn Value*? Cache.search(&cache, Key id)
|
||||||
@ -75,7 +77,6 @@ fn Value*? Cache.search(&cache, Key id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* HIT, set as recently used */
|
/* HIT, set as recently used */
|
||||||
//io::printfn("HIT: %d [%d]", entry.value, entry.key);
|
|
||||||
cache.used[entry.value] = true;
|
cache.used[entry.value] = true;
|
||||||
return &(cache.pool[entry.value]);
|
return &(cache.pool[entry.value]);
|
||||||
}
|
}
|
||||||
@ -96,19 +97,16 @@ fn void Cache.remove(&cache, Key id)
|
|||||||
/* If there is no free space left then just return the first position */
|
/* If there is no free space left then just return the first position */
|
||||||
fn usz Cache.get_free_spot(&cache) @private
|
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;
|
const BITS = $typeof(cache.present.data[0]).sizeof*8;
|
||||||
foreach (idx, d: cache.present.data) {
|
foreach (idx, d: cache.present.data) {
|
||||||
if (d != $typeof(d).max) {
|
if (d.clz() != BITS) {
|
||||||
usz spot = idx*BITS + BITS-d.clz();
|
return idx*BITS + BITS-d.clz();
|
||||||
if (cache.used[spot]) unreachable("free spot is not actually free: %d", spot);
|
|
||||||
return spot;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Value*? Cache.insert_at(&cache, Value* g, Key id, usz index) @private
|
fn Value*? Cache.insert_at(&cache, Value *g, Key id, usz index) @private
|
||||||
{
|
{
|
||||||
// TODO: verify index, g and id
|
// TODO: verify index, g and id
|
||||||
Value* spot;
|
Value* spot;
|
||||||
91
lib/ugui.c3l/src/fifo.c3
Normal file
91
lib/ugui.c3l/src/fifo.c3
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
module fifo::faults;
|
||||||
|
faultdef FULL, EMPTY;
|
||||||
|
|
||||||
|
module fifo{Type};
|
||||||
|
|
||||||
|
import std::core::mem;
|
||||||
|
import std::sort;
|
||||||
|
|
||||||
|
// TODO: specify the allocator
|
||||||
|
|
||||||
|
struct Fifo {
|
||||||
|
Type[] arr;
|
||||||
|
usz out;
|
||||||
|
usz count;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Fifo.init(&fifo, usz size)
|
||||||
|
{
|
||||||
|
fifo.arr = mem::new_array(Type, size);
|
||||||
|
fifo.out = 0;
|
||||||
|
fifo.count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Fifo.free(&fifo)
|
||||||
|
{
|
||||||
|
(void)mem::free(fifo.arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Fifo.enqueue(&fifo, Type *elem)
|
||||||
|
{
|
||||||
|
if (fifo.count >= fifo.arr.len) {
|
||||||
|
return fifo::faults::FULL?;
|
||||||
|
}
|
||||||
|
usz in = (fifo.out + fifo.count) % fifo.arr.len;
|
||||||
|
fifo.arr[in] = *elem;
|
||||||
|
fifo.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Type*? Fifo.dequeue(&fifo)
|
||||||
|
{
|
||||||
|
if (fifo.count == 0) {
|
||||||
|
return fifo::faults::EMPTY?;
|
||||||
|
}
|
||||||
|
Type *ret = &fifo.arr[fifo.out];
|
||||||
|
fifo.count--;
|
||||||
|
fifo.out = (fifo.out + 1) % fifo.arr.len;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Type Fifo.get(&fifo, usz i) @operator([])
|
||||||
|
{
|
||||||
|
return fifo.arr[(fifo.out + i) % fifo.arr.len];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Fifo.set(&fifo, usz i, Type val) @operator([]=)
|
||||||
|
{
|
||||||
|
fifo.arr[(fifo.out + i) % fifo.arr.len] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Type* Fifo.get_ref(&fifo, usz i) @operator(&[])
|
||||||
|
{
|
||||||
|
return &fifo.arr[(fifo.out + i) % fifo.arr.len];
|
||||||
|
}
|
||||||
|
|
||||||
|
macro usz Fifo.len(&fifo) @operator(len)
|
||||||
|
{
|
||||||
|
return fifo.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Fifo.sort(&fifo)
|
||||||
|
{
|
||||||
|
Type[] arr = mem::new_array(Type, fifo.count);
|
||||||
|
defer mem::free(arr);
|
||||||
|
|
||||||
|
foreach(i, c: fifo) {
|
||||||
|
arr[i] = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't keep ordering
|
||||||
|
//sort::quicksort(arr);
|
||||||
|
|
||||||
|
// seems to keep the right order but we will never know...
|
||||||
|
// also since most things are already ordered the time is closer to O(n) than to O(n^2)
|
||||||
|
sort::insertionsort(arr);
|
||||||
|
|
||||||
|
fifo.count = 0;
|
||||||
|
fifo.out = 0;
|
||||||
|
foreach (&c: arr) {
|
||||||
|
fifo.enqueue(c)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
187
lib/ugui.c3l/src/ugui_button.c3
Normal file
187
lib/ugui.c3l/src/ugui_button.c3
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
module ugui;
|
||||||
|
|
||||||
|
import std::io;
|
||||||
|
|
||||||
|
// button element
|
||||||
|
struct ElemButton {
|
||||||
|
int filler;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// draw a button, return the events on that button
|
||||||
|
macro Ctx.button(&ctx, Rect size, bool state = false, ...)
|
||||||
|
=> ctx.button_id(@compute_id($vasplat), size, state);
|
||||||
|
fn ElemEvents? Ctx.button_id(&ctx, Id id, Rect size, bool active)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
|
||||||
|
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
|
||||||
|
|
||||||
|
elem.bounds = ctx.position_element(parent, size, style_norm);
|
||||||
|
|
||||||
|
// if the bounds are null the element is outside the div view,
|
||||||
|
// no interaction should occur so just return
|
||||||
|
if (elem.bounds.is_null()) { return {}; }
|
||||||
|
|
||||||
|
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
|
||||||
|
|
||||||
|
elem.events = ctx.get_elem_events(elem);
|
||||||
|
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
|
||||||
|
|
||||||
|
// Draw the button
|
||||||
|
ctx.push_rect(elem.bounds, parent.div.z_index, is_active ? style_active : style_norm)!;
|
||||||
|
|
||||||
|
return elem.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool active = false, ...)
|
||||||
|
=> ctx.button_label_id(@compute_id($vasplat), label, size, active);
|
||||||
|
fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool active)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
|
||||||
|
|
||||||
|
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||||
|
Rect text_size = ctx.get_text_bounds(label)!;
|
||||||
|
Rect btn_size = text_size.add({0,0,10,10});
|
||||||
|
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
|
||||||
|
|
||||||
|
// 2. Layout
|
||||||
|
elem.bounds = ctx.position_element(parent, btn_size, style_norm);
|
||||||
|
if (elem.bounds.is_null()) { return {}; }
|
||||||
|
|
||||||
|
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
|
||||||
|
|
||||||
|
elem.events = ctx.get_elem_events(elem);
|
||||||
|
|
||||||
|
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
|
||||||
|
Style* style = is_active ? style_active : style_norm;
|
||||||
|
|
||||||
|
// Draw the button
|
||||||
|
text_size.x = elem.bounds.x;
|
||||||
|
text_size.y = elem.bounds.y;
|
||||||
|
Point off = ctx.center_text(text_size, elem.bounds);
|
||||||
|
text_size.x += off.x;
|
||||||
|
text_size.y += off.y;
|
||||||
|
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
|
||||||
|
ctx.push_string(text_size, label, parent.div.z_index, style.fg)!;
|
||||||
|
|
||||||
|
return elem.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Ctx.button_icon(&ctx, String icon, String on_icon = "", bool active = false, ...)
|
||||||
|
=> ctx.button_icon_id(@compute_id($vasplat), icon, on_icon, active);
|
||||||
|
fn ElemEvents? Ctx.button_icon_id(&ctx, Id id, String icon, String on_icon, bool active)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
|
||||||
|
|
||||||
|
Sprite* def_sprite = ctx.sprite_atlas.get(icon)!;
|
||||||
|
Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){};
|
||||||
|
Rect max_size = def_sprite.rect().max(on_sprite.rect());
|
||||||
|
|
||||||
|
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
|
||||||
|
|
||||||
|
elem.bounds = ctx.position_element(parent, max_size, style_norm);
|
||||||
|
|
||||||
|
// if the bounds are null the element is outside the div view,
|
||||||
|
// no interaction should occur so just return
|
||||||
|
if (elem.bounds.is_null()) { return {}; }
|
||||||
|
|
||||||
|
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
|
||||||
|
|
||||||
|
elem.events = ctx.get_elem_events(elem);
|
||||||
|
|
||||||
|
bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
|
||||||
|
Style* style = is_active ? style_active : style_norm;
|
||||||
|
|
||||||
|
Id tex_id = ctx.sprite_atlas.id;
|
||||||
|
if (active && on_icon != "") {
|
||||||
|
ctx.push_sprite(elem.bounds, on_sprite.uv(), tex_id, parent.div.z_index, type: on_sprite.type)!;
|
||||||
|
} else {
|
||||||
|
ctx.push_sprite(elem.bounds, def_sprite.uv(), tex_id, parent.div.z_index, type: def_sprite.type)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the button
|
||||||
|
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
|
||||||
|
|
||||||
|
return elem.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: this should be inside the style
|
||||||
|
macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...)
|
||||||
|
=> ctx.checkbox_id(@compute_id($vasplat), desc, off, active, tick_sprite);
|
||||||
|
fn void? Ctx.checkbox_id(&ctx, Id id, String description, Point off, bool* active, String tick_sprite)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
|
||||||
|
Style* style = ctx.styles.get_style(@str_hash("checkbox"));
|
||||||
|
|
||||||
|
Rect size = {off.x, off.y, style.size, style.size};
|
||||||
|
elem.bounds = ctx.position_element(parent, size, style);
|
||||||
|
|
||||||
|
// if the bounds are null the element is outside the div view,
|
||||||
|
// no interaction should occur so just return
|
||||||
|
if (elem.bounds.is_null()) return;
|
||||||
|
|
||||||
|
elem.events = ctx.get_elem_events(elem);
|
||||||
|
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
|
||||||
|
|
||||||
|
if (tick_sprite != {}) {
|
||||||
|
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
|
||||||
|
if (*active) {
|
||||||
|
ctx.draw_sprite_raw(tick_sprite, elem.bounds, center: true)!;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
|
||||||
|
if (*active) {
|
||||||
|
ushort x = style.size / 4;
|
||||||
|
Rect check = elem.bounds.add({x, x, -x*2, -x*2});
|
||||||
|
Style s = *style;
|
||||||
|
s.bg = s.primary;
|
||||||
|
s.margin = s.padding = {};
|
||||||
|
s.border = 0;
|
||||||
|
ctx.push_rect(check, parent.div.z_index, &s)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: this should be inside the style
|
||||||
|
macro Ctx.toggle(&ctx, String desc, Point off, bool* active)
|
||||||
|
=> ctx.toggle_id(@compute_id($vasplat), desc, off, active);
|
||||||
|
fn void? Ctx.toggle_id(&ctx, Id id, String description, Point off, bool* active)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
|
||||||
|
Style* style = ctx.styles.get_style(@str_hash("toggle"));
|
||||||
|
|
||||||
|
Rect size = {off.x, off.y, style.size*2, style.size};
|
||||||
|
elem.bounds = ctx.position_element(parent, size, style);
|
||||||
|
|
||||||
|
// if the bounds are null the element is outside the div view,
|
||||||
|
// no interaction should occur so just return
|
||||||
|
if (elem.bounds.is_null()) return;
|
||||||
|
|
||||||
|
elem.events = ctx.get_elem_events(elem);
|
||||||
|
if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
|
||||||
|
|
||||||
|
// Draw the button
|
||||||
|
// FIXME: THIS IS SHIT
|
||||||
|
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
|
||||||
|
Rect t = elem.bounds.add({*active ? (style.size+3) : +3, +3, -style.size-6, -6});
|
||||||
|
Style s = *style;
|
||||||
|
s.bg = s.primary;
|
||||||
|
s.margin = s.padding = {};
|
||||||
|
s.border = 0;
|
||||||
|
ctx.push_rect(t, parent.div.z_index, &s)!;
|
||||||
|
}
|
||||||
209
lib/ugui.c3l/src/ugui_cmd.c3
Normal file
209
lib/ugui.c3l/src/ugui_cmd.c3
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
module ugui;
|
||||||
|
|
||||||
|
import std::ascii;
|
||||||
|
import std::io;
|
||||||
|
|
||||||
|
// command type
|
||||||
|
enum CmdType {
|
||||||
|
CMD_RECT,
|
||||||
|
CMD_UPDATE_ATLAS,
|
||||||
|
CMD_SPRITE,
|
||||||
|
CMD_SCISSOR,
|
||||||
|
}
|
||||||
|
|
||||||
|
// command to draw a rect
|
||||||
|
struct CmdRect {
|
||||||
|
Rect rect;
|
||||||
|
ushort thickness;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement the Printable interface
|
||||||
|
fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
|
||||||
|
{
|
||||||
|
return f.printf("Cmd{ type: %s, z_index: %d }", cmd.type, cmd.z_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.enqueue(cmd);
|
||||||
|
}
|
||||||
|
if (cull_rect(rect, ctx.div_scissor)) return;
|
||||||
|
return ctx.cmd_queue.enqueue(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.push_rect(&ctx, Rect rect, int z_index, Style* style)
|
||||||
|
{
|
||||||
|
Rect padding = style.padding;
|
||||||
|
ushort border = style.border;
|
||||||
|
ushort radius = style.radius;
|
||||||
|
Color bg = style.bg;
|
||||||
|
Color border_color = style.secondary;
|
||||||
|
|
||||||
|
if (border != 0) {
|
||||||
|
Cmd cmd = {
|
||||||
|
.type = CMD_RECT,
|
||||||
|
.rect.rect = rect,
|
||||||
|
.rect.color = border_color,
|
||||||
|
.rect.radius = radius+border,
|
||||||
|
.rect.thickness = border,
|
||||||
|
};
|
||||||
|
ctx.push_cmd(&cmd, z_index)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cmd cmd = {
|
||||||
|
.type = CMD_RECT,
|
||||||
|
.rect.rect = {
|
||||||
|
.x = rect.x + border + padding.x,
|
||||||
|
.y = rect.y + border + padding.y,
|
||||||
|
.h = rect.h - (border*2) - (padding.y+padding.h),
|
||||||
|
.w = rect.w - (border*2) - (padding.x+padding.w),
|
||||||
|
},
|
||||||
|
.rect.color = bg,
|
||||||
|
.rect.radius = radius,
|
||||||
|
.rect.thickness = max(rect.w, rect.h)/2+1,
|
||||||
|
};
|
||||||
|
if (cull_rect(cmd.rect.rect, ctx.div_scissor)) return;
|
||||||
|
ctx.push_cmd(&cmd, z_index)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_string(&ctx, Rect bounds, char[] text, int z_index, Color hue)
|
||||||
|
{
|
||||||
|
if (text.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.push_scissor(bounds, z_index)!;
|
||||||
|
|
||||||
|
short baseline = (short)ctx.font.ascender;
|
||||||
|
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||||
|
short line_gap = (short)ctx.font.linegap;
|
||||||
|
Id texture_id = ctx.font.id; // or ctx.font.atlas.id
|
||||||
|
Point orig = {
|
||||||
|
.x = bounds.x,
|
||||||
|
.y = bounds.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
short line_len;
|
||||||
|
Codepoint cp;
|
||||||
|
usz off, x;
|
||||||
|
while (off < text.len && (cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||||
|
off += x;
|
||||||
|
Glyph* gp;
|
||||||
|
if (!ascii::is_cntrl((char)cp)) {
|
||||||
|
gp = ctx.font.get_glyph(cp)!;
|
||||||
|
Rect gb = {
|
||||||
|
.x = orig.x + line_len + gp.ox,
|
||||||
|
.y = orig.y + gp.oy + baseline,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h,
|
||||||
|
};
|
||||||
|
Rect gt = {
|
||||||
|
.x = gp.u,
|
||||||
|
.y = gp.v,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h,
|
||||||
|
};
|
||||||
|
// push the sprite only if it collides with the bounds
|
||||||
|
if (!cull_rect(gb, bounds)) ctx.push_sprite(gb, gt, texture_id, z_index, hue)!;
|
||||||
|
line_len += gp.adv;
|
||||||
|
} else if (cp == '\n'){
|
||||||
|
orig.y += line_height + line_gap;
|
||||||
|
line_len = 0;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: we never get here if an error was thrown before
|
||||||
|
ctx.push_scissor({}, 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)!;
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,23 +1,12 @@
|
|||||||
module ugui;
|
module ugui;
|
||||||
|
|
||||||
import mtree;
|
import vtree;
|
||||||
import cache;
|
import cache;
|
||||||
|
import fifo;
|
||||||
|
|
||||||
import std::io;
|
import std::io;
|
||||||
import std::core::string;
|
import std::core::string;
|
||||||
import std::core::mem::allocator;
|
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
|
// element ids are just long ints
|
||||||
@ -34,34 +23,29 @@ enum ElemType {
|
|||||||
|
|
||||||
bitstruct ElemFlags : uint {
|
bitstruct ElemFlags : uint {
|
||||||
bool updated : 0;
|
bool updated : 0;
|
||||||
bool is_new : 1; // element is new in the cache
|
bool is_new : 1;
|
||||||
bool shown : 2; // element has been shown (drawn) this frame
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bitstruct ElemEvents : uint {
|
bitstruct ElemEvents : uint {
|
||||||
bool key_press : 0;
|
bool key_press : 0;
|
||||||
bool key_release : 1;
|
bool key_release : 1;
|
||||||
bool key_repeat : 2;
|
bool key_hold : 2;
|
||||||
bool mouse_hover : 3;
|
bool mouse_hover : 3;
|
||||||
bool mouse_press : 4;
|
bool mouse_press : 4;
|
||||||
bool mouse_release : 5;
|
bool mouse_release : 5;
|
||||||
bool mouse_hold : 6;
|
bool mouse_hold : 6;
|
||||||
bool update : 7;
|
bool update : 7;
|
||||||
bool text_input : 8;
|
bool text_input : 8;
|
||||||
bool has_focus : 9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// element structure
|
// element structure
|
||||||
struct Elem {
|
struct Elem {
|
||||||
Id id;
|
Id id;
|
||||||
int tree_idx;
|
isz tree_idx;
|
||||||
ElemFlags flags;
|
ElemFlags flags;
|
||||||
ElemEvents events;
|
ElemEvents events;
|
||||||
Rect bounds;
|
Rect bounds;
|
||||||
Rect children_bounds;
|
|
||||||
ElemType type;
|
ElemType type;
|
||||||
Layout layout;
|
|
||||||
int z_index;
|
|
||||||
union {
|
union {
|
||||||
ElemDiv div;
|
ElemDiv div;
|
||||||
ElemButton button;
|
ElemButton button;
|
||||||
@ -72,43 +56,24 @@ struct Elem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
// relationships between elements are stored in a tree, it stores just the ids
|
||||||
alias IdTree = mtree::MTree{Id};
|
alias IdTree = vtree::VTree{Id};
|
||||||
|
|
||||||
// elements themselves are kept in a cache
|
// elements themselves are kept in a cache
|
||||||
|
const uint MAX_ELEMENTS = 256;
|
||||||
alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
|
alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
|
||||||
|
|
||||||
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID;
|
alias CmdQueue = fifo::Fifo{Cmd};
|
||||||
|
|
||||||
struct InputData {
|
faultdef INVALID_SIZE, EVENT_UNSUPPORTED, UNEXPECTED_ELEMENT, WRONG_ELEMENT_TYPE, WRONG_ID;
|
||||||
InputEvents events;
|
|
||||||
struct mouse {
|
const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 };
|
||||||
Point pos, delta;
|
|
||||||
// mouse_down: bitmap of mouse buttons that are held
|
const uint STACK_STEP = 10;
|
||||||
// mouse_updated: bitmap of mouse buttons that have been updated
|
const uint MAX_ELEMS = 128;
|
||||||
// mouse_released = mouse_updated & ~mouse_down
|
const uint MAX_CMDS = 2048;
|
||||||
// mouse_pressed = mouse_updated & mouse_down
|
const uint ROOT_ID = 1;
|
||||||
MouseButtons down;
|
const uint TEXT_MAX = 64;
|
||||||
MouseButtons updated;
|
|
||||||
// scroll wheel
|
|
||||||
Point scroll;
|
|
||||||
}
|
|
||||||
struct keyboard {
|
|
||||||
char[TEXT_MAX] text;
|
|
||||||
usz text_len;
|
|
||||||
ModKeys modkeys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Ctx {
|
struct Ctx {
|
||||||
IdTree tree;
|
IdTree tree;
|
||||||
@ -120,24 +85,39 @@ struct Ctx {
|
|||||||
Font font;
|
Font font;
|
||||||
SpriteAtlas sprite_atlas;
|
SpriteAtlas sprite_atlas;
|
||||||
|
|
||||||
bool skip_frame;
|
|
||||||
|
|
||||||
bool has_focus;
|
bool has_focus;
|
||||||
InputData input, current_input;
|
struct input {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Id hover_id;
|
Id hover_id;
|
||||||
Id focus_id;
|
Id focus_id;
|
||||||
|
|
||||||
Rect div_scissor; // the current div bounds used for scissor test
|
Rect div_scissor; // the current div bounds used for scissor test
|
||||||
int active_div; // tree node indicating the current active div
|
isz active_div; // tree node indicating the current active div
|
||||||
}
|
}
|
||||||
|
|
||||||
// return a pointer to the parent of the current active div
|
// return a pointer to the parent of the current active div
|
||||||
fn Elem*? Ctx.get_parent(&ctx)
|
fn Elem*? Ctx.get_parent(&ctx)
|
||||||
{
|
{
|
||||||
Id parent_id = ctx.tree[ctx.active_div]!;
|
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||||
Elem* parent = ctx.cache.search(parent_id)!;
|
return ctx.cache.search(parent_id);
|
||||||
return parent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro @bits(#a) => $typeof(#a).sizeof*8;
|
macro @bits(#a) => $typeof(#a).sizeof*8;
|
||||||
@ -148,7 +128,6 @@ const uint GOLDEN_RATIO = 0x9E3779B9;
|
|||||||
// with the Cantor pairing function
|
// with the Cantor pairing function
|
||||||
fn Id? Ctx.gen_id(&ctx, Id id2)
|
fn Id? Ctx.gen_id(&ctx, Id id2)
|
||||||
{
|
{
|
||||||
// FIXME: this is SHIT
|
|
||||||
Id id1 = ctx.tree.get(ctx.active_div)!;
|
Id id1 = ctx.tree.get(ctx.active_div)!;
|
||||||
// Mix the two IDs non-linearly
|
// Mix the two IDs non-linearly
|
||||||
Id mixed = id1 ^ id2.rotate_left(13);
|
Id mixed = id1 ^ id2.rotate_left(13);
|
||||||
@ -162,34 +141,29 @@ macro Id @compute_id(...)
|
|||||||
{
|
{
|
||||||
Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE);
|
Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE);
|
||||||
$for var $i = 0; $i < $vacount; $i++:
|
$for var $i = 0; $i < $vacount; $i++:
|
||||||
id ^= (Id)$vaexpr[$i].hash();
|
id ^= (Id)$vaconst[$i].hash();
|
||||||
$endfor
|
$endfor
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get or push an element from the cache, return a pointer to it
|
// get or push an element from the cache, return a pointer to it
|
||||||
// resets all flags except is_new which is set accordingly
|
// resets all flags except is_new which is set accordingly
|
||||||
fn PElemTuple? Ctx.get_elem(&ctx, Id id, ElemType type)
|
fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type)
|
||||||
{
|
{
|
||||||
|
Elem empty_elem;
|
||||||
bool is_new;
|
bool is_new;
|
||||||
Elem* parent;
|
|
||||||
Elem* elem;
|
Elem* elem;
|
||||||
parent = ctx.get_parent() ?? &&(Elem){};
|
elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!;
|
||||||
|
|
||||||
elem = ctx.cache.get_or_insert(&&(Elem){}, id, &is_new)!;
|
|
||||||
elem.flags = (ElemFlags)0;
|
elem.flags = (ElemFlags)0;
|
||||||
elem.flags.is_new = is_new;
|
elem.flags.is_new = is_new;
|
||||||
elem.flags.shown = true;
|
|
||||||
elem.id = id;
|
elem.id = id;
|
||||||
elem.layout = {};
|
|
||||||
if (is_new == false && elem.type != type) {
|
if (is_new == false && elem.type != type) {
|
||||||
return WRONG_ELEMENT_TYPE?;
|
return WRONG_ELEMENT_TYPE?;
|
||||||
} else {
|
} else {
|
||||||
elem.type = type;
|
elem.type = type;
|
||||||
}
|
}
|
||||||
elem.z_index = parent.z_index;
|
elem.tree_idx = ctx.tree.add(id, ctx.active_div)!;
|
||||||
elem.tree_idx = ctx.tree.add(ctx.active_div, id)!;
|
return elem;
|
||||||
return {elem, parent};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// find an element, does not allocate a new one in cache
|
// find an element, does not allocate a new one in cache
|
||||||
@ -210,18 +184,18 @@ fn Elem*? Ctx.get_active_div(&ctx)
|
|||||||
return ctx.cache.search(id);
|
return ctx.cache.search(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn void? Ctx.init(&ctx, Allocator allocator)
|
fn void? Ctx.init(&ctx)
|
||||||
{
|
{
|
||||||
ctx.tree.init(MAX_ELEMENTS, allocator);
|
ctx.tree.init(MAX_ELEMENTS)!;
|
||||||
defer catch { (void)ctx.tree.free(); }
|
defer catch { (void)ctx.tree.free(); }
|
||||||
|
|
||||||
ctx.cache.init(allocator)!;
|
ctx.cache.init()!;
|
||||||
defer catch { (void)ctx.cache.free(); }
|
defer catch { (void)ctx.cache.free(); }
|
||||||
|
|
||||||
ctx.cmd_queue.init(allocator::mem, MAX_COMMANDS);
|
ctx.cmd_queue.init(MAX_CMDS)!;
|
||||||
defer catch { (void)ctx.cmd_queue.free(); }
|
defer catch { (void)ctx.cmd_queue.free(); }
|
||||||
|
|
||||||
ctx.styles.init(allocator);
|
ctx.styles.init(allocator::heap());
|
||||||
ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default"));
|
ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default"));
|
||||||
defer catch { ctx.styles.free(); }
|
defer catch { ctx.styles.free(); }
|
||||||
|
|
||||||
@ -243,7 +217,7 @@ fn void? Ctx.frame_begin(&ctx)
|
|||||||
// 1. Reset the active div
|
// 1. Reset the active div
|
||||||
// 2. Get the root element from the cache and update it
|
// 2. Get the root element from the cache and update it
|
||||||
ctx.active_div = 0;
|
ctx.active_div = 0;
|
||||||
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!.first;
|
Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!;
|
||||||
ctx.active_div = elem.tree_idx;
|
ctx.active_div = elem.tree_idx;
|
||||||
// The root should have the updated flag only if the size of the window
|
// The root should have the updated flag only if the size of the window
|
||||||
// was changed between frasmes, this propagates an element size recalculation
|
// was changed between frasmes, this propagates an element size recalculation
|
||||||
@ -255,56 +229,36 @@ fn void? Ctx.frame_begin(&ctx)
|
|||||||
//elem.flags.has_focus = ctx.has_focus;
|
//elem.flags.has_focus = ctx.has_focus;
|
||||||
|
|
||||||
elem.bounds = {0, 0, ctx.width, ctx.height};
|
elem.bounds = {0, 0, ctx.width, ctx.height};
|
||||||
elem.z_index = 0;
|
elem.div.layout = LAYOUT_ROW;
|
||||||
|
elem.div.z_index = 0;
|
||||||
|
elem.div.children_bounds = elem.bounds;
|
||||||
elem.div.scroll_x.enabled = false;
|
elem.div.scroll_x.enabled = false;
|
||||||
elem.div.scroll_y.enabled = false;
|
elem.div.scroll_y.enabled = false;
|
||||||
elem.layout.dir = ROW;
|
elem.div.pcb = {};
|
||||||
elem.layout.anchor = TOP_LEFT;
|
elem.div.origin_c = {};
|
||||||
elem.layout.w = @exact(ctx.width);
|
elem.div.origin_r = {};
|
||||||
elem.layout.h = @exact(ctx.height);
|
|
||||||
|
|
||||||
ctx.div_scissor = elem.bounds;
|
ctx.div_scissor = {0, 0, ctx.width, ctx.height};
|
||||||
|
|
||||||
ctx.skip_frame = false;
|
// The root element does not push anything to the stack
|
||||||
|
// TODO: add a background color taken from a theme or config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int DEBUG = 1;
|
||||||
|
|
||||||
fn void? Ctx.frame_end(&ctx)
|
fn void? Ctx.frame_end(&ctx)
|
||||||
{
|
{
|
||||||
|
// FIXME: this is not guaranteed to be root. the user might forget to close a div or some other element
|
||||||
Elem* root = ctx.get_active_div()!;
|
Elem* root = ctx.get_active_div()!;
|
||||||
if (root.id != ROOT_ID) {
|
root.div.layout = LAYOUT_ROW;
|
||||||
return WRONG_ID?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. clear input fields
|
|
||||||
Point mdelta = ctx.current_input.mouse.pos - ctx.input.mouse.pos;
|
|
||||||
ctx.input = ctx.current_input;
|
|
||||||
ctx.input.mouse.delta = mdelta;
|
|
||||||
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
|
// 1. clear the tree
|
||||||
ctx.tree.nuke();
|
ctx.tree.nuke();
|
||||||
|
|
||||||
|
// 2. clear input fields
|
||||||
|
ctx.input.events = (InputEvents)0;
|
||||||
|
ctx.input.keyboard.text_len = 0;
|
||||||
|
|
||||||
// send atlas updates
|
// send atlas updates
|
||||||
if (ctx.font.should_update) {
|
if (ctx.font.should_update) {
|
||||||
ctx.push_update_atlas(&ctx.font.atlas)!;
|
ctx.push_update_atlas(&ctx.font.atlas)!;
|
||||||
@ -315,19 +269,8 @@ fn void? Ctx.frame_end(&ctx)
|
|||||||
ctx.sprite_atlas.should_update = false;
|
ctx.sprite_atlas.should_update = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// send skip frame request
|
// debug
|
||||||
if (ctx.skip_frame) {
|
$if DEBUG == 1:
|
||||||
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
|
// draw mouse position
|
||||||
Cmd cmd = {
|
Cmd cmd = {
|
||||||
.type = CMD_RECT,
|
.type = CMD_RECT,
|
||||||
@ -340,59 +283,48 @@ $if $feature(DEBUG_POINTER):
|
|||||||
},
|
},
|
||||||
.rect.color = 0xff00ffffu.to_rgba()
|
.rect.color = 0xff00ffffu.to_rgba()
|
||||||
};
|
};
|
||||||
ctx.cmd_queue.push(cmd);
|
ctx.cmd_queue.enqueue(&cmd)!;
|
||||||
$endif
|
$endif
|
||||||
|
|
||||||
|
// sort the command buffer by the z-index
|
||||||
|
ctx.cmd_queue.sort()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<*
|
||||||
|
* @ensure elem != null
|
||||||
|
*>
|
||||||
|
macro bool Ctx.is_hovered(&ctx, Elem *elem)
|
||||||
|
{
|
||||||
|
return ctx.input.mouse.pos.in_rect(elem.bounds);
|
||||||
|
}
|
||||||
|
|
||||||
macro bool Ctx.is_hovered(&ctx, Elem *elem) => ctx.input.mouse.pos.in_rect(elem.bounds);
|
macro bool Ctx.elem_focus(&ctx, Elem *elem)
|
||||||
|
{
|
||||||
|
return ctx.focus_id == elem.id;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the element is hovered and/or focused, if it is update the context ids.
|
// TODO: add other events
|
||||||
// The order in which the elements are passed to this function is not relevant
|
// FIXME: this does not work with touch
|
||||||
fn void Ctx.update_hover_and_focus(&ctx, Elem* elem)
|
// FIXME: hacked together, please do better
|
||||||
|
fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
|
||||||
{
|
{
|
||||||
bool hover = ctx.is_hovered(elem);
|
bool hover = ctx.is_hovered(elem);
|
||||||
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_ANY));
|
bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT));
|
||||||
|
|
||||||
if (hover) {
|
if (ctx.is_mouse_pressed(BTN_ANY) && !hover){
|
||||||
Elem* prev_hover = ctx.find_elem(ctx.hover_id);
|
focus = false;
|
||||||
bool different = prev_hover.id != elem.id;
|
if (ctx.focus_id == elem.id) ctx.focus_id = 0;
|
||||||
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) {
|
if (hover) { ctx.hover_id = elem.id; }
|
||||||
Elem* prev_focus = ctx.find_elem(ctx.hover_id);
|
if (focus) { ctx.focus_id = elem.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);
|
ElemEvents ev = {
|
||||||
}
|
.mouse_hover = hover,
|
||||||
|
.mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY),
|
||||||
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_release = hover && focus && ctx.is_mouse_released(BTN_ANY),
|
||||||
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
|
.mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY),
|
||||||
.key_press = focus && ctx.input.events.key_press,
|
.text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
|
||||||
.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),
|
|
||||||
};
|
};
|
||||||
|
return ev;
|
||||||
}
|
}
|
||||||
153
lib/ugui.c3l/src/ugui_div.c3
Normal file
153
lib/ugui.c3l/src/ugui_div.c3
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
module ugui;
|
||||||
|
|
||||||
|
import std::io;
|
||||||
|
import std::math;
|
||||||
|
|
||||||
|
// div element
|
||||||
|
struct ElemDiv {
|
||||||
|
Layout layout;
|
||||||
|
struct scroll_x {
|
||||||
|
bool enabled;
|
||||||
|
bool on;
|
||||||
|
float value;
|
||||||
|
}
|
||||||
|
struct scroll_y {
|
||||||
|
bool enabled;
|
||||||
|
bool on;
|
||||||
|
float value;
|
||||||
|
}
|
||||||
|
ushort scroll_size;
|
||||||
|
int z_index;
|
||||||
|
Rect children_bounds; // current frame children bounds
|
||||||
|
Rect pcb; // previous frame children bounds
|
||||||
|
Point origin_r, origin_c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// begin a widget container, or div, the size determines the offset (x,y) width and height.
|
||||||
|
// if the width or height are zero the width or height are set to the maximum available.
|
||||||
|
// if the width or height are negative the width or height will be calculated based on the children size
|
||||||
|
// sort similar to a flexbox, and the minimum size is set by the negative of the width or height
|
||||||
|
// FIXME: there is a bug if the size.w or size.h == -0
|
||||||
|
macro Ctx.div_begin(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...)
|
||||||
|
=> ctx.div_begin_id(@compute_id($vasplat), size, scroll_x, scroll_y);
|
||||||
|
fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem* parent = ctx.get_parent()!;
|
||||||
|
Elem* elem = ctx.get_elem(id, ETYPE_DIV)!;
|
||||||
|
ctx.active_div = elem.tree_idx;
|
||||||
|
|
||||||
|
Style* style = ctx.styles.get_style(@str_hash("default"));
|
||||||
|
Style* slider_style = ctx.styles.get_style(@str_hash("slider"));
|
||||||
|
|
||||||
|
elem.div.scroll_x.enabled = scroll_x;
|
||||||
|
elem.div.scroll_y.enabled = scroll_y;
|
||||||
|
elem.div.scroll_size = slider_style.size ? slider_style.size : (style.size ? style.size : DEFAULT_STYLE.size);
|
||||||
|
elem.div.z_index = parent.div.z_index + 1;
|
||||||
|
|
||||||
|
// 2. layout the element
|
||||||
|
Rect wanted_size = {
|
||||||
|
.x = size.x,
|
||||||
|
.y = size.y,
|
||||||
|
.w = size.w < 0 ? max(elem.div.pcb.w, (short)-size.w) : size.w,
|
||||||
|
.h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h,
|
||||||
|
};
|
||||||
|
elem.bounds = ctx.position_element(parent, wanted_size, style);
|
||||||
|
elem.div.children_bounds = {};
|
||||||
|
|
||||||
|
// update the ctx scissor
|
||||||
|
ctx.div_scissor = elem.bounds;
|
||||||
|
ctx.push_scissor(elem.bounds, elem.div.z_index)!;
|
||||||
|
|
||||||
|
// 4. Fill the div fields
|
||||||
|
elem.div.origin_c = {
|
||||||
|
.x = elem.bounds.x,
|
||||||
|
.y = elem.bounds.y
|
||||||
|
};
|
||||||
|
elem.div.origin_r = elem.div.origin_c;
|
||||||
|
elem.div.layout = parent.div.layout;
|
||||||
|
|
||||||
|
// Add the background to the draw stack
|
||||||
|
bool do_border = parent.div.layout == LAYOUT_FLOATING;
|
||||||
|
ctx.push_rect(elem.bounds, elem.div.z_index, style)!;
|
||||||
|
|
||||||
|
elem.events = ctx.get_elem_events(elem);
|
||||||
|
|
||||||
|
// TODO: check active
|
||||||
|
// TODO: check resizeable
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Ctx.div_end(&ctx)
|
||||||
|
{
|
||||||
|
// swap the children bounds
|
||||||
|
Elem* parent = ctx.get_parent()!;
|
||||||
|
Elem* elem = ctx.get_active_div()!;
|
||||||
|
elem.div.pcb = elem.div.children_bounds;
|
||||||
|
|
||||||
|
// FIXME: this causes all elements inside the div to loose focus since the mouse press happens
|
||||||
|
// both inside the element and inside the div bounds
|
||||||
|
//elem.events = ctx.get_elem_events(elem);
|
||||||
|
|
||||||
|
Rect cb = elem.div.pcb;
|
||||||
|
// children bounds bottom-right corner
|
||||||
|
Point cbc = {
|
||||||
|
.x = cb.x + cb.w,
|
||||||
|
.y = cb.y + cb.h,
|
||||||
|
};
|
||||||
|
// div bounds bottom-right corner
|
||||||
|
Point bc = {
|
||||||
|
.x = elem.bounds.x + elem.bounds.w,
|
||||||
|
.y = elem.bounds.y + elem.bounds.h,
|
||||||
|
};
|
||||||
|
|
||||||
|
// set the scrollbar flag, is used in layout
|
||||||
|
// 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(@str_hash("div_scrollbar_horizontal"))!;
|
||||||
|
Id vsid_real = ctx.gen_id(@str_hash("div_scrollbar_vertical"))!;
|
||||||
|
short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid_real || ctx.is_hovered(ctx.find_elem(vsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
|
||||||
|
short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid_real || ctx.is_hovered(ctx.find_elem(hsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
|
||||||
|
|
||||||
|
if (elem.div.scroll_y.on) {
|
||||||
|
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
Rect vslider = {
|
||||||
|
.x = elem.bounds.x + elem.bounds.w - wdim,
|
||||||
|
.y = elem.bounds.y,
|
||||||
|
.w = wdim,
|
||||||
|
.h = elem.bounds.h - hdim,
|
||||||
|
};
|
||||||
|
Layout prev_l = elem.div.layout;
|
||||||
|
elem.div.layout = LAYOUT_ABSOLUTE;
|
||||||
|
ctx.slider_ver_id(vsid_raw, vslider, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
|
||||||
|
elem.div.layout = prev_l;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elem.div.scroll_x.on) {
|
||||||
|
if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
Rect hslider = {
|
||||||
|
.x = elem.bounds.x,
|
||||||
|
.y = elem.bounds.y + elem.bounds.h - hdim,
|
||||||
|
.w = elem.bounds.w - wdim,
|
||||||
|
.h = hdim,
|
||||||
|
};
|
||||||
|
Layout prev_l = elem.div.layout;
|
||||||
|
elem.div.layout = LAYOUT_ABSOLUTE;
|
||||||
|
ctx.slider_hor_id(hsid_raw, hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!;
|
||||||
|
elem.div.layout = prev_l;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the active_div returns to the parent of the current one
|
||||||
|
ctx.active_div = ctx.tree.parentof(ctx.active_div)!;
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
module ugui::font;
|
module ugui;
|
||||||
|
|
||||||
import schrift;
|
import schrift;
|
||||||
|
import grapheme;
|
||||||
import std::collections::map;
|
import std::collections::map;
|
||||||
import std::core::mem;
|
import std::core::mem;
|
||||||
import std::core::mem::allocator;
|
import std::core::mem::allocator;
|
||||||
@ -8,19 +9,11 @@ import std::io;
|
|||||||
import std::ascii;
|
import std::ascii;
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
// CODEPOINT //
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
// unicode code point, different type for a different hash
|
// unicode code point, different type for a different hash
|
||||||
alias Codepoint = uint;
|
alias Codepoint = uint;
|
||||||
|
|
||||||
//macro uint Codepoint.hash(self) => ((uint)self).hash();
|
//macro uint Codepoint.hash(self) => ((uint)self).hash();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
// FONT ATLAS //
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
/* width and height of a glyph contain the kering advance
|
/* width and height of a glyph contain the kering advance
|
||||||
* (u,v)
|
* (u,v)
|
||||||
* +-------------*---+ -
|
* +-------------*---+ -
|
||||||
@ -65,18 +58,9 @@ struct Font {
|
|||||||
bool should_update; // should send update_atlas command, resets at frame_end()
|
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};
|
fn void? Font.load(&font, String name, ZString path, uint height, float scale)
|
||||||
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.table.init(allocator::heap(), capacity: FONT_CACHED);
|
||||||
font.id = name.hash();
|
font.id = name.hash();
|
||||||
|
|
||||||
font.size = height*scale;
|
font.size = height*scale;
|
||||||
@ -107,14 +91,10 @@ fn void? Font.load(&font, Allocator allocator, String name, ZString path, uint h
|
|||||||
|
|
||||||
// preallocate the ASCII range
|
// preallocate the ASCII range
|
||||||
for (char c = ' '; c < '~'; c++) {
|
for (char c = ' '; c < '~'; c++) {
|
||||||
// FIXME: without @inline, this crashes with O1 or greater
|
font.get_glyph((Codepoint)c)!;
|
||||||
font.get_glyph((Codepoint)c) @inline!;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<*
|
|
||||||
@param [&inout] font
|
|
||||||
*>
|
|
||||||
fn Glyph*? Font.get_glyph(&font, Codepoint code)
|
fn Glyph*? Font.get_glyph(&font, Codepoint code)
|
||||||
{
|
{
|
||||||
Glyph*? gp;
|
Glyph*? gp;
|
||||||
@ -128,7 +108,6 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code)
|
|||||||
return gp;
|
return gp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// missing glyph, render and place into an atlas
|
// missing glyph, render and place into an atlas
|
||||||
Glyph glyph;
|
Glyph glyph;
|
||||||
|
|
||||||
@ -174,9 +153,6 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code)
|
|||||||
return font.table.get_ref(code);
|
return font.table.get_ref(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
<*
|
|
||||||
@param [&inout] font
|
|
||||||
*>
|
|
||||||
fn void Font.free(&font)
|
fn void Font.free(&font)
|
||||||
{
|
{
|
||||||
font.atlas.free();
|
font.atlas.free();
|
||||||
@ -184,35 +160,99 @@ fn void Font.free(&font)
|
|||||||
schrift::freefont(font.sft.font);
|
schrift::freefont(font.sft.font);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0)
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
// 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);
|
return ctx.font.load(name, path, height, scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
<*
|
<*
|
||||||
@param [&in] ctx
|
@require off != null
|
||||||
@param [in] label
|
|
||||||
*>
|
*>
|
||||||
// TODO: check if the font is present in the context
|
fn Codepoint str_to_codepoint(char[] str, usz* off)
|
||||||
fn Id Ctx.get_font_id(&ctx, String label) => (Id)label.hash();
|
{
|
||||||
|
Codepoint cp;
|
||||||
|
isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp);
|
||||||
|
if (b == 0 || b > str.len) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
*off = b;
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Rect? Ctx.get_text_bounds(&ctx, String text)
|
||||||
|
{
|
||||||
|
Rect text_bounds;
|
||||||
|
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||||
|
short line_gap = (short)ctx.font.linegap;
|
||||||
|
text_bounds.h = line_height;
|
||||||
|
Glyph* gp;
|
||||||
|
|
||||||
|
// TODO: account for unicode codepoints
|
||||||
|
short line_len;
|
||||||
|
Codepoint cp;
|
||||||
|
usz off, x;
|
||||||
|
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||||
|
off += x;
|
||||||
|
bool n;
|
||||||
|
if (!ascii::is_cntrl((char)cp)) {
|
||||||
|
gp = ctx.font.get_glyph(cp)!;
|
||||||
|
line_len += gp.adv;
|
||||||
|
} else if (cp == '\n'){
|
||||||
|
text_bounds.h += line_height + line_gap;
|
||||||
|
line_len = 0;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line_len > text_bounds.w) {
|
||||||
|
text_bounds.w = line_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text_bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Point? Ctx.get_cursor_position(&ctx, String text)
|
||||||
|
{
|
||||||
|
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||||
|
short line_gap = (short)ctx.font.linegap;
|
||||||
|
Glyph* gp;
|
||||||
|
|
||||||
|
// TODO: account for unicode codepoints
|
||||||
|
Point line;
|
||||||
|
Codepoint cp;
|
||||||
|
usz off, x;
|
||||||
|
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||||
|
off += x;
|
||||||
|
bool n;
|
||||||
|
if (!ascii::is_cntrl((char)cp)) {
|
||||||
|
gp = ctx.font.get_glyph(cp)!;
|
||||||
|
line.x += gp.adv;
|
||||||
|
} else if (cp == '\n'){
|
||||||
|
line.y += line_height + line_gap;
|
||||||
|
line.x = 0;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds)
|
||||||
|
{
|
||||||
|
short dw = bounds.w - text_bounds.w;
|
||||||
|
short dh = bounds.h - text_bounds.h;
|
||||||
|
|
||||||
|
return {.x = dw/2, .y = dh/2};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check if the font is present in the context
|
||||||
|
fn Id Ctx.get_font_id(&ctx, String label)
|
||||||
|
{
|
||||||
|
return (Id)label.hash();
|
||||||
|
}
|
||||||
|
|
||||||
<*
|
|
||||||
@param [&in] ctx
|
|
||||||
@param [in] name
|
|
||||||
*>
|
|
||||||
fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
|
fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
|
||||||
{
|
{
|
||||||
// TODO: use the font name, for now there is only one font
|
// TODO: use the font name, for now there is only one font
|
||||||
@ -223,5 +263,4 @@ fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
|
|||||||
return &ctx.font.atlas;
|
return &ctx.font.atlas;
|
||||||
}
|
}
|
||||||
|
|
||||||
<* @param [&in] font *>
|
|
||||||
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);
|
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);
|
||||||
@ -1,10 +1,10 @@
|
|||||||
module ugui;
|
module ugui;
|
||||||
|
|
||||||
|
import grapheme;
|
||||||
import std::io;
|
import std::io;
|
||||||
import std::math;
|
import std::math;
|
||||||
import std::core::string;
|
import std::core::string;
|
||||||
|
|
||||||
|
|
||||||
bitstruct InputEvents : uint {
|
bitstruct InputEvents : uint {
|
||||||
bool resize : 0; // window size was changed
|
bool resize : 0; // window size was changed
|
||||||
bool change_focus : 1; // window focus changed
|
bool change_focus : 1; // window focus changed
|
||||||
@ -13,9 +13,6 @@ bitstruct InputEvents : uint {
|
|||||||
bool mouse_scroll : 4; // mouse scroll wheel. x or y
|
bool mouse_scroll : 4; // mouse scroll wheel. x or y
|
||||||
bool text_input : 5;
|
bool text_input : 5;
|
||||||
bool mod_key : 6;
|
bool mod_key : 6;
|
||||||
bool key_press : 7;
|
|
||||||
bool key_release : 8;
|
|
||||||
bool key_repeat : 9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bitstruct MouseButtons : uint {
|
bitstruct MouseButtons : uint {
|
||||||
@ -44,13 +41,11 @@ bitstruct ModKeys : uint {
|
|||||||
bool scroll : 11;
|
bool scroll : 11;
|
||||||
bool bkspc : 12;
|
bool bkspc : 12;
|
||||||
bool del : 13;
|
bool del : 13;
|
||||||
bool home : 14;
|
|
||||||
bool end : 15;
|
|
||||||
// arrow keys
|
// arrow keys
|
||||||
bool up : 16;
|
bool up : 14;
|
||||||
bool down : 17;
|
bool down : 15;
|
||||||
bool left : 18;
|
bool left : 16;
|
||||||
bool right : 19;
|
bool right : 17;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModKeys KMOD_CTRL = {.lctrl = true, .rctrl = true};
|
const ModKeys KMOD_CTRL = {.lctrl = true, .rctrl = true};
|
||||||
@ -71,167 +66,139 @@ const MouseButtons BTN_5 = {.btn_5 = true};
|
|||||||
|
|
||||||
const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max);
|
const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max);
|
||||||
|
|
||||||
<* @param [&inout] ctx *>
|
fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String keys)
|
||||||
fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String ...keys)
|
|
||||||
{
|
{
|
||||||
bool is_mod = (bool)(ctx.current_input.keyboard.modkeys & mod);
|
bool is_mod = (bool)(ctx.input.keyboard.modkeys & mod);
|
||||||
bool is_keys = true;
|
bool is_keys = true;
|
||||||
String haystack = (String)ctx.get_keys();
|
String haystack = (String)ctx.input.keyboard.text[0..ctx.input.keyboard.text_len];
|
||||||
foreach (needle: keys) {
|
char[2] needle;
|
||||||
is_keys = is_keys && haystack.contains(needle);
|
foreach (c: keys) {
|
||||||
|
needle[0] = c;
|
||||||
|
is_keys = is_keys && haystack.contains((String)needle[..]);
|
||||||
}
|
}
|
||||||
return is_mod && is_keys;
|
return is_mod && is_keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window size was changed
|
// Window size was changed
|
||||||
<* @param [&inout] ctx *>
|
|
||||||
fn void? Ctx.input_window_size(&ctx, short width, short height)
|
fn void? Ctx.input_window_size(&ctx, short width, short height)
|
||||||
{
|
{
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return INVALID_SIZE?;
|
return INVALID_SIZE?;
|
||||||
}
|
}
|
||||||
ctx.current_input.events.resize = ctx.width != width || ctx.height != height;
|
ctx.input.events.resize = ctx.width != width || ctx.height != height;
|
||||||
ctx.width = width;
|
ctx.width = width;
|
||||||
ctx.height = height;
|
ctx.height = height;
|
||||||
if (ctx.current_input.events.resize) ctx.skip_frame = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window gained/lost focus
|
// Window gained/lost focus
|
||||||
<* @param [&inout] ctx *>
|
|
||||||
fn void Ctx.input_changefocus(&ctx, bool has_focus)
|
fn void Ctx.input_changefocus(&ctx, bool has_focus)
|
||||||
{
|
{
|
||||||
// FIXME: raylib only has an API to query the focus status so we have to
|
// FIXME: raylib only has an API to query the focus status so we have to
|
||||||
// update the input flag only if the focus changed
|
// update the input flag only if the focus changed
|
||||||
ctx.current_input.events.change_focus = ctx.has_focus != has_focus;
|
ctx.input.events.change_focus = ctx.has_focus != has_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_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_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down;
|
||||||
macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down;
|
macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down;
|
||||||
|
|
||||||
|
// FIXME: hthis compairson could be done with a cast using MouseButtons.inner
|
||||||
|
// property but I could not figure out how
|
||||||
macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn) => (ctx.mouse_pressed() & btn) != BTN_NONE;
|
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_released(&ctx, MouseButtons btn) => (ctx.mouse_released() & btn) != BTN_NONE;
|
||||||
macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE;
|
macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE;
|
||||||
|
|
||||||
// Mouse Buttons down
|
// Mouse Buttons down
|
||||||
<* @param [&inout] ctx *>
|
|
||||||
fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons)
|
fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons)
|
||||||
{
|
{
|
||||||
ctx.current_input.mouse.updated = ctx.current_input.mouse.down ^ buttons;
|
ctx.input.mouse.updated = ctx.input.mouse.down ^ buttons;
|
||||||
ctx.current_input.mouse.down = buttons;
|
ctx.input.mouse.down = buttons;
|
||||||
ctx.current_input.events.mouse_btn = ctx.current_input.mouse.down != BTN_NONE || ctx.current_input.mouse.updated != BTN_NONE;
|
ctx.input.events.mouse_btn = (uint)ctx.input.mouse.down != 0 || (uint)ctx.input.mouse.updated != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse was moved, report absolute position
|
// Mouse was moved, report absolute position
|
||||||
<* @param [&inout] ctx *>
|
|
||||||
fn void Ctx.input_mouse_abs(&ctx, short x, short y)
|
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.input.mouse.pos.x = math::clamp(x, (short)0, ctx.width);
|
||||||
ctx.current_input.mouse.pos.y = math::clamp(y, (short)0, ctx.height);
|
ctx.input.mouse.pos.y = math::clamp(y, (short)0, ctx.height);
|
||||||
|
|
||||||
short dx, dy;
|
short dx, dy;
|
||||||
dx = x - ctx.current_input.mouse.pos.x;
|
dx = x - ctx.input.mouse.pos.x;
|
||||||
dy = y - ctx.current_input.mouse.pos.y;
|
dy = y - ctx.input.mouse.pos.y;
|
||||||
|
|
||||||
ctx.current_input.mouse.delta.x = dx;
|
ctx.input.mouse.delta.x = dx;
|
||||||
ctx.current_input.mouse.delta.y = dy;
|
ctx.input.mouse.delta.y = dy;
|
||||||
|
|
||||||
ctx.current_input.events.mouse_move = dx != 0 || dy != 0;
|
ctx.input.events.mouse_move = dx != 0 || dy != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse was moved, report relative motion
|
// Mouse was moved, report relative motion
|
||||||
<* @param [&inout] ctx *>
|
|
||||||
fn void Ctx.input_mouse_delta(&ctx, short dx, short dy)
|
fn void Ctx.input_mouse_delta(&ctx, short dx, short dy)
|
||||||
{
|
{
|
||||||
ctx.current_input.mouse.delta.x = dx;
|
ctx.input.mouse.delta.x = dx;
|
||||||
ctx.current_input.mouse.delta.y = dy;
|
ctx.input.mouse.delta.y = dy;
|
||||||
|
|
||||||
short mx, my;
|
short mx, my;
|
||||||
mx = ctx.current_input.mouse.pos.x + dx;
|
mx = ctx.input.mouse.pos.x + dx;
|
||||||
my = ctx.current_input.mouse.pos.y + dy;
|
my = ctx.input.mouse.pos.y + dy;
|
||||||
|
|
||||||
ctx.current_input.mouse.pos.x = math::clamp(mx, (short)0, ctx.width);
|
ctx.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.input.mouse.pos.y = math::clamp(my, (short)0, ctx.height);
|
||||||
|
|
||||||
ctx.current_input.events.mouse_move = dx != 0 || dy != 0;
|
ctx.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)
|
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.input.mouse.scroll.x = (short)((float)-x*scale);
|
||||||
ctx.current_input.mouse.scroll.y = (short)((float)-y*scale);
|
ctx.input.mouse.scroll.y = (short)((float)-y*scale);
|
||||||
ctx.current_input.events.mouse_scroll = x !=0 || y != 0;
|
ctx.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
|
// append utf-8 encoded text to the context text input
|
||||||
<* @param [&inout] ctx *>
|
|
||||||
fn void Ctx.input_text_utf8(&ctx, char[] text)
|
fn void Ctx.input_text_utf8(&ctx, char[] text)
|
||||||
{
|
{
|
||||||
if (text == "") return;
|
if (text.len == 0) { return; }
|
||||||
|
|
||||||
usz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len;
|
usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len;
|
||||||
usz len = text.len > remaining ? remaining : text.len;
|
usz len = text.len > remaining ? remaining : text.len;
|
||||||
char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..];
|
char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..];
|
||||||
s[..len-1] = text[..len-1];
|
s[..len-1] = text[..len-1];
|
||||||
ctx.current_input.keyboard.text_len += len;
|
ctx.input.keyboard.text_len += len;
|
||||||
ctx.current_input.events.text_input = true;
|
ctx.input.events.text_input = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
<*
|
fn void Ctx.input_text_unicode(&ctx, char[] text)
|
||||||
@param [&inout] ctx
|
|
||||||
@param [in] text
|
|
||||||
*>
|
|
||||||
fn void? Ctx.input_text_unicode(&ctx, uint[] text)
|
|
||||||
{
|
{
|
||||||
if (text.len == 0) return;
|
if (text.ptr == null || text.len == 0) { return; }
|
||||||
|
|
||||||
usz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len;
|
char[32] tmp;
|
||||||
char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..];
|
usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len;
|
||||||
|
char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..];
|
||||||
|
|
||||||
usz off = conv::utf32to8(text, s)!;
|
usz off;
|
||||||
ctx.current_input.keyboard.text_len += off;
|
foreach (idx, cp: text) {
|
||||||
|
if (off >= remaining) { break; }
|
||||||
|
usz enc = grapheme::encode_utf8(cp, tmp[..], tmp.len);
|
||||||
|
s[off..off+enc] = tmp[..enc];
|
||||||
|
off += enc;
|
||||||
|
}
|
||||||
|
ctx.input.keyboard.text_len += off;
|
||||||
|
|
||||||
ctx.current_input.events.text_input = true;
|
ctx.input.events.text_input = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
<* @param [&inout] ctx *>
|
|
||||||
fn void Ctx.input_char(&ctx, char c)
|
fn void Ctx.input_char(&ctx, char c)
|
||||||
{
|
{
|
||||||
char[1] b = {c};
|
char[1] b = {c};
|
||||||
ctx.input_text_utf8(b[..]);
|
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
|
// Modifier keys, like control or backspace
|
||||||
<* @param [&inout] ctx *>
|
// TODO: make this call repetible to input modkeys one by one
|
||||||
fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys, bool set)
|
fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys)
|
||||||
{
|
{
|
||||||
if (set) {
|
ctx.input.keyboard.modkeys = modkeys;
|
||||||
ctx.current_input.keyboard.modkeys |= modkeys;
|
ctx.input.events.mod_key = (uint)ctx.input.keyboard.modkeys != 0;
|
||||||
} else {
|
|
||||||
ctx.current_input.keyboard.modkeys &= ~modkeys;
|
|
||||||
}
|
|
||||||
ctx.current_input.events.mod_key = (uint)ctx.current_input.keyboard.modkeys != 0;
|
|
||||||
}
|
}
|
||||||
196
lib/ugui.c3l/src/ugui_layout.c3
Normal file
196
lib/ugui.c3l/src/ugui_layout.c3
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
module ugui;
|
||||||
|
|
||||||
|
enum Layout {
|
||||||
|
LAYOUT_ROW,
|
||||||
|
LAYOUT_COLUMN,
|
||||||
|
LAYOUT_FLOATING,
|
||||||
|
LAYOUT_ABSOLUTE,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Ctx.layout_set_row(&ctx)
|
||||||
|
{
|
||||||
|
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||||
|
Elem *parent = ctx.cache.search(parent_id)!;
|
||||||
|
|
||||||
|
if (parent.type != ETYPE_DIV) {
|
||||||
|
// what?
|
||||||
|
return UNEXPECTED_ELEMENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.div.layout = LAYOUT_ROW;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Ctx.layout_set_column(&ctx)
|
||||||
|
{
|
||||||
|
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||||
|
Elem *parent = ctx.cache.search(parent_id)!;
|
||||||
|
|
||||||
|
if (parent.type != ETYPE_DIV) {
|
||||||
|
// what?
|
||||||
|
return UNEXPECTED_ELEMENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.div.layout = LAYOUT_COLUMN;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Ctx.layout_set_floating(&ctx)
|
||||||
|
{
|
||||||
|
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||||
|
Elem *parent = ctx.cache.search(parent_id)!;
|
||||||
|
|
||||||
|
if (parent.type != ETYPE_DIV) {
|
||||||
|
// what?
|
||||||
|
return UNEXPECTED_ELEMENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.div.layout = LAYOUT_FLOATING;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Ctx.layout_next_row(&ctx)
|
||||||
|
{
|
||||||
|
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||||
|
Elem *parent = ctx.cache.search(parent_id)!;
|
||||||
|
|
||||||
|
if (parent.type != ETYPE_DIV) {
|
||||||
|
return UNEXPECTED_ELEMENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.div.origin_r = {
|
||||||
|
.x = parent.bounds.x,
|
||||||
|
.y = parent.div.children_bounds.bottom_right().y,
|
||||||
|
};
|
||||||
|
parent.div.origin_c = parent.div.origin_r;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Ctx.layout_next_column(&ctx)
|
||||||
|
{
|
||||||
|
Id parent_id = ctx.tree.get(ctx.active_div)!;
|
||||||
|
Elem *parent = ctx.cache.search(parent_id)!;
|
||||||
|
|
||||||
|
if (parent.type != ETYPE_DIV) {
|
||||||
|
return UNEXPECTED_ELEMENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.div.origin_c = {
|
||||||
|
.x = parent.div.children_bounds.bottom_right().x,
|
||||||
|
.y = parent.bounds.y,
|
||||||
|
};
|
||||||
|
parent.div.origin_r = parent.div.origin_c;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
macro Rect Elem.get_view(&elem)
|
||||||
|
{
|
||||||
|
Rect off;
|
||||||
|
if (elem.div.scroll_x.enabled && elem.div.scroll_x.on) {
|
||||||
|
off.x = (short)((float)(elem.div.pcb.w - elem.bounds.w) * elem.div.scroll_x.value);
|
||||||
|
off.w = -elem.div.scroll_size;
|
||||||
|
}
|
||||||
|
if (elem.div.scroll_y.enabled && elem.div.scroll_y.on) {
|
||||||
|
off.y = (short)((float)(elem.div.pcb.h - elem.bounds.h) * elem.div.scroll_y.value);
|
||||||
|
off.h = -elem.div.scroll_size;
|
||||||
|
}
|
||||||
|
return elem.bounds.add(off);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Point Elem.get_view_off(&elem)
|
||||||
|
{
|
||||||
|
return elem.get_view().sub(elem.bounds).position();
|
||||||
|
}
|
||||||
|
|
||||||
|
// position the rectangle inside the parent according to the layout
|
||||||
|
// parent: parent div
|
||||||
|
// rect: the requested size
|
||||||
|
// style: apply style
|
||||||
|
<*
|
||||||
|
@require ctx != null
|
||||||
|
@require parent.type == ETYPE_DIV
|
||||||
|
*>
|
||||||
|
fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, Style* style)
|
||||||
|
{
|
||||||
|
ElemDiv* div = &parent.div;
|
||||||
|
|
||||||
|
Rect parent_bounds, parent_view;
|
||||||
|
Rect child_placement, child_occupied;
|
||||||
|
|
||||||
|
// 1. Select the right origin
|
||||||
|
Point origin;
|
||||||
|
switch (div.layout) {
|
||||||
|
case LAYOUT_ROW:
|
||||||
|
origin = div.origin_r;
|
||||||
|
case LAYOUT_COLUMN:
|
||||||
|
origin = div.origin_c;
|
||||||
|
case LAYOUT_FLOATING: // none, relative to zero zero
|
||||||
|
case LAYOUT_ABSOLUTE: // absolute position, this is a no-op, return the rect
|
||||||
|
return rect;
|
||||||
|
default: // error
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Compute the parent's view
|
||||||
|
parent_bounds = parent.bounds;
|
||||||
|
parent_view = parent.get_view();
|
||||||
|
|
||||||
|
// 3. Compute the placement and occupied area
|
||||||
|
|
||||||
|
// grow rect (wanted size) when widht or height are less than zero
|
||||||
|
bool adapt_x = rect.w <= 0;
|
||||||
|
bool adapt_y = rect.h <= 0;
|
||||||
|
if (adapt_x) rect.w = parent_bounds.w - parent_bounds.x - origin.x;
|
||||||
|
if (adapt_y) rect.h = parent_bounds.h - parent_bounds.y - origin.y;
|
||||||
|
|
||||||
|
// offset placement and area
|
||||||
|
child_placement = child_placement.off(origin.add(rect.position()));
|
||||||
|
child_occupied = child_occupied.off(origin.add(rect.position()));
|
||||||
|
|
||||||
|
Rect margin = style.margin;
|
||||||
|
Rect padding = style.padding;
|
||||||
|
ushort border = style.border;
|
||||||
|
|
||||||
|
// padding, grows both the placement and occupied area
|
||||||
|
child_placement = child_placement.grow(padding.position().add(padding.size()));
|
||||||
|
child_occupied = child_occupied.grow(padding.position().add(padding.size()));
|
||||||
|
// border, grows both the placement and occupied area
|
||||||
|
child_placement = child_placement.grow({border*2, border*2});
|
||||||
|
child_occupied = child_occupied.grow({border*2, border*2});
|
||||||
|
// margin, offsets the placement and grows the occupied area
|
||||||
|
child_placement = child_placement.off(margin.position());
|
||||||
|
child_occupied = child_occupied.grow(margin.position().add(margin.size()));
|
||||||
|
|
||||||
|
// oh yeah also adjust the rect if i was to grow
|
||||||
|
if (adapt_x) rect.w -= padding.x+padding.w + border*2 + margin.x+margin.w;
|
||||||
|
if (adapt_y) rect.h -= padding.y+padding.h + border*2 + margin.y+margin.h;
|
||||||
|
|
||||||
|
// set the size
|
||||||
|
child_placement = child_placement.grow(rect.size());
|
||||||
|
child_occupied = child_occupied.grow(rect.size());
|
||||||
|
|
||||||
|
// 4. Update the parent's origin
|
||||||
|
div.origin_r = {
|
||||||
|
.x = child_occupied.bottom_right().x,
|
||||||
|
.y = origin.y,
|
||||||
|
};
|
||||||
|
div.origin_c = {
|
||||||
|
.x = origin.x,
|
||||||
|
.y = child_occupied.bottom_right().y,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. Update the parent's children bounds
|
||||||
|
if (!child_occupied.bottom_right().in_rect(div.children_bounds)) {
|
||||||
|
// right overflow
|
||||||
|
if (child_occupied.bottom_right().x > div.children_bounds.bottom_right().x) {
|
||||||
|
div.children_bounds.w += child_occupied.bottom_right().x - div.children_bounds.bottom_right().x;
|
||||||
|
}
|
||||||
|
// bottom overflow
|
||||||
|
if (child_occupied.bottom_right().y > div.children_bounds.bottom_right().y) {
|
||||||
|
div.children_bounds.h += child_occupied.bottom_right().y - div.children_bounds.bottom_right().y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 99. return the placement
|
||||||
|
if (child_placement.collides(parent_view)) {
|
||||||
|
return child_placement.off(parent.get_view_off().neg());
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
module ugui;
|
module ugui;
|
||||||
import std::math;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------------------- //
|
||||||
// RECTANGLE //
|
// RECTANGLE //
|
||||||
@ -10,9 +10,6 @@ struct Rect {
|
|||||||
short x, y, w, h;
|
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
|
// return true if rect a contains b
|
||||||
macro bool Rect.contains(Rect a, Rect b)
|
macro bool Rect.contains(Rect a, Rect b)
|
||||||
{
|
{
|
||||||
@ -36,26 +33,11 @@ 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.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
|
// check for empty rect
|
||||||
macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0;
|
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
|
// returns the element-wise addition of r1 and r2
|
||||||
macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
|
macro Rect Rect.add(Rect r1, Rect r2)
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
.x = r1.x + r2.x,
|
.x = r1.x + r2.x,
|
||||||
@ -66,7 +48,7 @@ macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns the element-wise subtraction of r1 and r2
|
// returns the element-wise subtraction of r1 and r2
|
||||||
macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
|
macro Rect Rect.sub(Rect r1, Rect r2)
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
.x = r1.x - r2.x,
|
.x = r1.x - r2.x,
|
||||||
@ -77,7 +59,7 @@ macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns the element-wise multiplication of r1 and r2
|
// returns the element-wise multiplication of r1 and r2
|
||||||
macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*)
|
macro Rect Rect.mul(Rect r1, Rect r2)
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
.x = r1.x * r2.x,
|
.x = r1.x * r2.x,
|
||||||
@ -124,7 +106,7 @@ macro Rect Rect.min(Rect a, Rect b)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Offset a rect by a point
|
// Offset a rect by a point
|
||||||
macro Rect Rect.off(Rect r, Point p) @operator_s(+)
|
macro Rect Rect.off(Rect r, Point p)
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
.x = r.x + p.x,
|
.x = r.x + p.x,
|
||||||
@ -154,35 +136,6 @@ macro Point Rect.bottom_right(Rect r)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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 //
|
// POINT //
|
||||||
@ -198,16 +151,39 @@ 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);
|
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)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
.x = a.x + b.x,
|
||||||
|
.y = a.y + b.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
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};
|
return {
|
||||||
|
.x = a.x - b.x,
|
||||||
|
.y = a.y - b.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
macro Point Point.max(Point a, Point b) => {.x = max(a.x, b.x), .y = max(a.y, b.y)};
|
macro Point Point.neg(Point p) => {-p.x, -p.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;
|
macro Point Point.max(Point a, Point b)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
.x = max(a.x, b.x),
|
||||||
|
.y = max(a.y, b.y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Point Point.min(Point a, Point b)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
.x = min(a.x, b.x),
|
||||||
|
.y = min(a.y, b.y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------------------- //
|
||||||
// COLOR //
|
// COLOR //
|
||||||
@ -238,29 +214,8 @@ macro Color uint.@to_rgba($u)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
|
macro uint Color.to_uint(c)
|
||||||
|
{
|
||||||
// ---------------------------------------------------------------------------------- //
|
uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
|
||||||
// SIZE //
|
return u;
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
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.sat_add(b.min), .max = a.max.sat_add(b.max)};
|
|
||||||
macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min.sat_sub(b.min), .max = a.max.sat_sub(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;
|
|
||||||
132
lib/ugui.c3l/src/ugui_slider.c3
Normal file
132
lib/ugui.c3l/src/ugui_slider.c3
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
module ugui;
|
||||||
|
|
||||||
|
import std::io;
|
||||||
|
import std::math;
|
||||||
|
|
||||||
|
// slider element
|
||||||
|
struct ElemSlider {
|
||||||
|
Rect handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* handle
|
||||||
|
* +----+-----+---------------------+
|
||||||
|
* | |#####| |
|
||||||
|
* +----+-----+---------------------+
|
||||||
|
*/
|
||||||
|
macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
|
||||||
|
=> ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent);
|
||||||
|
<*
|
||||||
|
@require value != null
|
||||||
|
*>
|
||||||
|
fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem* parent = ctx.get_parent()!;
|
||||||
|
Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!;
|
||||||
|
Style* style = ctx.styles.get_style(@str_hash("slider"));
|
||||||
|
|
||||||
|
// 2. Layout
|
||||||
|
elem.bounds = ctx.position_element(parent, size, style);
|
||||||
|
|
||||||
|
// handle width
|
||||||
|
short hw = (short)(elem.bounds.w * hpercent);
|
||||||
|
Rect handle = {
|
||||||
|
.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value),
|
||||||
|
.y = elem.bounds.y,
|
||||||
|
.w = hw,
|
||||||
|
.h = elem.bounds.h,
|
||||||
|
};
|
||||||
|
elem.slider.handle = handle;
|
||||||
|
|
||||||
|
Point m = ctx.input.mouse.pos;
|
||||||
|
elem.events = ctx.get_elem_events(elem);
|
||||||
|
|
||||||
|
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
|
||||||
|
*value = calc_value(elem.bounds.x, m.x, elem.bounds.w, hw);
|
||||||
|
elem.slider.handle.x = calc_slider(elem.bounds.x, elem.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(elem.bounds, parent.div.z_index, &s)!;
|
||||||
|
s.bg = s.primary;
|
||||||
|
s.padding = padding;
|
||||||
|
s.border = {};
|
||||||
|
ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
|
||||||
|
|
||||||
|
return elem.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* +--+
|
||||||
|
* | |
|
||||||
|
* | |
|
||||||
|
* +--+
|
||||||
|
* |##| handle
|
||||||
|
* |##|
|
||||||
|
* +--+
|
||||||
|
* | |
|
||||||
|
* | |
|
||||||
|
* +--+
|
||||||
|
*/
|
||||||
|
macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
|
||||||
|
=> ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent);
|
||||||
|
fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!;
|
||||||
|
Style* style = ctx.styles.get_style(@str_hash("slider"));
|
||||||
|
|
||||||
|
// 1. Fill the element fields
|
||||||
|
if (elem.flags.is_new) {
|
||||||
|
elem.type = ETYPE_SLIDER;
|
||||||
|
} else if (elem.type != ETYPE_SLIDER) {
|
||||||
|
return WRONG_ELEMENT_TYPE?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Layout
|
||||||
|
elem.bounds = ctx.position_element(parent, size, style);
|
||||||
|
|
||||||
|
// handle height
|
||||||
|
short hh = (short)(elem.bounds.h * hpercent);
|
||||||
|
Rect handle = {
|
||||||
|
.x = elem.bounds.x,
|
||||||
|
.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value),
|
||||||
|
.w = elem.bounds.w,
|
||||||
|
.h = hh,
|
||||||
|
};
|
||||||
|
elem.slider.handle = handle;
|
||||||
|
|
||||||
|
Point m = ctx.input.mouse.pos;
|
||||||
|
elem.events = ctx.get_elem_events(elem);
|
||||||
|
|
||||||
|
if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
|
||||||
|
*value = calc_value(elem.bounds.y, m.y, elem.bounds.h, hh);
|
||||||
|
elem.slider.handle.y = calc_slider(elem.bounds.y, elem.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(elem.bounds, parent.div.z_index, &s)!;
|
||||||
|
s.bg = s.primary;
|
||||||
|
s.padding = padding;
|
||||||
|
s.border = {};
|
||||||
|
ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
|
||||||
|
|
||||||
|
return elem.events;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@ -30,6 +30,10 @@ struct SpriteAtlas {
|
|||||||
bool should_update;
|
bool should_update;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ElemSprite {
|
||||||
|
Id id;
|
||||||
|
}
|
||||||
|
|
||||||
// name: some examples are "icons" or "images"
|
// name: some examples are "icons" or "images"
|
||||||
fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, ushort height)
|
fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, ushort height)
|
||||||
{
|
{
|
||||||
@ -40,7 +44,7 @@ fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, usho
|
|||||||
|
|
||||||
this.id = name.hash();
|
this.id = name.hash();
|
||||||
this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!;
|
this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!;
|
||||||
this.sprites.init(allocator::mem, capacity: SRITES_PER_ATLAS);
|
this.sprites.init(allocator::heap(), capacity: SRITES_PER_ATLAS);
|
||||||
this.should_update = false;
|
this.should_update = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,8 +104,50 @@ fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType t
|
|||||||
{
|
{
|
||||||
QOIDesc desc;
|
QOIDesc desc;
|
||||||
|
|
||||||
char[] pixels = qoi::read(allocator::mem, path, &desc, QOIChannels.RGBA)!;
|
char[] pixels = qoi::read(allocator::heap(), path, &desc, QOIChannels.RGBA)!;
|
||||||
defer mem::free(pixels);
|
defer mem::free(pixels);
|
||||||
|
|
||||||
ctx.sprite_atlas.insert(name, type, pixels, (ushort)desc.width, (ushort)desc.height, (ushort)desc.width)!;
|
ctx.sprite_atlas.insert(name, type, pixels, (ushort)desc.width, (ushort)desc.height, (ushort)desc.width)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
macro Ctx.sprite(&ctx, String name, Point off = {0,0}, ...)
|
||||||
|
=> ctx.sprite_id(@compute_id($vasplat), name, off);
|
||||||
|
fn void? Ctx.sprite_id(&ctx, Id id, String name, Point off)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Elem *elem = ctx.get_elem(id, ETYPE_SPRITE)!;
|
||||||
|
|
||||||
|
Style* style = ctx.styles.get_style(@str_hash("sprite"));
|
||||||
|
Sprite* sprite = ctx.sprite_atlas.get(name)!;
|
||||||
|
|
||||||
|
Rect uv = { sprite.u, sprite.v, sprite.w, sprite.h };
|
||||||
|
Rect bounds = { 0, 0, sprite.w, sprite.h };
|
||||||
|
|
||||||
|
elem.bounds = ctx.position_element(parent, bounds.off(off), style);
|
||||||
|
elem.sprite.id = ctx.get_sprite_atlas_id(name);
|
||||||
|
|
||||||
|
// if the bounds are null the element is outside the div view,
|
||||||
|
// no interaction should occur so just return
|
||||||
|
if (elem.bounds.is_null()) return;
|
||||||
|
|
||||||
|
Id tex_id = ctx.sprite_atlas.id;
|
||||||
|
|
||||||
|
return ctx.push_sprite(elem.bounds, uv, tex_id, parent.div.z_index)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds, bool center = false)
|
||||||
|
{
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Sprite* sprite = ctx.sprite_atlas.get(name)!;
|
||||||
|
Id tex_id = ctx.sprite_atlas.id;
|
||||||
|
|
||||||
|
if (center) {
|
||||||
|
Point off = {.x = (bounds.w - sprite.w) / 2, .y = (bounds.h - sprite.h) / 2};
|
||||||
|
bounds = bounds.off(off);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.push_sprite(bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!;
|
||||||
|
}
|
||||||
@ -7,8 +7,10 @@ import std::io;
|
|||||||
// global style, similar to the css box model
|
// global style, similar to the css box model
|
||||||
struct Style { // css box model
|
struct Style { // css box model
|
||||||
Rect padding;
|
Rect padding;
|
||||||
Rect border;
|
|
||||||
Rect margin;
|
Rect margin;
|
||||||
|
ushort border;
|
||||||
|
ushort radius;
|
||||||
|
ushort size;
|
||||||
|
|
||||||
Color bg; // background color
|
Color bg; // background color
|
||||||
Color fg; // foreground color
|
Color fg; // foreground color
|
||||||
@ -16,22 +18,20 @@ struct Style { // css box model
|
|||||||
Color secondary; // secondary color
|
Color secondary; // secondary color
|
||||||
Color accent; // accent color
|
Color accent; // accent color
|
||||||
|
|
||||||
ushort radius;
|
|
||||||
short size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Style DEFAULT_STYLE = {
|
const Style DEFAULT_STYLE = {
|
||||||
.margin = {2, 2, 2, 2},
|
.margin = {2, 2, 2, 2},
|
||||||
.border = {2, 2, 2, 2},
|
.padding = {},
|
||||||
.padding = {1, 1, 1, 1},
|
.border = 2,
|
||||||
.radius = 0,
|
.radius = 12,
|
||||||
.size = 16,
|
.size = 16,
|
||||||
|
|
||||||
.bg = 0x282828ffu.@to_rgba(),
|
.bg = 0x282828ffu.@to_rgba(),
|
||||||
.fg = 0xfbf1c7ffu.@to_rgba(),
|
.fg = 0xfbf1c7ffu.@to_rgba(),
|
||||||
.primary = 0xcc241dffu.@to_rgba(),
|
.primary = 0xcc241dffu.@to_rgba(),
|
||||||
.secondary = 0x458588ffu.@to_rgba(),
|
.secondary = 0x458588ffu.@to_rgba(),
|
||||||
.accent = 0xfabd2fffu.@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
|
// style is stored in a hashmap, each style has an Id that can be generated by a string or whatever
|
||||||
@ -89,8 +89,8 @@ fn int Ctx.import_style_from_file(&ctx, String path)
|
|||||||
* Style can be serialized and deserialized with a subset of CSS
|
* Style can be serialized and deserialized with a subset of CSS
|
||||||
* <style name> {
|
* <style name> {
|
||||||
* padding: left right top bottom;
|
* padding: left right top bottom;
|
||||||
* border: left right top bottom;
|
* margin: left right top bottoms;
|
||||||
* margin: left right top bottom;
|
* border: uint;
|
||||||
* radius: uint;
|
* radius: uint;
|
||||||
* size: uint;
|
* size: uint;
|
||||||
* Color: #RRGGBBAA;
|
* Color: #RRGGBBAA;
|
||||||
@ -104,8 +104,6 @@ fn int Ctx.import_style_from_file(&ctx, String path)
|
|||||||
* The default unit is pixels, but millimeters is also available. The parser function accepts a scale
|
* 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.
|
* 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;
|
module ugui::css;
|
||||||
|
|
||||||
@ -117,10 +115,7 @@ import std::io;
|
|||||||
enum TokenType {
|
enum TokenType {
|
||||||
INVALID,
|
INVALID,
|
||||||
IDENTIFIER,
|
IDENTIFIER,
|
||||||
RCURLY,
|
PUNCT,
|
||||||
LCURLY,
|
|
||||||
SEMICOLON,
|
|
||||||
COLON,
|
|
||||||
NUMBER,
|
NUMBER,
|
||||||
COLOR,
|
COLOR,
|
||||||
EOF,
|
EOF,
|
||||||
@ -153,12 +148,6 @@ fn short Token.to_px(&t, float mm_to_px)
|
|||||||
return (short)(t.value * mm_to_px);
|
return (short)(t.value * mm_to_px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
// LEXER //
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
|
|
||||||
struct Lexer {
|
struct Lexer {
|
||||||
String text;
|
String text;
|
||||||
usz line, col, off;
|
usz line, col, off;
|
||||||
@ -202,15 +191,9 @@ fn Token Lexer.next_token(&lex)
|
|||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
|
case ascii::is_punct_m(lex.peep()) && lex.peep() != '#': // punctuation
|
||||||
|
t.type = PUNCT;
|
||||||
t.text = lex.text[lex.off:1];
|
t.text = lex.text[lex.off:1];
|
||||||
if (lex.advance() == 0) { t.type = INVALID; break; }
|
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
|
case lex.peep() == '#': // color
|
||||||
t.type = COLOR;
|
t.type = COLOR;
|
||||||
@ -219,16 +202,14 @@ fn Token Lexer.next_token(&lex)
|
|||||||
while (ascii::is_alnum_m(lex.peep())) {
|
while (ascii::is_alnum_m(lex.peep())) {
|
||||||
if (lex.advance() == 0) { t.type = INVALID; break; }
|
if (lex.advance() == 0) { t.type = INVALID; break; }
|
||||||
}
|
}
|
||||||
usz h_len = lex.off - hex_start;
|
if (lex.off - hex_start != 8) {
|
||||||
if (h_len != 8 && h_len != 6) {
|
io::eprintfn("CSS lexing error at %d:%d: the only suppported color format is #RRGGBBAA", t.line, t.col);
|
||||||
io::eprintfn("CSS lexing error at %d:%d: the only suppported color formats are #RRGGBBAA or #RRGGBB", t.line, t.col);
|
|
||||||
t.type = INVALID;
|
t.type = INVALID;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
char[10] hex_str = (char[])"0x";
|
char[10] hex_str = (char[])"0x";
|
||||||
hex_str[2:h_len] = lex.text[hex_start:h_len];
|
hex_str[2..] = lex.text[hex_start..lex.off-1];
|
||||||
if (h_len == 6) hex_str[8..9] = "ff"[..];
|
|
||||||
uint? color_hex = ((String)hex_str[..]).to_uint();
|
uint? color_hex = ((String)hex_str[..]).to_uint();
|
||||||
if (catch color_hex) {
|
if (catch color_hex) {
|
||||||
t.type = INVALID;
|
t.type = INVALID;
|
||||||
@ -288,11 +269,6 @@ fn Token Lexer.peep_token(&lex)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
// PARSER //
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
|
|
||||||
struct Parser {
|
struct Parser {
|
||||||
Lexer lex;
|
Lexer lex;
|
||||||
Style style;
|
Style style;
|
||||||
@ -300,6 +276,17 @@ struct Parser {
|
|||||||
float mm_to_px;
|
float mm_to_px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro bool Parser.expect_text(&p, Token* t, TokenType type, String text)
|
||||||
|
{
|
||||||
|
*t = p.lex.next_token();
|
||||||
|
if (t.type == type && t.text == text) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
io::eprintfn("CSS parsing error at %d:%d: expected type:%s text:'%s' but got type:%s text:'%s'",
|
||||||
|
t.line, t.col, type, text, t.type, t.text);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
macro bool Parser.expect(&p, Token* t, TokenType type)
|
macro bool Parser.expect(&p, Token* t, TokenType type)
|
||||||
{
|
{
|
||||||
*t = p.lex.next_token();
|
*t = p.lex.next_token();
|
||||||
@ -308,8 +295,6 @@ macro bool Parser.expect(&p, Token* t, TokenType type)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// style := ( IDENTIFIER "{" property_list "}" )
|
|
||||||
// property_list := property property_list
|
|
||||||
fn bool Parser.parse_style(&p)
|
fn bool Parser.parse_style(&p)
|
||||||
{
|
{
|
||||||
Token t;
|
Token t;
|
||||||
@ -321,7 +306,7 @@ fn bool Parser.parse_style(&p)
|
|||||||
p.style_id = t.text.hash();
|
p.style_id = t.text.hash();
|
||||||
|
|
||||||
// style body
|
// style body
|
||||||
if (p.expect(&t, LCURLY) == false) return false;
|
if (p.expect_text(&t, PUNCT, "{") == false) return false;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (p.parse_property() == false) return false;
|
if (p.parse_property() == false) return false;
|
||||||
@ -329,20 +314,16 @@ fn bool Parser.parse_style(&p)
|
|||||||
if (t.type != IDENTIFIER) break;
|
if (t.type != IDENTIFIER) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p.expect(&t, RCURLY) == false) return false;
|
if (p.expect_text(&t, PUNCT, "}") == false) return false;
|
||||||
|
|
||||||
return true;
|
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)
|
fn bool Parser.parse_property(&p)
|
||||||
{
|
{
|
||||||
Token t, prop;
|
Token t, prop;
|
||||||
if (p.expect(&prop, IDENTIFIER) == false) return false;
|
if (p.expect(&prop, IDENTIFIER) == false) return false;
|
||||||
if (p.expect(&t, COLON) == false) return false;
|
if (p.expect_text(&t, PUNCT, ":") == false) return false;
|
||||||
|
|
||||||
switch (prop.text) {
|
switch (prop.text) {
|
||||||
case "padding":
|
case "padding":
|
||||||
@ -350,11 +331,6 @@ fn bool Parser.parse_property(&p)
|
|||||||
if (p.parse_size(&padding) == false) return false;
|
if (p.parse_size(&padding) == false) return false;
|
||||||
p.style.padding = padding;
|
p.style.padding = padding;
|
||||||
|
|
||||||
case "border":
|
|
||||||
Rect border;
|
|
||||||
if (p.parse_size(&border) == false) return false;
|
|
||||||
p.style.border = border;
|
|
||||||
|
|
||||||
case "margin":
|
case "margin":
|
||||||
Rect margin;
|
Rect margin;
|
||||||
if (p.parse_size(&margin) == false) return false;
|
if (p.parse_size(&margin) == false) return false;
|
||||||
@ -385,34 +361,42 @@ fn bool Parser.parse_property(&p)
|
|||||||
if (p.parse_color(&accent) == false) return false;
|
if (p.parse_color(&accent) == false) return false;
|
||||||
p.style.accent = accent;
|
p.style.accent = accent;
|
||||||
|
|
||||||
case "radius":
|
case "border":
|
||||||
short r;
|
short border;
|
||||||
if (p.parse_number(&r) == false) return false;
|
if (p.parse_number(&border) == false) return false;
|
||||||
if (r < 0) {
|
if (border < 0) {
|
||||||
io::eprintfn("CSS parsing error at %d:%d: 'radius' must be a positive number, got %d", t.line, t.col, r);
|
io::eprintfn("CSS parsing error at %d:%d: 'border' must be a positive number, got %d", t.line, t.col, border);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
p.style.radius = (ushort)r;
|
p.style.border = (ushort)border;
|
||||||
|
|
||||||
|
case "radius":
|
||||||
|
short radius;
|
||||||
|
if (p.parse_number(&radius) == false) return false;
|
||||||
|
if (radius < 0) {
|
||||||
|
io::eprintfn("CSS parsing error at %d:%d: 'radius' must be a positive number, got %d", t.line, t.col, radius);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p.style.radius = (ushort)radius;
|
||||||
|
|
||||||
case "size":
|
case "size":
|
||||||
short s;
|
short size;
|
||||||
if (p.parse_number(&s) == false) return false;
|
if (p.parse_number(&size) == false) return false;
|
||||||
if (s < 0) {
|
if (size < 0) {
|
||||||
io::eprintfn("CSS parsing error at %d:%d: 'size' must be a positive number, got %d", t.line, t.col, s);
|
io::eprintfn("CSS parsing error at %d:%d: 'size' must be a positive number, got %d", t.line, t.col, size);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
p.style.size = (ushort)s;
|
p.style.size = (ushort)size;
|
||||||
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
io::eprintfn("CSS parsing error at %d:%d: '%s' is not a valid property", prop.line, prop.col, prop.text);
|
io::eprintfn("CSS parsing error at %d:%d: '%s' is not a valid property", prop.line, prop.col, prop.text);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (p.expect(&t, SEMICOLON) == false) return false;
|
if (p.expect_text(&t, PUNCT, ";") == false) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// number := NUMBER
|
|
||||||
fn bool Parser.parse_number(&p, short* n)
|
fn bool Parser.parse_number(&p, short* n)
|
||||||
{
|
{
|
||||||
Token t;
|
Token t;
|
||||||
@ -421,7 +405,6 @@ fn bool Parser.parse_number(&p, short* n)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// color := COLOR
|
|
||||||
// FIXME: since '#' is punctuation this cannot be done in parsing but it has to be done in lexing
|
// 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)
|
fn bool Parser.parse_color(&p, Color* c)
|
||||||
{
|
{
|
||||||
@ -431,8 +414,6 @@ fn bool Parser.parse_color(&p, Color* c)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// size := number | number_list
|
|
||||||
// number_list := number number number number
|
|
||||||
fn bool Parser.parse_size(&p, Rect* r)
|
fn bool Parser.parse_size(&p, Rect* r)
|
||||||
{
|
{
|
||||||
short x;
|
short x;
|
||||||
@ -451,7 +432,7 @@ fn bool Parser.parse_size(&p, Rect* r)
|
|||||||
if (p.parse_number(&x) == false) return false;
|
if (p.parse_number(&x) == false) return false;
|
||||||
r.h = x;
|
r.h = x;
|
||||||
return true;
|
return true;
|
||||||
} else if (t.type == SEMICOLON) {
|
} else if (t.type == PUNCT && t.text == ";") {
|
||||||
// just one number, all dimensions are the same
|
// just one number, all dimensions are the same
|
||||||
r.x = r.y = r.w = r.h = x;
|
r.x = r.y = r.w = r.h = x;
|
||||||
return true;
|
return true;
|
||||||
90
lib/ugui.c3l/src/ugui_text.c3
Normal file
90
lib/ugui.c3l/src/ugui_text.c3
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
module ugui;
|
||||||
|
|
||||||
|
import std::io;
|
||||||
|
|
||||||
|
struct ElemText {
|
||||||
|
char[] str;
|
||||||
|
usz cursor; // cursor offset
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Ctx.text_unbounded(&ctx, String text, ...)
|
||||||
|
=> ctx.text_unbounded_id(@compute_id($vasplat), text);
|
||||||
|
fn void? Ctx.text_unbounded_id(&ctx, Id id, String text)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
|
||||||
|
Style* style = ctx.styles.get_style(@str_hash("text"));
|
||||||
|
|
||||||
|
elem.text.str = text;
|
||||||
|
|
||||||
|
// if the element is new or the parent was updated then redo layout
|
||||||
|
Rect text_size = ctx.get_text_bounds(text)!;
|
||||||
|
// 2. Layout
|
||||||
|
elem.bounds = ctx.position_element(parent, text_size, style);
|
||||||
|
if (elem.bounds.is_null()) { return; }
|
||||||
|
|
||||||
|
ctx.push_string(elem.bounds, text, parent.div.z_index, style.fg)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Ctx.text_box(&ctx, Rect size, char[] text, usz* text_len, ...)
|
||||||
|
=> ctx.text_box_id(@compute_id($vasplat), size, text, text_len);
|
||||||
|
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_len)
|
||||||
|
{
|
||||||
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
|
Elem *parent = ctx.get_parent()!;
|
||||||
|
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
|
||||||
|
Style* style = ctx.styles.get_style(@str_hash("text-box"));
|
||||||
|
|
||||||
|
elem.text.str = text;
|
||||||
|
|
||||||
|
// layout the text box
|
||||||
|
elem.bounds = ctx.position_element(parent, size, style);
|
||||||
|
|
||||||
|
// check input and update the text
|
||||||
|
elem.events = ctx.get_elem_events(elem);
|
||||||
|
|
||||||
|
if (elem.events.text_input) {
|
||||||
|
usz l = ctx.input.keyboard.text_len;
|
||||||
|
char[] t = ctx.input.keyboard.text[..l];
|
||||||
|
|
||||||
|
if (l != 0 && l < text.len - *text_len) {
|
||||||
|
text[*text_len..*text_len+l] = t[..];
|
||||||
|
*text_len += l;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.input.keyboard.modkeys.bkspc) {
|
||||||
|
*text_len = *text_len > 0 ? *text_len-1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
elem.text.cursor = *text_len;
|
||||||
|
|
||||||
|
// draw the box
|
||||||
|
short line_height = (short)ctx.font.line_height();
|
||||||
|
Rect text_box = elem.bounds.sub({0,0,0,line_height});
|
||||||
|
Rect input_box = {
|
||||||
|
.x = elem.bounds.x,
|
||||||
|
.y = elem.bounds.y + elem.bounds.h - line_height,
|
||||||
|
.w = elem.bounds.w,
|
||||||
|
.h = line_height,
|
||||||
|
};
|
||||||
|
Rect cursor;
|
||||||
|
Point b = ctx.get_cursor_position((String)text[:elem.text.cursor])!;
|
||||||
|
cursor = {
|
||||||
|
.x = b.x,
|
||||||
|
.y = b.y,
|
||||||
|
.w = 3,
|
||||||
|
.h = line_height,
|
||||||
|
};
|
||||||
|
cursor = cursor.off(elem.bounds.position());
|
||||||
|
|
||||||
|
ctx.push_rect(text_box, parent.div.z_index, style)!;
|
||||||
|
ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg)!;
|
||||||
|
ctx.push_rect(input_box, parent.div.z_index, style)!;
|
||||||
|
ctx.push_rect(cursor, parent.div.z_index, style)!;
|
||||||
|
|
||||||
|
return elem.events;
|
||||||
|
}
|
||||||
338
lib/ugui.c3l/src/vtree.c3
Normal file
338
lib/ugui.c3l/src/vtree.c3
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
module vtree::faults;
|
||||||
|
faultdef CANNOT_SHRINK, INVALID_REFERENCE, TREE_FULL, REFERENCE_NOT_PRESENT, INVALID_ARGUMENT;
|
||||||
|
|
||||||
|
module vtree{ElemType};
|
||||||
|
|
||||||
|
import std::core::mem;
|
||||||
|
import std::io;
|
||||||
|
|
||||||
|
struct VTree {
|
||||||
|
usz elements;
|
||||||
|
ElemType[] vector; // vector of element ids
|
||||||
|
isz[] refs, ordered_refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
macro VTree.ref_is_valid(&tree, isz ref) { return (ref >= 0 && ref < tree.refs.len); }
|
||||||
|
macro VTree.ref_is_present(&tree, isz ref) { return tree.refs[ref] >= 0; }
|
||||||
|
macro VTree.size(&tree) { return tree.refs.len; }
|
||||||
|
|
||||||
|
// macro to zero an elemen
|
||||||
|
macro @zero()
|
||||||
|
{
|
||||||
|
$if $assignable(0, ElemType):
|
||||||
|
return 0;
|
||||||
|
$else
|
||||||
|
return {};
|
||||||
|
$endif
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? VTree.init(&tree, usz size)
|
||||||
|
{
|
||||||
|
tree.vector = mem::new_array(ElemType, size);
|
||||||
|
defer catch { (void)mem::free(tree.vector); }
|
||||||
|
|
||||||
|
tree.refs = mem::new_array(isz, size);
|
||||||
|
defer catch { (void)mem::free(tree.refs); }
|
||||||
|
|
||||||
|
tree.ordered_refs = mem::new_array(isz, size);
|
||||||
|
defer catch { (void)mem::free(tree.ordered_refs); }
|
||||||
|
|
||||||
|
// set all refs to -1, meaning invalid (free) element
|
||||||
|
tree.refs[..] = -1;
|
||||||
|
|
||||||
|
tree.elements = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void VTree.free(&tree)
|
||||||
|
{
|
||||||
|
(void)mem::free(tree.vector);
|
||||||
|
(void)mem::free(tree.refs);
|
||||||
|
(void)mem::free(tree.ordered_refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void VTree.pack(&tree)
|
||||||
|
{
|
||||||
|
// TODO: add a PACKED flag to skip this
|
||||||
|
|
||||||
|
isz free_spot = -1;
|
||||||
|
for (usz 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) {
|
||||||
|
isz old_ref = i;
|
||||||
|
|
||||||
|
// move the item
|
||||||
|
tree.vector[free_spot] = tree.vector[i];
|
||||||
|
tree.refs[free_spot] = tree.refs[i];
|
||||||
|
|
||||||
|
tree.vector[i] = {};
|
||||||
|
tree.refs[i] = -1;
|
||||||
|
|
||||||
|
// and move all references
|
||||||
|
for (usz j = 0; j < tree.size(); j++) {
|
||||||
|
if (tree.refs[j] == old_ref) {
|
||||||
|
tree.refs[j] = free_spot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark the free spot as used
|
||||||
|
free_spot = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void? VTree.resize(&tree, usz newsize)
|
||||||
|
{
|
||||||
|
// return error when shrinking with too many elements
|
||||||
|
if (newsize < tree.elements) {
|
||||||
|
return vtree::faults::CANNOT_SHRINK?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pack the vector when shrinking to avoid data loss
|
||||||
|
if ((int)newsize < tree.size()) {
|
||||||
|
// FIXME: packing destroys all references to elements of vec
|
||||||
|
// so shrinking may cause dangling pointers
|
||||||
|
return vtree::faults::CANNOT_SHRINK?;
|
||||||
|
}
|
||||||
|
|
||||||
|
usz old_size = tree.size();
|
||||||
|
|
||||||
|
tree.vector = ((ElemType*)mem::realloc(tree.vector, newsize*ElemType.sizeof))[:newsize];
|
||||||
|
defer catch { (void)mem::free(tree.vector); }
|
||||||
|
|
||||||
|
tree.refs = ((isz*)mem::realloc(tree.refs, newsize*isz.sizeof))[:newsize];
|
||||||
|
defer catch { (void)mem::free(tree.refs); }
|
||||||
|
|
||||||
|
tree.ordered_refs = ((isz*)mem::realloc(tree.ordered_refs, newsize*isz.sizeof))[:newsize];
|
||||||
|
defer catch { (void)mem::free(tree.ordered_refs); }
|
||||||
|
|
||||||
|
if (newsize > tree.size()) {
|
||||||
|
tree.vector[old_size..newsize-1] = @zero();
|
||||||
|
tree.refs[old_size..newsize-1] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add an element to the tree, return it's ref
|
||||||
|
fn isz? VTree.add(&tree, ElemType elem, isz parent)
|
||||||
|
{
|
||||||
|
// invalid parent
|
||||||
|
if (!tree.ref_is_valid(parent)) {
|
||||||
|
return vtree::faults::INVALID_REFERENCE?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no space left
|
||||||
|
if (tree.elements >= tree.size()) {
|
||||||
|
return vtree::faults::TREE_FULL?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the parent exists
|
||||||
|
// if there are no elements in the tree the first add will set the root
|
||||||
|
if (!tree.ref_is_present(parent) && tree.elements != 0) {
|
||||||
|
return vtree::faults::REFERENCE_NOT_PRESENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the first free spot
|
||||||
|
isz free_spot = -1;
|
||||||
|
for (usz i = 0; i < tree.size(); i++) {
|
||||||
|
if (tree.refs[i] == -1) {
|
||||||
|
free_spot = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (free_spot < 0) {
|
||||||
|
return vtree::faults::TREE_FULL?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
fn usz? VTree.prune(&tree, isz ref)
|
||||||
|
{
|
||||||
|
if (!tree.ref_is_valid(ref)) {
|
||||||
|
return vtree::faults::INVALID_REFERENCE?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree.ref_is_present(ref)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.vector[ref] = @zero();
|
||||||
|
tree.refs[ref] = -1;
|
||||||
|
tree.elements--;
|
||||||
|
|
||||||
|
usz count = 1;
|
||||||
|
for (usz i = 0; tree.elements > 0 && i < tree.size(); i++) {
|
||||||
|
if (tree.refs[i] == ref) {
|
||||||
|
count += tree.prune(i)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usz VTree.nuke(&tree)
|
||||||
|
{
|
||||||
|
tree.vector[0..] = @zero();
|
||||||
|
tree.refs[0..] = -1;
|
||||||
|
usz x = tree.elements;
|
||||||
|
tree.elements = 0;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the size of the subtree starting from ref
|
||||||
|
fn usz? VTree.subtree_size(&tree, isz ref)
|
||||||
|
{
|
||||||
|
if (!tree.ref_is_valid(ref)) {
|
||||||
|
return vtree::faults::INVALID_REFERENCE?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree.ref_is_present(ref)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
usz count = 1;
|
||||||
|
for (usz i = 0; i < tree.size(); i++) {
|
||||||
|
// only root has the reference to itself
|
||||||
|
if (tree.refs[i] == ref && ref != i) {
|
||||||
|
count += tree.subtree_size(i)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate through the first level children, use a cursor like strtok_r
|
||||||
|
fn isz? VTree.children_it(&tree, isz parent, isz *cursor)
|
||||||
|
{
|
||||||
|
if (cursor == null) {
|
||||||
|
return vtree::faults::INVALID_ARGUMENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the cursor is out of bounds then we are done for sure
|
||||||
|
if (!tree.ref_is_valid(*cursor)) {
|
||||||
|
return vtree::faults::INVALID_REFERENCE?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// same for the parent, if it's invalid it can't have children
|
||||||
|
if (!tree.ref_is_valid(parent) || !tree.ref_is_present(parent)) {
|
||||||
|
return vtree::faults::INVALID_REFERENCE?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the first child, update the cursor and return the ref
|
||||||
|
for (isz 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]
|
||||||
|
*/
|
||||||
|
fn isz? VTree.level_order_it(&tree, isz ref, isz *cursor)
|
||||||
|
{
|
||||||
|
if (cursor == null) {
|
||||||
|
return vtree::faults::INVALID_ARGUMENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
isz[] 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;
|
||||||
|
queue[..] = -1;
|
||||||
|
|
||||||
|
// iterate through the queue appending found children
|
||||||
|
isz pos, off;
|
||||||
|
do {
|
||||||
|
// printf ("ref=%d\n", ref);
|
||||||
|
for (isz 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 (tree.ref_is_valid(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 (tree.ref_is_valid(*cursor)) {
|
||||||
|
return queue[(*cursor)++];
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isz? VTree.parentof(&tree, isz ref)
|
||||||
|
{
|
||||||
|
if (!tree.ref_is_valid(ref)) {
|
||||||
|
return vtree::faults::INVALID_REFERENCE?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree.ref_is_present(ref)) {
|
||||||
|
return vtree::faults::REFERENCE_NOT_PRESENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree.refs[ref];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ElemType? VTree.get(&tree, isz ref)
|
||||||
|
{
|
||||||
|
if (!tree.ref_is_valid(ref)) {
|
||||||
|
return vtree::faults::INVALID_REFERENCE?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree.ref_is_present(ref)) {
|
||||||
|
return vtree::faults::REFERENCE_NOT_PRESENT?;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree.vector[ref];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void VTree.print(&tree)
|
||||||
|
{
|
||||||
|
for (isz i = 0; i < tree.size(); i++) {
|
||||||
|
if (tree.refs[i] == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
io::printf("[%d] {parent=%d, data=", i, tree.refs[i]);
|
||||||
|
io::print(tree.vector[i]);
|
||||||
|
io::printn("}");
|
||||||
|
}
|
||||||
|
}
|
||||||
1
lib/vendor
Submodule
1
lib/vendor
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit db006221a8af625630fdb8b56707f3d07d4314a2
|
||||||
20
project.json
Normal file
20
project.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"langrev": "1",
|
||||||
|
"warnings": ["no-unused"],
|
||||||
|
"dependency-search-paths": ["lib", "lib/vendor/libraries"],
|
||||||
|
"dependencies": ["sdl3", "ugui"],
|
||||||
|
"features": [],
|
||||||
|
"authors": ["Alessandro Mauri <ale@shitposting.expert>"],
|
||||||
|
"version": "0.1.0",
|
||||||
|
"sources": ["src/**"],
|
||||||
|
"output": "build",
|
||||||
|
"target": "linux-x64",
|
||||||
|
"targets": {
|
||||||
|
"ugui": {
|
||||||
|
"type": "executable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cpu": "native",
|
||||||
|
"opt": "O0",
|
||||||
|
"debug-info": "full"
|
||||||
|
}
|
||||||
0
resources/.gitkeep
Normal file
0
resources/.gitkeep
Normal file
BIN
resources/hack-nerd.ttf
Normal file
BIN
resources/hack-nerd.ttf
Normal file
Binary file not shown.
33
resources/shaders/Makefile
Normal file
33
resources/shaders/Makefile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
SOURCE_DIR := ./source
|
||||||
|
COMPILED_DIR := ./compiled
|
||||||
|
|
||||||
|
SOURCE_FILES := $(wildcard $(SOURCE_DIR)/*.glsl)
|
||||||
|
|
||||||
|
COMPILED_FILES := $(patsubst $(SOURCE_DIR)/%.glsl,$(COMPILED_DIR)/%.spv,$(SOURCE_FILES))
|
||||||
|
|
||||||
|
all: $(COMPILED_FILES)
|
||||||
|
@echo "Compiling shaders from $(SOURCE_DIR) -> $(COMPILED_DIR)"
|
||||||
|
|
||||||
|
$(COMPILED_DIR)/%.spv: $(SOURCE_DIR)/%.glsl
|
||||||
|
@mkdir -p $(COMPILED_DIR)
|
||||||
|
@stage=$$(basename $< .glsl | cut -d. -f2); \
|
||||||
|
if [ "$$stage" = "frag" ] || [ "$$stage" = "vert" ]; then \
|
||||||
|
echo "$$stage $(notdir $<) > $(notdir $@)"; \
|
||||||
|
glslc -O0 -g -fshader-stage=$$stage $< -o $@; \
|
||||||
|
else \
|
||||||
|
echo "Skipping $<: unsupported stage $$stage"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
$(COMPILED_DIR):
|
||||||
|
mkdir -p $(COMPILED_DIR)
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf $(COMPILED_DIR)
|
||||||
|
|
||||||
|
.PHONY: tree
|
||||||
|
tree:
|
||||||
|
tree $(COMPILED_DIR)
|
||||||
|
|
||||||
|
.PHONY: compile_all
|
||||||
|
compile_all: clean all tree
|
||||||
21
resources/shaders/source/font.frag.glsl
Normal file
21
resources/shaders/source/font.frag.glsl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform Viewport {
|
||||||
|
ivec2 view;
|
||||||
|
};
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D tx;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 uv;
|
||||||
|
layout(location = 1) in vec4 color;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec2 ts = textureSize(tx, 0);
|
||||||
|
vec2 fts = vec2(ts);
|
||||||
|
vec2 real_uv = uv / fts;
|
||||||
|
|
||||||
|
vec4 opacity = texture(tx, real_uv);
|
||||||
|
fragColor = vec4(color.rgb, color.a*opacity.r);
|
||||||
|
}
|
||||||
35
resources/shaders/source/msdf.frag.glsl
Normal file
35
resources/shaders/source/msdf.frag.glsl
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform Viewport {
|
||||||
|
ivec2 view;
|
||||||
|
};
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D tx;
|
||||||
|
|
||||||
|
const float PX_RANGE = 4.0f;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 uv;
|
||||||
|
layout(location = 1) in vec4 color;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
float screen_px_range(vec2 uv) {
|
||||||
|
vec2 unit_range = vec2(PX_RANGE)/vec2(textureSize(tx, 0));
|
||||||
|
vec2 texel_size = vec2(1.0)/fwidth(uv);
|
||||||
|
return max(0.5*dot(unit_range, texel_size), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float median(float r, float g, float b) {
|
||||||
|
return max(min(r, g), min(max(r, g), b));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
ivec2 ts = textureSize(tx, 0);
|
||||||
|
vec2 fts = vec2(ts);
|
||||||
|
vec2 real_uv = uv / fts;
|
||||||
|
|
||||||
|
vec3 msd = texture(tx, real_uv).rgb;
|
||||||
|
float sd = median(msd.r, msd.g, msd.b);
|
||||||
|
float distance = screen_px_range(real_uv)*(sd - 0.5);
|
||||||
|
float opacity = clamp(distance + 0.5, 0.0, 1.0);
|
||||||
|
fragColor = color * opacity;
|
||||||
|
}
|
||||||
32
resources/shaders/source/rect.frag.glsl
Normal file
32
resources/shaders/source/rect.frag.glsl
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform Viewport {
|
||||||
|
ivec2 view;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 in_color;
|
||||||
|
layout(location = 1) in vec4 in_quad_size; // x,y, w,h
|
||||||
|
layout(location = 2) in float in_radius;
|
||||||
|
layout(location = 3) in float thickness;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
// SDF for a rounded rectangle given the centerpoint, half size and radius, all in pixels
|
||||||
|
float sdf_rr(vec2 p, vec2 half_size, float radius) {
|
||||||
|
vec2 q = abs(p) - half_size + radius;
|
||||||
|
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float smoothness = 0.9;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 centerpoint = in_quad_size.xy + in_quad_size.zw * 0.5;
|
||||||
|
vec2 half_size = in_quad_size.zw * 0.5;
|
||||||
|
float distance = -sdf_rr(vec2(gl_FragCoord) - centerpoint, half_size, in_radius);
|
||||||
|
|
||||||
|
float alpha_out = smoothstep(0.0-smoothness, 0.0, distance);
|
||||||
|
float alpha_in = 1.0 - smoothstep(thickness-smoothness, thickness, distance);
|
||||||
|
|
||||||
|
fragColor = vec4(in_color.rgb, in_color.a * alpha_out * alpha_in);
|
||||||
|
}
|
||||||
31
resources/shaders/source/rect.vert.glsl
Normal file
31
resources/shaders/source/rect.vert.glsl
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 1, binding = 0) uniform Viewport {
|
||||||
|
ivec2 view;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(location = 0) in ivec2 position;
|
||||||
|
layout(location = 1) in ivec4 attr; // quad x,y,w,h
|
||||||
|
layout(location = 2) in ivec2 uv; // x,y in the texture
|
||||||
|
layout(location = 3) in uvec4 color;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
layout(location = 1) out vec4 out_quad_size;
|
||||||
|
layout(location = 2) out float out_radius;
|
||||||
|
layout(location = 3) out float out_thickness;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
// vertex position
|
||||||
|
ivec2 px_pos = attr.xy + position.xy * attr.zw;
|
||||||
|
vec2 clip_pos;
|
||||||
|
clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0;
|
||||||
|
clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0);
|
||||||
|
|
||||||
|
gl_Position = vec4(clip_pos, 0.0, 1.0);
|
||||||
|
|
||||||
|
out_color = vec4(color) / 255.0;
|
||||||
|
out_quad_size = vec4(attr);
|
||||||
|
out_radius = float(abs(uv.x));
|
||||||
|
out_thickness = float(uv.y - uv.x);
|
||||||
|
}
|
||||||
18
resources/shaders/source/sprite.frag.glsl
Normal file
18
resources/shaders/source/sprite.frag.glsl
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform Viewport {
|
||||||
|
ivec2 view;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 uv;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D tx;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec2 ts = textureSize(tx, 0);
|
||||||
|
vec2 fts = vec2(ts);
|
||||||
|
vec2 real_uv = uv / fts;
|
||||||
|
fragColor = texture(tx, real_uv);
|
||||||
|
}
|
||||||
28
resources/shaders/source/sprite.vert.glsl
Normal file
28
resources/shaders/source/sprite.vert.glsl
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 1, binding = 0) uniform Viewport {
|
||||||
|
ivec2 view;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(location = 0) in ivec2 position;
|
||||||
|
layout(location = 1) in ivec4 attr; // quad x,y,w,h
|
||||||
|
layout(location = 2) in ivec2 in_uv;
|
||||||
|
layout(location = 3) in uvec4 color;
|
||||||
|
|
||||||
|
layout(location = 0) out vec2 out_uv;
|
||||||
|
layout(location = 1) out vec4 out_color;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
// vertex position
|
||||||
|
ivec2 px_pos = attr.xy + position.xy * attr.zw;
|
||||||
|
vec2 clip_pos;
|
||||||
|
clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0;
|
||||||
|
clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0);
|
||||||
|
|
||||||
|
gl_Position = vec4(clip_pos, 0.0, 1.0);
|
||||||
|
|
||||||
|
vec2 px_uv = in_uv.xy + position.xy * attr.zw;
|
||||||
|
out_uv = vec2(px_uv);
|
||||||
|
out_color = vec4(color) / 255.0;
|
||||||
|
}
|
||||||
68
resources/style.css
Normal file
68
resources/style.css
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
default {
|
||||||
|
bg: #282828ff;
|
||||||
|
fg: #fbf1c7ff;
|
||||||
|
primary: #cc241dff;
|
||||||
|
secondary: #458588ff;
|
||||||
|
accent: #fabd2fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 2 2 2 2;
|
||||||
|
border: 2;
|
||||||
|
radius: 10;
|
||||||
|
|
||||||
|
bg: #3c3836ff;
|
||||||
|
fg: #fbf1c7ff;
|
||||||
|
primary: #cc241dff;
|
||||||
|
secondary: #458588ff;
|
||||||
|
accent: #fabd2fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button-active {
|
||||||
|
margin: 2 2 2 2;
|
||||||
|
border: 2;
|
||||||
|
radius: 10;
|
||||||
|
|
||||||
|
bg: #504945ff;
|
||||||
|
fg: #fbf1c7ff;
|
||||||
|
primary: #cc241dff;
|
||||||
|
secondary: #cc241dff;
|
||||||
|
accent: #fabd2fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox {
|
||||||
|
margin: 2 2 2 2;
|
||||||
|
border: 2;
|
||||||
|
radius: 10;
|
||||||
|
size: 16;
|
||||||
|
bg: #3c3836ff;
|
||||||
|
fg: #fbf1c7ff;
|
||||||
|
primary: #cc241dff;
|
||||||
|
secondary: #458588ff;
|
||||||
|
accent: #fabd2fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle {
|
||||||
|
margin: 2 2 2 2;
|
||||||
|
border: 2;
|
||||||
|
radius: 0;
|
||||||
|
size: 16;
|
||||||
|
bg: #3c3836ff;
|
||||||
|
fg: #fbf1c7ff;
|
||||||
|
primary: #cc241dff;
|
||||||
|
secondary: #458588ff;
|
||||||
|
accent: #fabd2fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
slider {
|
||||||
|
margin: 2 2 2 2;
|
||||||
|
padding: 4 4 4 4;
|
||||||
|
border: 1;
|
||||||
|
radius: 4;
|
||||||
|
size: 8;
|
||||||
|
bg: #3c3836ff;
|
||||||
|
fg: #fbf1c7ff;
|
||||||
|
primary: #cc241dff;
|
||||||
|
secondary: #458588ff;
|
||||||
|
accent: #fabd2fff;
|
||||||
|
}
|
||||||
BIN
resources/tick_sdf.qoi
Normal file
BIN
resources/tick_sdf.qoi
Normal file
Binary file not shown.
BIN
resources/tux.qoi
Normal file
BIN
resources/tux.qoi
Normal file
Binary file not shown.
BIN
resources/tux_inv.qoi
Normal file
BIN
resources/tux_inv.qoi
Normal file
Binary file not shown.
0
src/.gitkeep
Normal file
0
src/.gitkeep
Normal file
217
src/cmd.c3
217
src/cmd.c3
@ -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()})!!;
|
|
||||||
|
|
||||||
365
src/layout.c3
365
src/layout.c3
@ -1,365 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
bitstruct ResizeDirection : char {
|
|
||||||
bool x : 0;
|
|
||||||
bool y : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
bitstruct ResizeAnchor : char {
|
|
||||||
bool top : 0;
|
|
||||||
bool bottom : 1;
|
|
||||||
bool left : 2;
|
|
||||||
bool right : 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
314
src/main.c3
Normal file
314
src/main.c3
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
import std::io;
|
||||||
|
import vtree;
|
||||||
|
import cache;
|
||||||
|
import ugui;
|
||||||
|
import std::time;
|
||||||
|
import std::collections::ringbuffer;
|
||||||
|
import std::core::string;
|
||||||
|
import std::ascii;
|
||||||
|
import sdlrenderer::ren;
|
||||||
|
import sdl3::sdl;
|
||||||
|
|
||||||
|
alias Times = ringbuffer::RingBuffer{time::NanoDuration[128]};
|
||||||
|
|
||||||
|
fn void Times.print_stats(×)
|
||||||
|
{
|
||||||
|
if (times.written == 0);
|
||||||
|
time::NanoDuration min, max, avg, x;
|
||||||
|
min = times.get(0);
|
||||||
|
for (usz i = 0; i < times.written; i++) {
|
||||||
|
x = times.get(i);
|
||||||
|
if (x < min) { min = x; }
|
||||||
|
if (x > max) { max = x; }
|
||||||
|
avg += x;
|
||||||
|
}
|
||||||
|
avg = (NanoDuration)((ulong)avg/times.written);
|
||||||
|
|
||||||
|
io::printfn("min=%s, max=%s, avg=%s", min, max, avg);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TimeStats {
|
||||||
|
time::NanoDuration min, max, avg;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn TimeStats Times.get_stats(×)
|
||||||
|
{
|
||||||
|
if (times.written == 0) return {};
|
||||||
|
time::NanoDuration min, max, avg, x;
|
||||||
|
min = times.get(0);
|
||||||
|
for (usz i = 0; i < times.written; i++) {
|
||||||
|
x = times.get(i);
|
||||||
|
if (x < min) { min = x; }
|
||||||
|
if (x > max) { max = x; }
|
||||||
|
avg += x;
|
||||||
|
}
|
||||||
|
avg = (NanoDuration)((ulong)avg/times.written);
|
||||||
|
|
||||||
|
return {.min = min, .max = max, .avg = avg};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const char[*] MSDF_FS_PATH = "resources/shaders/compiled/msdf.frag.spv";
|
||||||
|
const char[*] SPRITE_FS_PATH = "resources/shaders/compiled/sprite.frag.spv";
|
||||||
|
const char[*] FONT_FS_PATH = "resources/shaders/compiled/font.frag.spv";
|
||||||
|
const char[*] RECT_FS_PATH = "resources/shaders/compiled/rect.frag.spv";
|
||||||
|
|
||||||
|
const char[*] SPRITE_VS_PATH = "resources/shaders/compiled/sprite.vert.spv";
|
||||||
|
const char[*] RECT_VS_PATH = "resources/shaders/compiled/rect.vert.spv";
|
||||||
|
|
||||||
|
const char[*] STYLESHEET_PATH = "resources/style.css";
|
||||||
|
|
||||||
|
fn int main(String[] args)
|
||||||
|
{
|
||||||
|
ugui::Ctx ui;
|
||||||
|
ui.init()!!;
|
||||||
|
defer ui.free();
|
||||||
|
|
||||||
|
ren::Renderer ren;
|
||||||
|
ren.init("Ugui Test", 800, 600, true);
|
||||||
|
defer ren.free();
|
||||||
|
ui.input_window_size(800, 600)!!;
|
||||||
|
|
||||||
|
//
|
||||||
|
// FONT LOADING
|
||||||
|
//
|
||||||
|
{
|
||||||
|
// import font in the ui context
|
||||||
|
ui.load_font("font1", "resources/hack-nerd.ttf", 16)!!;
|
||||||
|
|
||||||
|
// create the rendering pipeline
|
||||||
|
ren.font_atlas_id = ui.get_font_id("font1");
|
||||||
|
ren.load_spirv_shader_from_file("UGUI_PIPELINE_FONT", SPRITE_VS_PATH, FONT_FS_PATH, 1, 0);
|
||||||
|
ren.create_pipeline("UGUI_PIPELINE_FONT", SPRITE);
|
||||||
|
|
||||||
|
// send the atlas to the gpu
|
||||||
|
Atlas* font_atlas = ui.get_font_atlas("font1")!!;
|
||||||
|
ren.new_texture("font1", JUST_ALPHA, font_atlas.buffer, font_atlas.width, font_atlas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ICON LOADING
|
||||||
|
//
|
||||||
|
{
|
||||||
|
// create the atlas and upload some icons
|
||||||
|
ui.sprite_atlas_create("icons", AtlasType.ATLAS_R8G8B8A8, 512, 512)!!;
|
||||||
|
ui.import_sprite_file_qoi("tux", "resources/tux.qoi")!!;
|
||||||
|
ui.import_sprite_file_qoi("tick", "resources/tick_sdf.qoi", SpriteType.SPRITE_MSDF)!!;
|
||||||
|
|
||||||
|
// create the rendering pipelines
|
||||||
|
ren.sprite_atlas_id = ui.get_sprite_atlas_id("icons");
|
||||||
|
// normal sprite pipeline
|
||||||
|
ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE", SPRITE_VS_PATH, SPRITE_FS_PATH, 1, 0);
|
||||||
|
ren.create_pipeline("UGUI_PIPELINE_SPRITE", SPRITE);
|
||||||
|
// msdf sprite pipeline
|
||||||
|
ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE_MSDF", SPRITE_VS_PATH, MSDF_FS_PATH, 1, 0);
|
||||||
|
ren.create_pipeline("UGUI_PIPELINE_SPRITE_MSDF", SPRITE);
|
||||||
|
|
||||||
|
// upload the atlas to the gpu
|
||||||
|
Atlas atlas = ui.sprite_atlas.atlas;
|
||||||
|
ren.new_texture("icons", FULL_COLOR, atlas.buffer, atlas.width, atlas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// RECT PIPELINE
|
||||||
|
//
|
||||||
|
ren.load_spirv_shader_from_file("UGUI_PIPELINE_RECT", RECT_VS_PATH, RECT_FS_PATH, 0, 0);
|
||||||
|
ren.create_pipeline("UGUI_PIPELINE_RECT", RECT);
|
||||||
|
|
||||||
|
|
||||||
|
// CSS INPUT
|
||||||
|
io::printfn("imported %d styles", ui.import_style_from_file(STYLESHEET_PATH));
|
||||||
|
|
||||||
|
isz frame;
|
||||||
|
double fps;
|
||||||
|
bool toggle = true;
|
||||||
|
time::Clock clock;
|
||||||
|
time::Clock fps_clock;
|
||||||
|
time::Clock sleep_clock;
|
||||||
|
Times ui_times;
|
||||||
|
Times draw_times;
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// MAIN LOOP
|
||||||
|
//
|
||||||
|
sdl::start_text_input(ren.win);
|
||||||
|
|
||||||
|
sdl::Event e;
|
||||||
|
bool quit = false;
|
||||||
|
ugui::ModKeys mod;
|
||||||
|
ugui::MouseButtons btn;
|
||||||
|
while (!quit) {
|
||||||
|
clock.mark();
|
||||||
|
fps_clock.mark();
|
||||||
|
sleep_clock.mark();
|
||||||
|
|
||||||
|
do {
|
||||||
|
switch (e.type) {
|
||||||
|
case EVENT_QUIT:
|
||||||
|
quit = true;
|
||||||
|
case EVENT_KEY_UP: nextcase;
|
||||||
|
case EVENT_KEY_DOWN:
|
||||||
|
mod.rctrl = e.key.key == K_RCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.rctrl;
|
||||||
|
mod.lctrl = e.key.key == K_LCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.lctrl;
|
||||||
|
mod.bkspc = e.key.key == K_BACKSPACE ? !!(e.type == EVENT_KEY_DOWN) : mod.bkspc;
|
||||||
|
|
||||||
|
// pressing ctrl+key or alt+key does not generate a character as such no
|
||||||
|
// TEXT_INPUT event is generated. When those keys are pressed we have to
|
||||||
|
// do manual text input, bummer
|
||||||
|
if (e.type == EVENT_KEY_DOWN && (mod.lctrl || mod.rctrl)) {
|
||||||
|
if (ascii::is_alnum_m((uint)e.key.key)) {
|
||||||
|
ui.input_char((char)e.key.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.type == EVENT_KEY_DOWN && e.key.key == K_RETURN) ui.input_char('\n');
|
||||||
|
|
||||||
|
case EVENT_TEXT_INPUT:
|
||||||
|
ui.input_text_utf8(e.text.text.str_view());
|
||||||
|
case EVENT_WINDOW_RESIZED:
|
||||||
|
ui.input_window_size((short)e.window.data1, (short)e.window.data2)!!;
|
||||||
|
case EVENT_WINDOW_FOCUS_GAINED:
|
||||||
|
ui.input_changefocus(true);
|
||||||
|
case EVENT_WINDOW_FOCUS_LOST:
|
||||||
|
ui.input_changefocus(false);
|
||||||
|
case EVENT_MOUSE_MOTION:
|
||||||
|
ui.input_mouse_abs((short)e.motion.x, (short)e.motion.y);
|
||||||
|
case EVENT_MOUSE_WHEEL:
|
||||||
|
ui.input_mouse_wheel((short)e.wheel.integer_x, (short)e.wheel.integer_y);
|
||||||
|
case EVENT_MOUSE_BUTTON_DOWN: nextcase;
|
||||||
|
case EVENT_MOUSE_BUTTON_UP:
|
||||||
|
sdl::MouseButtonFlags mb = sdl::get_mouse_state(null, null);
|
||||||
|
btn = {
|
||||||
|
.btn_left = !!(mb & BUTTON_LMASK),
|
||||||
|
.btn_right = !!(mb & BUTTON_RMASK),
|
||||||
|
.btn_middle = !!(mb & BUTTON_MMASK),
|
||||||
|
.btn_4 = !!(mb & BUTTON_X1MASK),
|
||||||
|
.btn_5 = !!(mb & BUTTON_X2MASK),
|
||||||
|
};
|
||||||
|
case EVENT_POLL_SENTINEL: break;
|
||||||
|
default:
|
||||||
|
io::eprintfn("unhandled event: %s", e.type);
|
||||||
|
}
|
||||||
|
} while(sdl::poll_event(&e));
|
||||||
|
ui.input_mod_keys(mod);
|
||||||
|
ui.input_mouse_button(btn);
|
||||||
|
|
||||||
|
/* End Input Handling */
|
||||||
|
|
||||||
|
/* Start UI Handling */
|
||||||
|
ui.frame_begin()!!;
|
||||||
|
|
||||||
|
if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true;
|
||||||
|
|
||||||
|
ui.div_begin({.w=-100})!!;
|
||||||
|
{
|
||||||
|
ui.layout_set_column()!!;
|
||||||
|
if (ui.button({0,0,30,30}, toggle)!!.mouse_press) {
|
||||||
|
io::printn("press button0");
|
||||||
|
toggle = !toggle;
|
||||||
|
}
|
||||||
|
//ui.layout_next_column()!!;
|
||||||
|
if (ui.button({0,0,30,30})!!.mouse_press) {
|
||||||
|
io::printn("press button1");
|
||||||
|
}
|
||||||
|
//ui.layout_next_column()!!;
|
||||||
|
if (ui.button({0,0,30,30})!!.mouse_release) {
|
||||||
|
io::printn("release button2");
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.layout_set_row()!!;
|
||||||
|
ui.layout_next_row()!!;
|
||||||
|
static float rf, gf, bf, af;
|
||||||
|
ui.slider_ver({0,0,30,100}, &rf)!!;
|
||||||
|
ui.slider_ver({0,0,30,100}, &gf)!!;
|
||||||
|
ui.slider_ver({0,0,30,100}, &bf)!!;
|
||||||
|
ui.slider_ver({0,0,30,100}, &af)!!;
|
||||||
|
|
||||||
|
ui.layout_next_column()!!;
|
||||||
|
ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!;
|
||||||
|
|
||||||
|
ui.layout_next_column()!!;
|
||||||
|
ui.button_label("Continua!")!!;
|
||||||
|
|
||||||
|
ui.layout_next_row()!!;
|
||||||
|
static bool check;
|
||||||
|
ui.checkbox("", {}, &check, "tick")!!;
|
||||||
|
ui.checkbox("", {}, &check)!!;
|
||||||
|
ui.toggle("", {}, &toggle)!!;
|
||||||
|
};
|
||||||
|
ui.sprite("tux")!!;
|
||||||
|
|
||||||
|
static char[128] text_box = "ciao mamma";
|
||||||
|
static usz text_len = "ciao mamma".len;
|
||||||
|
ui.text_box({0,0,200,200}, text_box[..], &text_len)!!;
|
||||||
|
|
||||||
|
ui.div_end()!!;
|
||||||
|
|
||||||
|
ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!;
|
||||||
|
{
|
||||||
|
ui.layout_set_column()!!;
|
||||||
|
static float slider2 = 0.5;
|
||||||
|
if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) {
|
||||||
|
io::printfn("other slider: %f", slider2);
|
||||||
|
}
|
||||||
|
ui.button({0,0,50,50})!!;
|
||||||
|
ui.button({0,0,50,50})!!;
|
||||||
|
ui.button({0,0,50,50})!!;
|
||||||
|
ui.button({0,0,50,50})!!;
|
||||||
|
if (toggle) {
|
||||||
|
ui.button({0,0,50,50})!!;
|
||||||
|
ui.button({0,0,50,50})!!;
|
||||||
|
ui.button({0,0,50,50})!!;
|
||||||
|
ui.button({0,0,50,50})!!;
|
||||||
|
}
|
||||||
|
ui.layout_next_column()!!;
|
||||||
|
ui.layout_set_row()!!;
|
||||||
|
static float f1, f2;
|
||||||
|
ui.slider_hor({0,0,100,30}, &f1)!!;
|
||||||
|
ui.slider_hor({0,0,100,30}, &f2)!!;
|
||||||
|
};
|
||||||
|
ui.div_end()!!;
|
||||||
|
|
||||||
|
// Timings counter
|
||||||
|
TimeStats dts = draw_times.get_stats();
|
||||||
|
TimeStats uts = ui_times.get_stats();
|
||||||
|
|
||||||
|
ui.layout_set_floating()!!;
|
||||||
|
// FIXME: I cannot anchor shit to the bottom of the screen
|
||||||
|
ui.div_begin({0, ui.height-150, -300, 150})!!;
|
||||||
|
{
|
||||||
|
ui.layout_set_column()!!;
|
||||||
|
ui.text_unbounded(string::tformat("frame %d, fps = %.2f", frame, fps))!!;
|
||||||
|
ui.text_unbounded(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!;
|
||||||
|
ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[..]))!!;
|
||||||
|
};
|
||||||
|
ui.div_end()!!;
|
||||||
|
|
||||||
|
ui.frame_end()!!;
|
||||||
|
/* End UI Handling */
|
||||||
|
ui_times.push(clock.mark());
|
||||||
|
//ui_times.print_stats();
|
||||||
|
|
||||||
|
|
||||||
|
/* Start UI Drawing */
|
||||||
|
ren.begin_render(true);
|
||||||
|
|
||||||
|
ren.render_ugui(&ui.cmd_queue);
|
||||||
|
|
||||||
|
ren.end_render();
|
||||||
|
|
||||||
|
draw_times.push(clock.mark());
|
||||||
|
//draw_times.print_stats();
|
||||||
|
/* End Drawing */
|
||||||
|
|
||||||
|
// wait for the next event, timeout after 100ms
|
||||||
|
|
||||||
|
sdl::wait_event_timeout(&e, (int)(100.0-sleep_clock.mark().to_ms()-0.5));
|
||||||
|
|
||||||
|
fps = 1.0 / fps_clock.mark().to_sec();
|
||||||
|
frame++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
367
src/mtree.c3
367
src/mtree.c3
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
962
src/renderer.c3
Normal file
962
src/renderer.c3
Normal file
@ -0,0 +1,962 @@
|
|||||||
|
module idlist{Type};
|
||||||
|
|
||||||
|
// extends the List type to search elements that have a type.id property
|
||||||
|
// TODO: check that type has an id
|
||||||
|
|
||||||
|
import std::collections::list;
|
||||||
|
|
||||||
|
alias IdList = List{Type};
|
||||||
|
|
||||||
|
macro Type* IdList.get_from_name(&self, String name)
|
||||||
|
{
|
||||||
|
return self.get_from_id(name.hash());
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Type* IdList.get_from_id(&self, id)
|
||||||
|
{
|
||||||
|
foreach(&s: self) {
|
||||||
|
if (s.id == id) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module sdlrenderer::ren;
|
||||||
|
|
||||||
|
// 2D renderer for ugui, based on SDL3 using the new GPU API
|
||||||
|
|
||||||
|
import std::io;
|
||||||
|
import std::core::mem;
|
||||||
|
import sdl3::sdl;
|
||||||
|
import idlist;
|
||||||
|
import ugui;
|
||||||
|
|
||||||
|
struct Shader {
|
||||||
|
sdl::GPUShader* frag;
|
||||||
|
sdl::GPUShader* vert;
|
||||||
|
ugui::Id id;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Pipeline {
|
||||||
|
sdl::GPUGraphicsPipeline* pipeline;
|
||||||
|
ugui::Id id;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Texture {
|
||||||
|
sdl::GPUTexture* texture;
|
||||||
|
sdl::GPUSampler* sampler;
|
||||||
|
ushort width, height;
|
||||||
|
ugui::Id id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GPU buffers that contain quad info, the size is determined by MAX_QUAD_BATCH
|
||||||
|
const int MAX_QUAD_BATCH = 2048;
|
||||||
|
struct QuadBuffer {
|
||||||
|
sdl::GPUBuffer* vert_buf; // on-gpu vertex buffer
|
||||||
|
sdl::GPUBuffer* idx_buf; // on-gpu index buffer
|
||||||
|
sdl::GPUBuffer* attr_buf; // on-gpu quad attribute buffer
|
||||||
|
|
||||||
|
sdl::GPUTransferBuffer* attr_ts;
|
||||||
|
|
||||||
|
QuadAttributes[] attr_ts_mapped;
|
||||||
|
|
||||||
|
// how many quads are currently stored
|
||||||
|
int count;
|
||||||
|
|
||||||
|
bool initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
alias ShaderList = IdList{Shader};
|
||||||
|
alias PipelineList = IdList{Pipeline};
|
||||||
|
alias TextureList = IdList{Texture};
|
||||||
|
|
||||||
|
struct Renderer {
|
||||||
|
sdl::Window* win;
|
||||||
|
sdl::GPUDevice* gpu;
|
||||||
|
sdl::GPURenderPass* render_pass;
|
||||||
|
sdl::GPUTexture* swapchain_texture;
|
||||||
|
sdl::GPUCommandBuffer* render_cmdbuf;
|
||||||
|
|
||||||
|
QuadBuffer quad_buffer;
|
||||||
|
ShaderList shaders;
|
||||||
|
PipelineList pipelines;
|
||||||
|
TextureList textures;
|
||||||
|
|
||||||
|
Id sprite_atlas_id;
|
||||||
|
Id font_atlas_id;
|
||||||
|
|
||||||
|
int scissor_x, scissor_y, scissor_w, scissor_h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// How each vertex is represented in the gpu
|
||||||
|
struct Vertex {
|
||||||
|
short x, y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes of each quad instance
|
||||||
|
struct QuadAttributes {
|
||||||
|
struct pos {
|
||||||
|
short x, y, w, h;
|
||||||
|
}
|
||||||
|
struct uv {
|
||||||
|
short u, v;
|
||||||
|
}
|
||||||
|
uint color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single quad
|
||||||
|
struct Quad {
|
||||||
|
struct vertices {
|
||||||
|
Vertex v1,v2,v3,v4;
|
||||||
|
}
|
||||||
|
struct indices {
|
||||||
|
short i1,i2,i3,i4,i5,i6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ViewsizeUniform @align(16) {
|
||||||
|
int w, h;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int DEBUG = 1;
|
||||||
|
const bool CYCLE = true;
|
||||||
|
|
||||||
|
fn void Renderer.init(&self, ZString title, uint width, uint height, bool vsync)
|
||||||
|
{
|
||||||
|
// set wayland hint automagically
|
||||||
|
$if DEBUG == 0:
|
||||||
|
bool has_wayland = false;
|
||||||
|
for (int i = 0; i < sdl::get_num_video_drivers(); i++) {
|
||||||
|
ZString driver = sdl::get_video_driver(i);
|
||||||
|
if (driver.str_view() == "wayland") {
|
||||||
|
has_wayland = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_wayland) {
|
||||||
|
sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "wayland");
|
||||||
|
}
|
||||||
|
$else
|
||||||
|
// in debug mode set the video driver to X11 because renderdoc
|
||||||
|
// doesn't support debugging in wayland yet.
|
||||||
|
sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "x11");
|
||||||
|
sdl::set_hint(sdl::HINT_RENDER_GPU_DEBUG, "1");
|
||||||
|
$endif
|
||||||
|
|
||||||
|
// init subsystems
|
||||||
|
if (!sdl::init(INIT_VIDEO)) {
|
||||||
|
unreachable("sdl error: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the window
|
||||||
|
self.win = sdl::create_window(title, width, height, WINDOW_RESIZABLE|WINDOW_VULKAN);
|
||||||
|
if (self.win == null) {
|
||||||
|
unreachable("sdl error: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the gpu device handle
|
||||||
|
self.gpu = sdl::create_gpu_device(GPU_SHADERFORMAT_SPIRV, true, "vulkan");
|
||||||
|
if (self.gpu == null) {
|
||||||
|
unreachable("failed to create gpu device: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sdl::claim_window_for_gpu_device(self.gpu, self.win)) {
|
||||||
|
unreachable("failed to claim window for use with gpu: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// set swapchain parameters, like vsync
|
||||||
|
GPUPresentMode present_mode = vsync ? GPU_PRESENTMODE_VSYNC : GPU_PRESENTMODE_IMMEDIATE;
|
||||||
|
sdl::set_gpu_swapchain_parameters(self.gpu, self.win, GPU_SWAPCHAINCOMPOSITION_SDR, present_mode);
|
||||||
|
|
||||||
|
//
|
||||||
|
// initialize the quad buffer
|
||||||
|
// ==========================
|
||||||
|
QuadBuffer* qb = &self.quad_buffer;
|
||||||
|
|
||||||
|
// since instanced rendering is used, on the gpu there is only one mesh, a single quad.
|
||||||
|
|
||||||
|
// create the vertex and index buffer on the gpu
|
||||||
|
qb.vert_buf = sdl::create_gpu_buffer(self.gpu,
|
||||||
|
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = Quad.vertices.sizeof}
|
||||||
|
);
|
||||||
|
if (qb.vert_buf == null) {
|
||||||
|
unreachable("failed to initialize quad buffer (vertex): %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
qb.idx_buf = sdl::create_gpu_buffer(self.gpu,
|
||||||
|
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_INDEX, .size = Quad.indices.sizeof}
|
||||||
|
);
|
||||||
|
if (qb.idx_buf == null) {
|
||||||
|
unreachable("failed to initialize quad buffer (index): %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
qb.attr_buf = sdl::create_gpu_buffer(self.gpu,
|
||||||
|
&&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = QuadAttributes.sizeof * MAX_QUAD_BATCH}
|
||||||
|
);
|
||||||
|
if (qb.attr_buf == null) {
|
||||||
|
unreachable("failed to initialize quad buffer (index): %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// upload the quad mesh
|
||||||
|
GPUTransferBuffer *ts = sdl::create_gpu_transfer_buffer(self.gpu,
|
||||||
|
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = Quad.sizeof}
|
||||||
|
);
|
||||||
|
if (ts == null) {
|
||||||
|
unreachable("failed to create gpu transfer buffer: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
Quad* quad = (Quad*)sdl::map_gpu_transfer_buffer(self.gpu, ts, false);
|
||||||
|
|
||||||
|
/* v1 v4
|
||||||
|
* +-------------+
|
||||||
|
* | _/|
|
||||||
|
* | _/ |
|
||||||
|
* | 1 _/ |
|
||||||
|
* | _/ |
|
||||||
|
* | _/ |
|
||||||
|
* | _/ 2 |
|
||||||
|
* |/ |
|
||||||
|
* +-------------+
|
||||||
|
* v2 v3
|
||||||
|
*/
|
||||||
|
quad.vertices.v1 = {.x = 0, .y = 0};
|
||||||
|
quad.vertices.v2 = {.x = 0, .y = 1};
|
||||||
|
quad.vertices.v3 = {.x = 1, .y = 1};
|
||||||
|
quad.vertices.v4 = {.x = 1, .y = 0};
|
||||||
|
// triangle 1 indices
|
||||||
|
quad.indices.i1 = 0; // v1
|
||||||
|
quad.indices.i2 = 1; // v2
|
||||||
|
quad.indices.i3 = 3; // v4
|
||||||
|
// triangle 2 indices
|
||||||
|
quad.indices.i4 = 1; // v2
|
||||||
|
quad.indices.i5 = 2; // v3
|
||||||
|
quad.indices.i6 = 3; // v4
|
||||||
|
|
||||||
|
sdl::unmap_gpu_transfer_buffer(self.gpu, ts);
|
||||||
|
|
||||||
|
GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu);
|
||||||
|
if (cmd == null) {
|
||||||
|
unreachable("failed to upload quad at acquiring command buffer: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
|
||||||
|
|
||||||
|
// upload vertices
|
||||||
|
sdl::upload_to_gpu_buffer(cpy,
|
||||||
|
&&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.vertices.offsetof},
|
||||||
|
&&(GPUBufferRegion){.buffer = qb.vert_buf, .offset = 0, .size = Quad.vertices.sizeof},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
// upload indices
|
||||||
|
sdl::upload_to_gpu_buffer(cpy,
|
||||||
|
&&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.indices.offsetof},
|
||||||
|
&&(GPUBufferRegion){.buffer = qb.idx_buf, .offset = 0, .size = Quad.indices.sizeof},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
sdl::end_gpu_copy_pass(cpy);
|
||||||
|
if (!sdl::submit_gpu_command_buffer(cmd)) {
|
||||||
|
unreachable("failed to upload quads at submit command buffer: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
sdl::release_gpu_transfer_buffer(self.gpu, ts);
|
||||||
|
|
||||||
|
|
||||||
|
// create and map the quad attributes transfer buffer
|
||||||
|
qb.attr_ts = sdl::create_gpu_transfer_buffer(self.gpu,
|
||||||
|
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = QuadAttributes.sizeof * MAX_QUAD_BATCH}
|
||||||
|
);
|
||||||
|
if (qb.attr_ts == null) {
|
||||||
|
unreachable("failed to create gpu transfer buffer: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
qb.attr_ts_mapped = ((QuadAttributes*)sdl::map_gpu_transfer_buffer(self.gpu, qb.attr_ts, false))[:MAX_QUAD_BATCH];
|
||||||
|
if (qb.attr_ts_mapped.ptr == null) {
|
||||||
|
unreachable("failed to map vertex or index buffers: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
qb.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.free(&self)
|
||||||
|
{
|
||||||
|
foreach (&s: self.shaders) {
|
||||||
|
sdl::release_gpu_shader(self.gpu, s.frag);
|
||||||
|
sdl::release_gpu_shader(self.gpu, s.vert);
|
||||||
|
}
|
||||||
|
self.shaders.free();
|
||||||
|
|
||||||
|
foreach (&p: self.pipelines) {
|
||||||
|
sdl::release_gpu_graphics_pipeline(self.gpu, p.pipeline);
|
||||||
|
}
|
||||||
|
self.pipelines.free();
|
||||||
|
|
||||||
|
foreach (&t: self.textures) {
|
||||||
|
sdl::release_gpu_texture(self.gpu, t.texture);
|
||||||
|
sdl::release_gpu_sampler(self.gpu, t.sampler);
|
||||||
|
}
|
||||||
|
self.textures.free();
|
||||||
|
|
||||||
|
QuadBuffer* qb = &self.quad_buffer;
|
||||||
|
sdl::unmap_gpu_transfer_buffer(self.gpu, qb.attr_ts);
|
||||||
|
sdl::release_gpu_transfer_buffer(self.gpu, qb.attr_ts);
|
||||||
|
sdl::release_gpu_buffer(self.gpu, qb.vert_buf);
|
||||||
|
sdl::release_gpu_buffer(self.gpu, qb.idx_buf);
|
||||||
|
sdl::release_gpu_buffer(self.gpu, qb.attr_buf);
|
||||||
|
|
||||||
|
sdl::release_window_from_gpu_device(self.gpu, self.win);
|
||||||
|
sdl::destroy_gpu_device(self.gpu);
|
||||||
|
sdl::destroy_window(self.win);
|
||||||
|
sdl::quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.resize_window(&self, uint width, uint height)
|
||||||
|
{
|
||||||
|
sdl::set_window_size(self.win, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.get_window_size(&self, int* width, int* height)
|
||||||
|
{
|
||||||
|
sdl::get_window_size_in_pixels(self.win, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Both the vertex shader and fragment shader have an implicit uniform buffer at binding 0 that
|
||||||
|
// contains the viewport size. It is populated automatically at every begin_render() call
|
||||||
|
fn void Renderer.load_spirv_shader_from_mem(&self, String name, char[] vert_code, char[] frag_code, uint textures, uint uniforms)
|
||||||
|
{
|
||||||
|
Shader s;
|
||||||
|
s.id = name.hash();
|
||||||
|
|
||||||
|
if (vert_code.len == 0 || frag_code.len == 0) {
|
||||||
|
unreachable("vertex shader and fragment shader cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vert_code.len > 0) {
|
||||||
|
// FIXME: these should be passed by parameter and/or automatically determined by parsing
|
||||||
|
// the shader code
|
||||||
|
GPUShaderCreateInfo shader_info = {
|
||||||
|
.code = vert_code.ptr,
|
||||||
|
.code_size = vert_code.len,
|
||||||
|
.entrypoint = "main",
|
||||||
|
.format = GPU_SHADERFORMAT_SPIRV,
|
||||||
|
.stage = GPU_SHADERSTAGE_VERTEX,
|
||||||
|
.num_samplers = 0,
|
||||||
|
.num_uniform_buffers = 1+uniforms,
|
||||||
|
.num_storage_buffers = 0,
|
||||||
|
.num_storage_textures = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
s.vert = sdl::create_gpu_shader(self.gpu, &shader_info);
|
||||||
|
if (s.vert == null) {
|
||||||
|
unreachable("failed to create gpu vertex shader: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frag_code.len > 0) {
|
||||||
|
// FIXME: these should be passed by parameter and/or automatically determined by parsing
|
||||||
|
// the shader code
|
||||||
|
GPUShaderCreateInfo shader_info = {
|
||||||
|
.code = frag_code.ptr,
|
||||||
|
.code_size = frag_code.len,
|
||||||
|
.entrypoint = "main",
|
||||||
|
.format = GPU_SHADERFORMAT_SPIRV,
|
||||||
|
.stage = GPU_SHADERSTAGE_FRAGMENT,
|
||||||
|
.num_samplers = textures,
|
||||||
|
.num_uniform_buffers = 1,
|
||||||
|
.num_storage_buffers = 0,
|
||||||
|
.num_storage_textures = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
s.frag = sdl::create_gpu_shader(self.gpu, &shader_info);
|
||||||
|
if (s.frag == null) {
|
||||||
|
unreachable("failed to create gpu fragment shader: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// push the shader into the list
|
||||||
|
self.shaders.push(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_path, String frag_path, uint textures, uint uniforms)
|
||||||
|
{
|
||||||
|
if (vert_path == "" || frag_path == "") {
|
||||||
|
unreachable("need both a vertex shader and fragment shader path");
|
||||||
|
}
|
||||||
|
|
||||||
|
char[] vert_code;
|
||||||
|
char[] frag_code;
|
||||||
|
|
||||||
|
// create vertex shader
|
||||||
|
usz size = file::get_size(vert_path)!!;
|
||||||
|
vert_code = mem::new_array(char, size + size%4);
|
||||||
|
file::load_buffer(vert_path, vert_code)!!;
|
||||||
|
defer mem::free(vert_code);
|
||||||
|
|
||||||
|
// create fragment shader
|
||||||
|
size = file::get_size(frag_path)!!;
|
||||||
|
frag_code = mem::new_array(char, size + size%4);
|
||||||
|
file::load_buffer(frag_path, frag_code)!!;
|
||||||
|
defer mem::free(frag_code);
|
||||||
|
|
||||||
|
self.load_spirv_shader_from_mem(name, vert_code, frag_code, textures, uniforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this describes what we want to draw, since for drawing different things we have to change
|
||||||
|
// the GPUPrimitiveType and GPURasterizerState for the pipeline.
|
||||||
|
enum PipelineType : (GPUPrimitiveType primitive_type, GPURasterizerState raster_state) {
|
||||||
|
RECT = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
|
||||||
|
SPRITE = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
|
||||||
|
LINE = {GPU_PRIMITIVETYPE_LINELIST, {.fill_mode = GPU_FILLMODE_LINE, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a graphics pipeline to draw to the window using a set of vertex/fragment shaders
|
||||||
|
// the pipeline is pushed into the renderer's pipeline list and it will have the same id as
|
||||||
|
// the shader set.
|
||||||
|
fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
|
||||||
|
{
|
||||||
|
Shader *s = self.shaders.get_from_name(shader_name);
|
||||||
|
if (s == null) {
|
||||||
|
unreachable("error in creating pipeline: no shader named %s", shader_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
GPUGraphicsPipelineCreateInfo ci = {
|
||||||
|
.vertex_shader = s.vert,
|
||||||
|
.fragment_shader = s.frag,
|
||||||
|
// This structure specifies how the vertex buffer looks in memory, what it contains
|
||||||
|
// and what is passed where to the gpu. Each vertex has three attributes, position,
|
||||||
|
// color and uv coordinates. Since this is a 2D pixel-based renderer the position
|
||||||
|
// is represented by two floats, the color as 32 bit rgba and the uv also as intgers.
|
||||||
|
.vertex_input_state = {
|
||||||
|
// the description of each vertex buffer, for now I use only one buffer
|
||||||
|
.vertex_buffer_descriptions = (GPUVertexBufferDescription[]){
|
||||||
|
{ // first slot, per-vertex attributes
|
||||||
|
.slot = 0,
|
||||||
|
.pitch = Vertex.sizeof,
|
||||||
|
.input_rate = GPU_VERTEXINPUTRATE_VERTEX,
|
||||||
|
},
|
||||||
|
{ // second slot, per-instance attributes
|
||||||
|
.slot = 1,
|
||||||
|
.pitch = QuadAttributes.sizeof,
|
||||||
|
.input_rate = GPU_VERTEXINPUTRATE_INSTANCE,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.num_vertex_buffers = 2,
|
||||||
|
// the description of each vertex
|
||||||
|
.vertex_attributes = (GPUVertexAttribute[]){
|
||||||
|
{ // at location zero there is the position of the vertex
|
||||||
|
.location = 0,
|
||||||
|
.buffer_slot = 0, // buffer slot zero so per-vertex
|
||||||
|
.format = GPU_VERTEXELEMENTFORMAT_SHORT2, // x,y
|
||||||
|
.offset = 0,
|
||||||
|
},
|
||||||
|
{ // at location one there is the per-quad position
|
||||||
|
.location = 1,
|
||||||
|
.buffer_slot = 1, // buffer slot one so per-instance
|
||||||
|
.format = GPU_VERTEXELEMENTFORMAT_SHORT4, // x,y,w,h
|
||||||
|
.offset = QuadAttributes.pos.offsetof,
|
||||||
|
},
|
||||||
|
{ // at location two there are the per-quad uv coordinates
|
||||||
|
.location = 2,
|
||||||
|
.buffer_slot = 1,
|
||||||
|
.format = GPU_VERTEXELEMENTFORMAT_SHORT2,
|
||||||
|
.offset = QuadAttributes.uv.offsetof,
|
||||||
|
},
|
||||||
|
{ // at location three there is the quad color
|
||||||
|
.location = 3,
|
||||||
|
.buffer_slot = 1,
|
||||||
|
.format = GPU_VERTEXELEMENTFORMAT_UBYTE4,
|
||||||
|
.offset = QuadAttributes.color.offsetof,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.num_vertex_attributes = 4,
|
||||||
|
},
|
||||||
|
// the pipeline's primitive type and rasterizer state differs based on what needs to
|
||||||
|
// be drawn
|
||||||
|
.primitive_type = type.primitive_type,
|
||||||
|
.rasterizer_state = type.raster_state,
|
||||||
|
.multisample_state = {}, // no multisampling, all zeroes
|
||||||
|
.depth_stencil_state = {}, // no stencil test, all zeroes
|
||||||
|
.target_info = { // the target (texture) description
|
||||||
|
.color_target_descriptions = (GPUColorTargetDescription[]){{
|
||||||
|
// rendering happens to the window, so get it's format
|
||||||
|
.format = sdl::get_gpu_swapchain_texture_format(self.gpu, self.win),
|
||||||
|
.blend_state = {
|
||||||
|
// alpha blending on everything
|
||||||
|
// https://en.wikipedia.org/wiki/Alpha_compositing
|
||||||
|
.src_color_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA,
|
||||||
|
.dst_color_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
|
||||||
|
.color_blend_op = GPU_BLENDOP_ADD,
|
||||||
|
.src_alpha_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA,
|
||||||
|
.dst_alpha_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
|
||||||
|
.alpha_blend_op = GPU_BLENDOP_ADD,
|
||||||
|
.enable_blend = true,
|
||||||
|
// color write mask is not enabled so all rgba channels are written to
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
.num_color_targets = 1,
|
||||||
|
.depth_stencil_format = {}, // FIXME: no stencil, no depth buffering
|
||||||
|
.has_depth_stencil_target = false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// create the pipeline and add it to the pipeline list
|
||||||
|
Pipeline p = {
|
||||||
|
.id = s.id,
|
||||||
|
.pipeline = sdl::create_gpu_graphics_pipeline(self.gpu, &ci),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (p.pipeline == null) {
|
||||||
|
unreachable("failed to create pipeline (shaders: %s, type: %s): %s", shader_name, type.nameof, sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pipelines.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: with TEXTUREUSAGE_SAMPLER the texture format cannot be intger _UINT so it has to be nermalized
|
||||||
|
enum TextureType : (GPUTextureFormat format) {
|
||||||
|
FULL_COLOR = GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
|
||||||
|
JUST_ALPHA = GPU_TEXTUREFORMAT_R8_UNORM
|
||||||
|
}
|
||||||
|
|
||||||
|
macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixels, uint width, uint height)
|
||||||
|
{
|
||||||
|
$switch $typeof(name_or_id):
|
||||||
|
$case uint: return self.new_texture_by_id(id, type, pixels, width, height);
|
||||||
|
$case String: return self.new_texture_by_id(name_or_id.hash(), type, pixels, width, height);
|
||||||
|
$default: unreachable("texture must have a name (String) or an id (uint)");
|
||||||
|
$endswitch
|
||||||
|
}
|
||||||
|
|
||||||
|
macro void Renderer.update_texture(&self, name_or_id, char[] pixels, uint width, uint height, uint x = 0, uint y = 0)
|
||||||
|
{
|
||||||
|
$switch $typeof(name_or_id):
|
||||||
|
$case uint: return self.update_texture_by_id(name_or_id, pixels, width, height, x, y);
|
||||||
|
$case String: return self.update_texture_by_id(name_or_id.hash(), pixels, width, height, x, y);
|
||||||
|
$default: unreachable("texture must have a name (String) or an id (uint)");
|
||||||
|
$endswitch
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// create a new gpu texture from a pixel buffer, the format has to be specified
|
||||||
|
// the new texture s given an id and pushed into a texture list
|
||||||
|
fn void Renderer.new_texture_by_id(&self, Id id, TextureType type, char[] pixels, uint width, uint height)
|
||||||
|
{
|
||||||
|
// the texture description
|
||||||
|
GPUTextureCreateInfo tci = {
|
||||||
|
.type = GPU_TEXTURETYPE_2D,
|
||||||
|
.format = type.format,
|
||||||
|
// all textures are used with samplers, which means read-only textures that contain data to be sampled
|
||||||
|
.usage = GPU_TEXTUREUSAGE_SAMPLER,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.layer_count_or_depth = 1,
|
||||||
|
.num_levels = 1, // no mip maps so just one level
|
||||||
|
// .sample_count not used since the texture is not a render target
|
||||||
|
};
|
||||||
|
|
||||||
|
GPUTexture* texture = sdl::create_gpu_texture(self.gpu, &tci);
|
||||||
|
if (texture == null) {
|
||||||
|
unreachable("failed to create texture (id: %s, type: %s): %s", id, type.nameof, sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// the sampler description, how the texture should be sampled
|
||||||
|
GPUSamplerCreateInfo sci = {
|
||||||
|
.min_filter = GPU_FILTER_LINEAR, // linear interpolation for textures
|
||||||
|
.mag_filter = GPU_FILTER_LINEAR,
|
||||||
|
.mipmap_mode = GPU_SAMPLERMIPMAPMODE_NEAREST,
|
||||||
|
.address_mode_u = GPU_SAMPLERADDRESSMODE_REPEAT, // tiling textures
|
||||||
|
.address_mode_v = GPU_SAMPLERADDRESSMODE_REPEAT,
|
||||||
|
.address_mode_w = GPU_SAMPLERADDRESSMODE_REPEAT,
|
||||||
|
// everything else is not used and not needed
|
||||||
|
};
|
||||||
|
|
||||||
|
GPUSampler* sampler = sdl::create_gpu_sampler(self.gpu, &sci);
|
||||||
|
if (sampler == null) {
|
||||||
|
unreachable("failed to create sampler (texture id: %s, type: %s): %s", id, type.nameof, sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Texture t = {
|
||||||
|
.id = id,
|
||||||
|
.texture = texture,
|
||||||
|
.sampler = sampler,
|
||||||
|
};
|
||||||
|
self.textures.push(t);
|
||||||
|
|
||||||
|
// upload the texture data
|
||||||
|
self.update_texture_by_id(id, pixels, width, height, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.update_texture_by_id(&self, Id id, char[] pixels, uint width, uint height, uint x, uint y)
|
||||||
|
{
|
||||||
|
Texture* t = self.textures.get_from_id(id);
|
||||||
|
if (t == null || t.texture == null) {
|
||||||
|
unreachable("failed updating texture: no texture with id %s", id);
|
||||||
|
}
|
||||||
|
GPUTexture* texture = t.texture;
|
||||||
|
|
||||||
|
// FIXME: do a better job at validating the copy
|
||||||
|
if (x > t.width || y > t.height) {
|
||||||
|
unreachable("failed updating texture: attempting to copy outside of the texture region");
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload image data
|
||||||
|
GPUCommandBuffer* cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu);
|
||||||
|
if (cmdbuf == null) {
|
||||||
|
unreachable("failed to upload texture data at acquiring command buffer: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
GPUCopyPass* copypass = sdl::begin_gpu_copy_pass(cmdbuf);
|
||||||
|
if (copypass == null) {
|
||||||
|
unreachable("failed to upload texture data at beginning copy pass: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
GPUTransferBuffer* buf = sdl::create_gpu_transfer_buffer(self.gpu,
|
||||||
|
&&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = pixels.len}
|
||||||
|
);
|
||||||
|
if (buf == null) {
|
||||||
|
unreachable("failed to upload texture data at creating the transfer buffer: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
char* gpu_mem = (char*)sdl::map_gpu_transfer_buffer(self.gpu, buf, CYCLE);
|
||||||
|
if (gpu_mem == null) {
|
||||||
|
unreachable("failed to upload texture data at mapping the transfer buffer: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
// copy the data to the driver's memory
|
||||||
|
gpu_mem[:pixels.len] = pixels[..];
|
||||||
|
sdl::unmap_gpu_transfer_buffer(self.gpu, buf);
|
||||||
|
|
||||||
|
// upload the data to gpu memory
|
||||||
|
sdl::upload_to_gpu_texture(copypass,
|
||||||
|
&&(GPUTextureTransferInfo){.transfer_buffer = buf, .offset = 0},
|
||||||
|
&&(GPUTextureRegion){.texture = texture, .x = x, .y = y, .w = width, .h = height, .d = 1},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
sdl::end_gpu_copy_pass(copypass);
|
||||||
|
if (!sdl::submit_gpu_command_buffer(cmdbuf)) {
|
||||||
|
unreachable("failed to upload texture data at command buffer submission: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
sdl::release_gpu_transfer_buffer(self.gpu, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn bool Renderer.push_sprite(&self, short x, short y, short w, short h, short u, short v, uint color = 0xffffffff)
|
||||||
|
{
|
||||||
|
QuadAttributes qa = {
|
||||||
|
.pos = {.x = x, .y = y, .w = w, .h = h},
|
||||||
|
.uv = {.u = u, .v = v},
|
||||||
|
.color = color
|
||||||
|
};
|
||||||
|
|
||||||
|
return self.map_quad(qa);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bool Renderer.push_quad(&self, short x, short y, short w, short h, uint color, ushort radius = 0, ushort thickness = 0)
|
||||||
|
{
|
||||||
|
QuadAttributes qa = {
|
||||||
|
.pos = {.x = x, .y = y, .w = w, .h = h},
|
||||||
|
.uv = {.u = radius, .v = radius+thickness},
|
||||||
|
.color = color
|
||||||
|
};
|
||||||
|
|
||||||
|
return self.map_quad(qa);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this does not upload a quad, but it simply copies the quad data to the correct transfer buffers.
|
||||||
|
// Data transfer to the GPU only happens in draw_quads() to save time
|
||||||
|
fn bool Renderer.map_quad(&self, QuadAttributes qa)
|
||||||
|
{
|
||||||
|
if (self.quad_buffer.count >= MAX_QUAD_BATCH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QuadBuffer* qb = &self.quad_buffer;
|
||||||
|
|
||||||
|
// upload the quad data to the gpu
|
||||||
|
if (qb.initialized == false) {
|
||||||
|
unreachable("quad buffer not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
qb.attr_ts_mapped[qb.count] = qa;
|
||||||
|
|
||||||
|
qb.count++;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.upload_quads(&self)
|
||||||
|
{
|
||||||
|
QuadBuffer* qb = &self.quad_buffer;
|
||||||
|
|
||||||
|
GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu);
|
||||||
|
if (cmd == null) {
|
||||||
|
unreachable("failed to upload quad at acquiring command buffer: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
|
||||||
|
|
||||||
|
// upload quad attributes
|
||||||
|
sdl::upload_to_gpu_buffer(cpy,
|
||||||
|
&&(GPUTransferBufferLocation){.transfer_buffer = qb.attr_ts, .offset = 0},
|
||||||
|
&&(GPUBufferRegion){.buffer = qb.attr_buf, .offset = 0, .size = QuadAttributes.sizeof * qb.count},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
sdl::end_gpu_copy_pass(cpy);
|
||||||
|
if (!sdl::submit_gpu_command_buffer(cmd)) {
|
||||||
|
unreachable("failed to upload quads at submit command buffer: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw all quads in the quad buffer, since uniforms are per-drawcall it makes no sense
|
||||||
|
// to draw them one a the time
|
||||||
|
fn void Renderer.draw_quads(&self, uint off, uint count)
|
||||||
|
{
|
||||||
|
QuadBuffer* qb = &self.quad_buffer;
|
||||||
|
|
||||||
|
// too many quads to draw
|
||||||
|
if (off >= qb.count || count > qb.count - off) {
|
||||||
|
unreachable("too many quads, have %d, requested %d, offset %d", qb.count, count, off);
|
||||||
|
}
|
||||||
|
|
||||||
|
sdl::bind_gpu_vertex_buffers(self.render_pass, 0,
|
||||||
|
(GPUBufferBinding[]){
|
||||||
|
{.buffer = qb.vert_buf, .offset = 0},
|
||||||
|
{.buffer = qb.attr_buf, .offset = 0},
|
||||||
|
}, 2);
|
||||||
|
sdl::bind_gpu_index_buffer(self.render_pass, &&(GPUBufferBinding){.buffer = qb.idx_buf, .offset = 0}, GPU_INDEXELEMENTSIZE_16BIT);
|
||||||
|
|
||||||
|
sdl::draw_gpu_indexed_primitives(self.render_pass, 6, count, 0, 0, off);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.reset_quads(&self)
|
||||||
|
{
|
||||||
|
self.quad_buffer.count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn void Renderer.begin_render(&self, bool clear_screen)
|
||||||
|
{
|
||||||
|
self.render_cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu);
|
||||||
|
sdl::wait_and_acquire_gpu_swapchain_texture(self.render_cmdbuf, self.win, &self.swapchain_texture, null, null);
|
||||||
|
|
||||||
|
// push the window size as a uniform
|
||||||
|
// TODO: maybe make this configurable and/or add more things
|
||||||
|
ViewsizeUniform v;
|
||||||
|
self.get_window_size(&v.w, &v.h);
|
||||||
|
sdl::push_gpu_vertex_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof);
|
||||||
|
sdl::push_gpu_fragment_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof);
|
||||||
|
|
||||||
|
if (clear_screen) {
|
||||||
|
GPURenderPass* pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
|
||||||
|
&&(GPUColorTargetInfo){
|
||||||
|
.texture = self.swapchain_texture,
|
||||||
|
.mip_level = 0,
|
||||||
|
.layer_or_depth_plane = 0,
|
||||||
|
.clear_color = {.r = 1.0, .g = 0.0, .b = 1.0, .a = 1.0},
|
||||||
|
.load_op = GPU_LOADOP_CLEAR, // clear the screen at the start of the render pass
|
||||||
|
.store_op = GPU_STOREOP_STORE,
|
||||||
|
.resolve_texture = null,
|
||||||
|
.resolve_mip_level = 0,
|
||||||
|
.resolve_layer = 0,
|
||||||
|
.cycle = false,
|
||||||
|
.cycle_resolve_texture = false
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
null // huh
|
||||||
|
);
|
||||||
|
if (pass == null) {
|
||||||
|
unreachable("render pass creation went wrong: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
sdl::end_gpu_render_pass(pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.end_render(&self)
|
||||||
|
{
|
||||||
|
sdl::submit_gpu_command_buffer(self.render_cmdbuf);
|
||||||
|
self.reset_quads();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.start_render_pass(&self, String pipeline_name)
|
||||||
|
{
|
||||||
|
self.render_pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
|
||||||
|
&&(GPUColorTargetInfo){
|
||||||
|
.texture = self.swapchain_texture,
|
||||||
|
.mip_level = 0,
|
||||||
|
.layer_or_depth_plane = 0,
|
||||||
|
.clear_color = {.r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0},
|
||||||
|
.load_op = GPU_LOADOP_DONT_CARE,
|
||||||
|
.store_op = GPU_STOREOP_STORE,
|
||||||
|
.resolve_texture = null,
|
||||||
|
.resolve_mip_level = 0,
|
||||||
|
.resolve_layer = 0,
|
||||||
|
.cycle = false,
|
||||||
|
.cycle_resolve_texture = false
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
null // huh
|
||||||
|
);
|
||||||
|
|
||||||
|
if (self.render_pass == null) {
|
||||||
|
unreachable("render pass creation went wrong: %s", sdl::get_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
sdl::GPUGraphicsPipeline* p;
|
||||||
|
p = self.pipelines.get_from_name(pipeline_name).pipeline;
|
||||||
|
if (p == null) {
|
||||||
|
unreachable("no pipeline");
|
||||||
|
}
|
||||||
|
|
||||||
|
sdl::bind_gpu_graphics_pipeline(self.render_pass, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.end_render_pass(&self)
|
||||||
|
{
|
||||||
|
sdl::end_gpu_render_pass(self.render_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.bind_texture(&self, String texture_name)
|
||||||
|
{
|
||||||
|
ren::Texture* tx = self.textures.get_from_name(texture_name);
|
||||||
|
sdl::bind_gpu_fragment_samplers(self.render_pass, 0,
|
||||||
|
(GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.set_scissor(&self, int x, int y, int w, int h)
|
||||||
|
{
|
||||||
|
// in vulkan scissor size must be positive, clamp to zero
|
||||||
|
w = max(w, 0);
|
||||||
|
h = max(h, 0);
|
||||||
|
sdl::set_gpu_scissor(self.render_pass, &&(sdl::Rect){x,y,w,h});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.reset_scissor(&self)
|
||||||
|
{
|
||||||
|
int w, h;
|
||||||
|
sdl::get_window_size(self.win, &w, &h);
|
||||||
|
self.set_scissor(0, 0, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// === NOTES ===
|
||||||
|
/* 1. The uniform data is per-render pass. So you can do:
|
||||||
|
* - push uniform
|
||||||
|
* - draw 1
|
||||||
|
* - draw 2
|
||||||
|
* But not:
|
||||||
|
* - push uniform
|
||||||
|
* - draw
|
||||||
|
* - push new uniform
|
||||||
|
* - draw
|
||||||
|
* And not even:
|
||||||
|
* - draw
|
||||||
|
* - push uniform
|
||||||
|
* - draw
|
||||||
|
*
|
||||||
|
* 2. The GPU buffers are read per-command-buffer and not per
|
||||||
|
* render pass. So I cannot override an element in the buffer
|
||||||
|
* before submitting the command buffer.
|
||||||
|
*/
|
||||||
|
/// === END NOTES ===
|
||||||
|
|
||||||
|
|
||||||
|
fn void Renderer.render_ugui(&self, CmdQueue* queue)
|
||||||
|
{
|
||||||
|
// upload pass
|
||||||
|
foreach (&c : queue) {
|
||||||
|
if (c.type == CMD_RECT) {
|
||||||
|
CmdRect r = c.rect;
|
||||||
|
self.push_quad(r.rect.x, r.rect.y, r.rect.w, r.rect.h, r.color.to_uint(), r.radius, r.thickness);
|
||||||
|
} else if (c.type == CMD_SPRITE) {
|
||||||
|
CmdSprite s = c.sprite;
|
||||||
|
self.push_sprite(s.rect.x, s.rect.y, s.texture_rect.w, s.texture_rect.h, s.texture_rect.x, s.texture_rect.y, s.hue.to_uint());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.upload_quads();
|
||||||
|
|
||||||
|
Cmd* last_command;
|
||||||
|
uint off;
|
||||||
|
uint count;
|
||||||
|
for (Cmd* cmd; (cmd = queue.dequeue() ?? null) != null;) {
|
||||||
|
if (last_command == null || last_command.type != cmd.type) {
|
||||||
|
self.end_command(last_command, off, count);
|
||||||
|
self.begin_command(cmd);
|
||||||
|
off += count;
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cmd.type) {
|
||||||
|
case CMD_RECT: nextcase;
|
||||||
|
case CMD_SPRITE:
|
||||||
|
count++;
|
||||||
|
case CMD_UPDATE_ATLAS:
|
||||||
|
// TODO: verify the correct type
|
||||||
|
CmdUpdateAtlas u = cmd.update_atlas;
|
||||||
|
char[] pixels = u.raw_buffer[..u.width*u.height*u.bpp];
|
||||||
|
self.update_texture(u.id, pixels, u.width, u.height);
|
||||||
|
case CMD_SCISSOR:
|
||||||
|
ugui::Rect s = cmd.scissor.rect;
|
||||||
|
if (s.x == 0 && s.y == 0 && s.w == 0 && s.h == 0) {
|
||||||
|
self.get_window_size((int*)&s.w, (int*)&s.h);
|
||||||
|
}
|
||||||
|
self.scissor_x = s.x;
|
||||||
|
self.scissor_y = s.y;
|
||||||
|
self.scissor_w = s.w;
|
||||||
|
self.scissor_h = s.h;
|
||||||
|
default: unreachable("unknown command: %s", cmd.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
last_command = cmd;
|
||||||
|
}
|
||||||
|
self.end_command(last_command, off, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.begin_command(&self, Cmd* cmd)
|
||||||
|
{
|
||||||
|
if (cmd == null) return;
|
||||||
|
|
||||||
|
switch (cmd.type) {
|
||||||
|
case CMD_RECT:
|
||||||
|
self.start_render_pass("UGUI_PIPELINE_RECT");
|
||||||
|
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
|
||||||
|
case CMD_SPRITE:
|
||||||
|
// TODO: support multiple sprite and font atlases
|
||||||
|
CmdSprite s = cmd.sprite;
|
||||||
|
String pipeline;
|
||||||
|
String texture;
|
||||||
|
if (s.texture_id == self.sprite_atlas_id) {
|
||||||
|
switch (s.type) {
|
||||||
|
case SPRITE_NORMAL: pipeline = "UGUI_PIPELINE_SPRITE";
|
||||||
|
case SPRITE_SDF: pipeline = "UGUI_PIPELINE_SPRITE_SDF";
|
||||||
|
case SPRITE_MSDF: pipeline = "UGUI_PIPELINE_SPRITE_MSDF";
|
||||||
|
case SPRITE_ANIMATED: unreachable("animated sprtes are unsupported for now");
|
||||||
|
default: unreachable("unknown sprite type %s", s.type);
|
||||||
|
}
|
||||||
|
texture = "icons";
|
||||||
|
} else if (s.texture_id == self.font_atlas_id) {
|
||||||
|
pipeline = "UGUI_PIPELINE_FONT";
|
||||||
|
texture = "font1";
|
||||||
|
}
|
||||||
|
self.start_render_pass(pipeline);
|
||||||
|
self.bind_texture(texture);
|
||||||
|
self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
|
||||||
|
case CMD_UPDATE_ATLAS: break;
|
||||||
|
case CMD_SCISSOR: break;
|
||||||
|
default: unreachable("unknown command: %s", cmd.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void Renderer.end_command(&self, Cmd* cmd, uint off, uint count)
|
||||||
|
{
|
||||||
|
if (cmd == null) return;
|
||||||
|
|
||||||
|
switch (cmd.type) {
|
||||||
|
case CMD_RECT: nextcase;
|
||||||
|
case CMD_SPRITE:
|
||||||
|
self.draw_quads(off, count);
|
||||||
|
self.end_render_pass();
|
||||||
|
case CMD_UPDATE_ATLAS: break;
|
||||||
|
case CMD_SCISSOR: break;
|
||||||
|
default: unreachable("unknown command: %s", cmd.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
588
src/string.c3
588
src/string.c3
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
288
src/textedit.c3
288
src/textedit.c3
@ -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;
|
|
||||||
}
|
|
||||||
@ -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)!;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,327 +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;
|
|
||||||
}
|
|
||||||
ResizeAnchor resize_anchor;
|
|
||||||
ResizeAnchor resize_now;
|
|
||||||
ResizeDirection resized; // the element has been manually resized
|
|
||||||
Point resize_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
ResizeAnchor resize = {},
|
|
||||||
...;
|
|
||||||
@body()
|
|
||||||
)
|
|
||||||
{
|
|
||||||
ctx.div_begin(width, height, dir, anchor, absolute, off, scroll_x, scroll_y, resize, $vasplat)!;
|
|
||||||
@body();
|
|
||||||
return ctx.div_end()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
macro Ctx.div_begin(&ctx,
|
|
||||||
Size width = @grow(), Size height = @grow(),
|
|
||||||
LayoutDirection dir, Anchor anchor,
|
|
||||||
bool absolute, Point off,
|
|
||||||
bool scroll_x, bool scroll_y,
|
|
||||||
ResizeAnchor resize,
|
|
||||||
...
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return ctx.div_begin_id(@compute_id($vasplat), width, height, dir, anchor, absolute, off, scroll_x, scroll_y, resize);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
ResizeAnchor resize
|
|
||||||
)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
elem.div.resize_anchor = resize;
|
|
||||||
|
|
||||||
// check if the div is resizeable
|
|
||||||
bool resized = elem.div.resized && (resize != (ResizeAnchor){});
|
|
||||||
if (resize != (ResizeAnchor){}) {
|
|
||||||
// if the element was not resized yet then the size is as-specified
|
|
||||||
// if the element was resized the size is the same as the last frame
|
|
||||||
if (elem.div.resized.x == true) {
|
|
||||||
width = @exact(elem.div.resize_size.x);
|
|
||||||
}
|
|
||||||
if (elem.div.resized.y == true) {
|
|
||||||
height = @exact(elem.div.resize_size.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check resize action
|
|
||||||
/*
|
|
||||||
* top border
|
|
||||||
* +-------------------------------------------+
|
|
||||||
* | +---------------------------------------+ |
|
|
||||||
* | | | |
|
|
||||||
* | | | |
|
|
||||||
* | | | |
|
|
||||||
* | | | |
|
|
||||||
* left | | | | right
|
|
||||||
* border | | | | border
|
|
||||||
* | | | |
|
|
||||||
* | | | |
|
|
||||||
* | | | |
|
|
||||||
* | | | |
|
|
||||||
* | +---------------------------------------+ |
|
|
||||||
* +-------------------------------------------+
|
|
||||||
* bottom border
|
|
||||||
*/
|
|
||||||
if (elem.div.resize_anchor != (ResizeAnchor){}) {
|
|
||||||
Rect b = elem.bounds;
|
|
||||||
Rect s = style.border + style.margin + style.padding;
|
|
||||||
Rect b_l = {.x = b.x, .y = b.y, .w = s.x, .h = b.h};
|
|
||||||
Rect b_r = {.x = b.x+b.w-s.w, .y = b.y, .w = s.w, .h = b.h};
|
|
||||||
Rect b_t = {.x = b.x, .y = b.y, .w = b.w, .h = s.y};
|
|
||||||
Rect b_b = {.x = b.x, .y = b.y+b.h, .w = b.w, .h = s.h};
|
|
||||||
|
|
||||||
Rect content_bounds = elem.bounds.pad(elem.layout.content_offset);
|
|
||||||
Point m = ctx.input.mouse.pos;
|
|
||||||
if (elem.events.mouse_hover && elem.events.mouse_press) {
|
|
||||||
if (elem.div.resize_anchor.right && m.in_rect(b_r)) elem.div.resize_now.right = true;
|
|
||||||
if (elem.div.resize_anchor.left && m.in_rect(b_l)) elem.div.resize_now.left = true;
|
|
||||||
if (elem.div.resize_anchor.top && m.in_rect(b_t)) elem.div.resize_now.top = true;
|
|
||||||
if (elem.div.resize_anchor.bottom && m.in_rect(b_b)) elem.div.resize_now.bottom = true;
|
|
||||||
} else if (ctx.is_mouse_released(BTN_ANY)) {
|
|
||||||
elem.div.resize_now = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elem.div.resize_now.right == true) {
|
|
||||||
elem.div.resized.x = true;
|
|
||||||
elem.div.resize_size.x = content_bounds.w - (elem.bounds.x + elem.bounds.w - m.x);
|
|
||||||
}
|
|
||||||
if (elem.div.resize_now.bottom == true) {
|
|
||||||
elem.div.resized.y = true;
|
|
||||||
elem.div.resize_size.y = content_bounds.h - (elem.bounds.y + elem.bounds.h - m.y);
|
|
||||||
}
|
|
||||||
if (elem.div.resize_now.left == true) {
|
|
||||||
elem.div.resized.x = true;
|
|
||||||
elem.div.resize_size.x = content_bounds.w - (elem.bounds.x - m.x);
|
|
||||||
}
|
|
||||||
if (elem.div.resize_now.top == true) {
|
|
||||||
elem.div.resized.y = true;
|
|
||||||
elem.div.resize_size.y = content_bounds.h - (elem.bounds.y - m.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@ -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)!;
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
@ -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)!;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
0
test/.gitkeep
Normal file
0
test/.gitkeep
Normal file
14
test/test_bitsruct.c3
Normal file
14
test/test_bitsruct.c3
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
bitstruct Bits : uint {
|
||||||
|
bool a : 0;
|
||||||
|
bool b : 1;
|
||||||
|
bool c : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn int main()
|
||||||
|
{
|
||||||
|
Bits a = {false, true, false};
|
||||||
|
Bits b = {true, true, false};
|
||||||
|
Bits c = a | b;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
10
test/test_bittype.c3
Normal file
10
test/test_bittype.c3
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import std::io;
|
||||||
|
import std::collections::bitset;
|
||||||
|
|
||||||
|
def Bits = bitset::BitSet(<128>);
|
||||||
|
|
||||||
|
fn void main()
|
||||||
|
{
|
||||||
|
Bits b;
|
||||||
|
io::printn($typeof(b.data[0]).sizeof);
|
||||||
|
}
|
||||||
19
test/test_color.c3
Normal file
19
test/test_color.c3
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import std::io;
|
||||||
|
|
||||||
|
struct Color { short r,g,b,a; }
|
||||||
|
|
||||||
|
macro Color uint.to_rgba(uint u)
|
||||||
|
{
|
||||||
|
return Color{
|
||||||
|
.r = (char)((u >> 24) & 0xff),
|
||||||
|
.g = (char)((u >> 16) & 0xff),
|
||||||
|
.b = (char)((u >> 8) & 0xff),
|
||||||
|
.a = (char)((u >> 0) & 0xff)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void main(String[] args)
|
||||||
|
{
|
||||||
|
uint col = args[1].to_uint()!!;
|
||||||
|
io::printn(col.to_rgba());
|
||||||
|
}
|
||||||
14
test/test_custom_hash.c3
Normal file
14
test/test_custom_hash.c3
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import std::collections::map;
|
||||||
|
|
||||||
|
def Codepoint = uint;
|
||||||
|
fn uint Codepoint.hash(Codepoint code) => code < 128 ? code : ((uint)code).hash();
|
||||||
|
def CodeMap = map::HashMap(<Codepoint, Codepoint>);
|
||||||
|
|
||||||
|
|
||||||
|
fn int main()
|
||||||
|
{
|
||||||
|
CodeMap m;
|
||||||
|
m.new_init();
|
||||||
|
m.free();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
72
test/test_error.c3
Normal file
72
test/test_error.c3
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import std::io;
|
||||||
|
|
||||||
|
struct FaultStack {
|
||||||
|
usz elem;
|
||||||
|
anyfault[16] v;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void FaultStack.push(&fs, anyfault f)
|
||||||
|
{
|
||||||
|
if (fs.elem < fs.v.len) {
|
||||||
|
fs.v[fs.elem++] = f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn anyfault FaultStack.pop(&fs)
|
||||||
|
{
|
||||||
|
return fs.elem > 0 ? fs.v[fs.elem-- - 1] : anyfault{};
|
||||||
|
}
|
||||||
|
|
||||||
|
FaultStack fs;
|
||||||
|
|
||||||
|
fn int! err1()
|
||||||
|
{
|
||||||
|
return IoError.OUT_OF_SPACE?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void! err2()
|
||||||
|
{
|
||||||
|
return IoError.EOF?;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
macro @unwrap(#f)
|
||||||
|
{
|
||||||
|
$if ($typeof(#f).typeid == void!.typeid) {
|
||||||
|
if (catch err = #f) { fs.push(err); }
|
||||||
|
return;
|
||||||
|
} $else {
|
||||||
|
$typeof(#f) x = #f;
|
||||||
|
if (catch err = x) {
|
||||||
|
fs.push(err);
|
||||||
|
return $typeof(#f!!){};
|
||||||
|
} else {return x;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
<*
|
||||||
|
@require @typekind(#func) == OPTIONAL : `@unwrap requires an optional value`
|
||||||
|
*>
|
||||||
|
macro @unwrap(#func)
|
||||||
|
{
|
||||||
|
anyfault exc = @catch(#func);
|
||||||
|
if (exc != anyfault{}) {
|
||||||
|
fs.push(exc);
|
||||||
|
$if $typeof(#func!!).typeid != void.typeid:
|
||||||
|
return $typeof(#func!!){};
|
||||||
|
$else
|
||||||
|
return;
|
||||||
|
$endif
|
||||||
|
} else {
|
||||||
|
return #func!!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void main()
|
||||||
|
{
|
||||||
|
@unwrap(err1());
|
||||||
|
@unwrap(err2());
|
||||||
|
|
||||||
|
io::printfn("%s", fs.v);
|
||||||
|
}
|
||||||
6
test/test_font.c3
Normal file
6
test/test_font.c3
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import rl;
|
||||||
|
|
||||||
|
fn int main(void)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
30
test/test_idgen.c3
Normal file
30
test/test_idgen.c3
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import std::io;
|
||||||
|
|
||||||
|
alias Id = uint;
|
||||||
|
|
||||||
|
fn void foo_ex(Id id)
|
||||||
|
{
|
||||||
|
io::printfn("id = %d", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
macro Id @compute_id(...)
|
||||||
|
{
|
||||||
|
Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE);
|
||||||
|
$for var $i = 0; $i < $vacount; $i++:
|
||||||
|
id ^= (Id)$vaconst[$i].hash();
|
||||||
|
$endfor
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro foo(...) => foo_ex(@compute_id($vasplat));
|
||||||
|
|
||||||
|
fn int main()
|
||||||
|
{
|
||||||
|
foo_ex(1234);
|
||||||
|
foo();
|
||||||
|
foo();
|
||||||
|
foo();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
26
test/test_keyboard.c3
Normal file
26
test/test_keyboard.c3
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import rl;
|
||||||
|
import std::io;
|
||||||
|
|
||||||
|
fn int main(String[] args)
|
||||||
|
{
|
||||||
|
short width = 800;
|
||||||
|
short height = 450;
|
||||||
|
rl::set_config_flags(rl::FLAG_WINDOW_RESIZABLE);
|
||||||
|
rl::init_window(width, height, "Ugui Test");
|
||||||
|
rl::set_target_fps(60);
|
||||||
|
rl::enable_event_waiting();
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
KeyboardKey k;
|
||||||
|
while (!rl::window_should_close()) {
|
||||||
|
do {
|
||||||
|
k = rl::get_char_pressed();
|
||||||
|
io::printfn("%s", k);
|
||||||
|
} while (k != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
rl::close_window();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
26
test/test_union.c3
Normal file
26
test/test_union.c3
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
struct CmdA {
|
||||||
|
int a, b;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CmdB {
|
||||||
|
float a, b;
|
||||||
|
}
|
||||||
|
|
||||||
|
union AnyCmd {
|
||||||
|
CmdA a;
|
||||||
|
CmdB b;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Cmd {
|
||||||
|
int type;
|
||||||
|
AnyCmd cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn int main()
|
||||||
|
{
|
||||||
|
Cmd c;
|
||||||
|
c.type = 1;
|
||||||
|
c.cmd.a = {.a = 1, .b = 2};
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
7
test/test_vtree.c3
Normal file
7
test/test_vtree.c3
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import std::io;
|
||||||
|
import vtree;
|
||||||
|
|
||||||
|
fn int main()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
226
test/ugui_font.c3
Normal file
226
test/ugui_font.c3
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
module ugui;
|
||||||
|
|
||||||
|
import cache;
|
||||||
|
//#include <grapheme.h>
|
||||||
|
//#include <assert.h>
|
||||||
|
|
||||||
|
//#include "stb_truetype.h"
|
||||||
|
//#include "stbimage_write.h"
|
||||||
|
|
||||||
|
// unicode code point, different type for a different hash
|
||||||
|
def Codepoint = uint;
|
||||||
|
|
||||||
|
/* width and height of a glyph contain the kering advance
|
||||||
|
* (u,v)
|
||||||
|
* +-------------*---+ -
|
||||||
|
* | ^ | | ^
|
||||||
|
* | |oy | | |
|
||||||
|
* | v | | |
|
||||||
|
* | .ii. | | |
|
||||||
|
* | @@@@@@. |<->| |
|
||||||
|
* | V@Mio@@o |adv| |h
|
||||||
|
* | :i. V@V | | |
|
||||||
|
* | :oM@@M | | |
|
||||||
|
* | :@@@MM@M | | |
|
||||||
|
* | @@o o@M | | |
|
||||||
|
* |<->:@@. M@M | | |
|
||||||
|
* |ox @@@o@@@@ | | |
|
||||||
|
* | :M@@V:@@.| | v
|
||||||
|
* +-------------*---+ -
|
||||||
|
* |<------------->|
|
||||||
|
* w
|
||||||
|
*/
|
||||||
|
struct Glyph {
|
||||||
|
Codepoint code;
|
||||||
|
uint u, v;
|
||||||
|
ushort w, h, a, x, y;
|
||||||
|
}
|
||||||
|
|
||||||
|
def GlyphCache = cache::Cache(<Codepoint, Glyph, 1024>);
|
||||||
|
|
||||||
|
// identity map the ASCII range
|
||||||
|
fn uint Codepoint.hash(Codepoint code) => code < 128 ? code : ((uint)code).hash();
|
||||||
|
|
||||||
|
struct FontAtlas {
|
||||||
|
uint width, height;
|
||||||
|
char* atlas;
|
||||||
|
uint glyph_max_w, glyph_max_h;
|
||||||
|
int size;
|
||||||
|
int file_size;
|
||||||
|
char *file;
|
||||||
|
void *priv;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
macro is_utf8(char c) => c & 0x80;
|
||||||
|
const uint BDEPTH = 1;
|
||||||
|
const uint BORDER = 4;
|
||||||
|
|
||||||
|
// FIXME: as of now only monospaced fonts look decent since no
|
||||||
|
// kerning information is stored
|
||||||
|
|
||||||
|
struct Priv @private {
|
||||||
|
stbtt_fontinfo stb;
|
||||||
|
float scale;
|
||||||
|
int baseline;
|
||||||
|
unsigned char *bitmap;
|
||||||
|
struct cache c;
|
||||||
|
}
|
||||||
|
//#define PRIV(x) ((struct priv *)x->priv)
|
||||||
|
|
||||||
|
|
||||||
|
struct font_atlas * font_init(void)
|
||||||
|
{
|
||||||
|
struct font_atlas *p = emalloc(sizeof(struct font_atlas));
|
||||||
|
memset(p, 0, sizeof(struct font_atlas));
|
||||||
|
p->priv = emalloc(sizeof(struct priv));
|
||||||
|
memset(p->priv, 0, sizeof(struct priv));
|
||||||
|
PRIV(p)->c = cache_init();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// loads a font into memory, storing all the ASCII characters in the atlas, each font
|
||||||
|
// atlas structure holds glyphs of a specific size in pixels
|
||||||
|
// NOTE: size includes ascend and descend (so 12 does not mean that 'A' is 12px tall)
|
||||||
|
int font_load(struct font_atlas *atlas, const char *path, int size)
|
||||||
|
{
|
||||||
|
if (!atlas || !path)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
int err;
|
||||||
|
|
||||||
|
dump_file(path, &(atlas->file), &(atlas->file_size));
|
||||||
|
|
||||||
|
err = stbtt_InitFont(&(PRIV(atlas)->stb), (unsigned char *)atlas->file, 0);
|
||||||
|
if (err == 0) return -1;
|
||||||
|
|
||||||
|
int ascent, descent, linegap, baseline;
|
||||||
|
int x0,y0,x1,y1;
|
||||||
|
float scale;
|
||||||
|
stbtt_GetFontVMetrics(&(PRIV(atlas)->stb), &ascent, &descent, &linegap);
|
||||||
|
stbtt_GetFontBoundingBox(&(PRIV(atlas)->stb), &x0, &y0, &x1, &y1);
|
||||||
|
scale = stbtt_ScaleForPixelHeight(&(PRIV(atlas)->stb), size);
|
||||||
|
baseline = scale * -y0;
|
||||||
|
atlas->glyph_max_w = (scale*x1) - (scale*x0);
|
||||||
|
atlas->glyph_max_h = (baseline+scale*y1) - (baseline+scale*y0);
|
||||||
|
atlas->atlas = emalloc(CACHE_SIZE*BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h);
|
||||||
|
memset(atlas->atlas, 0, CACHE_SIZE*BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h);
|
||||||
|
PRIV(atlas)->baseline = atlas->glyph_max_h - baseline;
|
||||||
|
PRIV(atlas)->scale = scale;
|
||||||
|
PRIV(atlas)->bitmap = emalloc(BDEPTH*atlas->glyph_max_w*atlas->glyph_max_h);
|
||||||
|
// FIXME: make this a square atlas
|
||||||
|
atlas->width = atlas->glyph_max_w*CACHE_SIZE/4;
|
||||||
|
atlas->height = atlas->glyph_max_h*4;
|
||||||
|
atlas->size = size;
|
||||||
|
|
||||||
|
// preallocate all ascii characters
|
||||||
|
for (char c = ' '; c <= '~'; c++) {
|
||||||
|
if (!font_get_glyph_texture(atlas, c, NULL))
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int font_free(struct font_atlas *atlas)
|
||||||
|
{
|
||||||
|
efree(atlas->atlas);
|
||||||
|
efree(atlas->file);
|
||||||
|
efree(PRIV(atlas)->bitmap);
|
||||||
|
cache_free(&PRIV(atlas)->c);
|
||||||
|
efree(atlas->priv);
|
||||||
|
efree(atlas);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: time and take the median of the time it takes to generate the cache and
|
||||||
|
// the time it takes to draw the glyph
|
||||||
|
const struct font_glyph * font_get_glyph_texture(struct font_atlas *atlas, unsigned int code, int *updated)
|
||||||
|
{
|
||||||
|
int _u = 0;
|
||||||
|
if (!updated) updated = &_u;
|
||||||
|
|
||||||
|
const struct font_glyph *r;
|
||||||
|
if ((r = cache_search(&PRIV(atlas)->c, code)) != NULL) {
|
||||||
|
*updated = 0;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
*updated = 1;
|
||||||
|
// generate the sdf and put it into the cache
|
||||||
|
// TODO: generate the whole block at once
|
||||||
|
int idx = stbtt_FindGlyphIndex(&PRIV(atlas)->stb, code);
|
||||||
|
int x0,y0,x1,y1,gw,gh,l,off_x,off_y,adv,base;
|
||||||
|
base = atlas->glyph_max_h - PRIV(atlas)->baseline;
|
||||||
|
stbtt_GetGlyphBitmapBoxSubpixel(
|
||||||
|
&PRIV(atlas)->stb,
|
||||||
|
idx,
|
||||||
|
PRIV(atlas)->scale,
|
||||||
|
PRIV(atlas)->scale,
|
||||||
|
0,0,
|
||||||
|
&x0,&y0,
|
||||||
|
&x1, &y1);
|
||||||
|
gw = x1 - x0;
|
||||||
|
gh = y1 - y0;
|
||||||
|
stbtt_GetGlyphHMetrics(&PRIV(atlas)->stb, idx, &adv, &l);
|
||||||
|
adv *= PRIV(atlas)->scale;
|
||||||
|
off_x = PRIV(atlas)->scale*l;
|
||||||
|
off_y = atlas->glyph_max_h+y0;
|
||||||
|
stbtt_MakeGlyphBitmapSubpixel(
|
||||||
|
&PRIV(atlas)->stb,
|
||||||
|
PRIV(atlas)->bitmap,
|
||||||
|
atlas->glyph_max_w,
|
||||||
|
atlas->glyph_max_h,
|
||||||
|
atlas->glyph_max_w,
|
||||||
|
PRIV(atlas)->scale,
|
||||||
|
PRIV(atlas)->scale,
|
||||||
|
0, 0,
|
||||||
|
idx);
|
||||||
|
|
||||||
|
// TODO: bounds check usign atlas height
|
||||||
|
// TODO: clear spot area in the atlas before writing on it
|
||||||
|
unsigned int spot = cache_get_free_spot(&PRIV(atlas)->c);
|
||||||
|
unsigned int ty = ((atlas->glyph_max_w * spot) / atlas->width) * atlas->glyph_max_h;
|
||||||
|
unsigned int tx = (atlas->glyph_max_w * spot) % atlas->width;
|
||||||
|
unsigned int w = atlas->width;
|
||||||
|
|
||||||
|
unsigned char *a = (void *)atlas->atlas;
|
||||||
|
|
||||||
|
//printf("max:%d %d spot:%d : %d %d %d %d\n", atlas->glyph_max_w, atlas->glyph_max_h, spot, tx, ty, off_x, off_y);
|
||||||
|
|
||||||
|
for (int y = 0; y < gh; y++) {
|
||||||
|
for (int x = 0; x < gw; x++) {
|
||||||
|
int c, r;
|
||||||
|
r = (ty+y)*w;
|
||||||
|
c = tx+x;
|
||||||
|
a[r+c] = PRIV(atlas)->bitmap[y*atlas->glyph_max_w+x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct font_glyph g = {
|
||||||
|
.codepoint = code,
|
||||||
|
.u = tx,
|
||||||
|
.v = ty,
|
||||||
|
.w = gw,
|
||||||
|
.h = gh,
|
||||||
|
.x = off_x,
|
||||||
|
.y = off_y-base,
|
||||||
|
.a = adv,
|
||||||
|
};
|
||||||
|
return cache_insert_at(&PRIV(atlas)->c, &g, g.codepoint, spot);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void font_dump(const struct font_atlas *atlas, const char *path)
|
||||||
|
{
|
||||||
|
stbi_write_png(
|
||||||
|
path,
|
||||||
|
atlas->width,
|
||||||
|
atlas->height,
|
||||||
|
BDEPTH,
|
||||||
|
atlas->atlas,
|
||||||
|
BDEPTH*atlas->width);
|
||||||
|
}
|
||||||
94
test_renderer.c3
Normal file
94
test_renderer.c3
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import sdlrenderer::ren;
|
||||||
|
import std::io;
|
||||||
|
import std::thread;
|
||||||
|
import sdl3::sdl;
|
||||||
|
import std::compression::qoi;
|
||||||
|
import std::core::mem::allocator;
|
||||||
|
|
||||||
|
char[*] shader_rect_vert = $embed("resources/shaders/compiled/rect.vert.spv");
|
||||||
|
char[*] shader_rect_frag = $embed("resources/shaders/compiled/rect.frag.spv");
|
||||||
|
|
||||||
|
char[*] shader_sprite_vert = $embed("resources/shaders/compiled/sprite.vert.spv");
|
||||||
|
char[*] shader_sprite_frag = $embed("resources/shaders/compiled/sprite.frag.spv");
|
||||||
|
|
||||||
|
const uint WINDOW_WIDTH = 640;
|
||||||
|
const uint WINDOW_HEIGHT = 480;
|
||||||
|
|
||||||
|
|
||||||
|
fn int main()
|
||||||
|
{
|
||||||
|
ren::Renderer ren;
|
||||||
|
ren.init("test window", WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||||
|
|
||||||
|
// TODO: these could be the same function
|
||||||
|
ren.load_spirv_shader_from_mem("rect shader", &shader_rect_vert, &shader_rect_frag, 0, 0);
|
||||||
|
ren.create_pipeline("rect shader", RECT);
|
||||||
|
|
||||||
|
// load the tux qoi image
|
||||||
|
QOIDesc img_desc;
|
||||||
|
char[] img_pixels = qoi::read(allocator::temp(), "resources/tux.qoi", &img_desc)!!;
|
||||||
|
// and put it in a texture
|
||||||
|
ren.new_texture("tux", FULL_COLOR, img_pixels, img_desc.width, img_desc.height);
|
||||||
|
// create a new pipeline to use the texture
|
||||||
|
ren.load_spirv_shader_from_mem("sprite shader", &shader_sprite_vert, &shader_sprite_frag, 1, 0);
|
||||||
|
ren.create_pipeline("sprite shader", SPRITE);
|
||||||
|
|
||||||
|
|
||||||
|
sdl::Event e;
|
||||||
|
bool quit = false;
|
||||||
|
for (usz i = 0; !quit; i++) {
|
||||||
|
|
||||||
|
if (sdl::poll_event(&e)) {
|
||||||
|
if (e.type == EVENT_QUIT) {
|
||||||
|
quit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 300) {
|
||||||
|
io::printn("ciao!");
|
||||||
|
img_pixels = qoi::read(allocator::temp(), "resources/tux_inv.qoi", &img_desc)!!;
|
||||||
|
ren.update_texture("tux", img_pixels, img_desc.width, img_desc.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
ren.begin_render(true);
|
||||||
|
|
||||||
|
// Colored Rectangles Render Pass
|
||||||
|
ren.start_render_pass("rect shader");
|
||||||
|
|
||||||
|
// rect 1
|
||||||
|
ren.push_quad(100,100,100,100,0xff00ff00, 20);
|
||||||
|
// rect 2
|
||||||
|
ren.push_quad(0,0,20,20,0xff0000ff);
|
||||||
|
// rect 3
|
||||||
|
ren.push_quad(200,300,50,50,0xffff0000);
|
||||||
|
|
||||||
|
//ren.set_scissor(0,50,200,300);
|
||||||
|
|
||||||
|
ren.draw_quads();
|
||||||
|
|
||||||
|
ren.end_render_pass();
|
||||||
|
// End Rectangle Render Pass
|
||||||
|
|
||||||
|
|
||||||
|
// Textured Rectangles Render Pass
|
||||||
|
ren.start_render_pass("sprite shader");
|
||||||
|
|
||||||
|
// bind the pipeline's sampler
|
||||||
|
ren.bind_texture("tux");
|
||||||
|
|
||||||
|
// tux
|
||||||
|
ren.push_sprite(300, 0, 54, 64, 0, 0);
|
||||||
|
|
||||||
|
ren.reset_scissor();
|
||||||
|
|
||||||
|
ren.draw_quads();
|
||||||
|
|
||||||
|
ren.end_render_pass();
|
||||||
|
// End Textured Rectangle Render Pass
|
||||||
|
|
||||||
|
ren.end_render();
|
||||||
|
}
|
||||||
|
|
||||||
|
ren.free();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user