Compare commits

..

199 Commits

Author SHA1 Message Date
1905191453 fix wrong id in scrollbar 2025-10-28 22:00:29 +01:00
8e39eee4af fix scolling for nested divs 2025-10-28 21:11:45 +01:00
da001601e5 fix wrong sprite scale 2025-10-28 00:26:37 +01:00
665e10fa30 fix absolute placement and scrollbar 2025-10-28 00:26:18 +01:00
66acf8d4a3 skip frame on resize 2025-10-27 20:03:24 +01:00
6a88ea55ec add license 2025-10-27 13:00:34 +01:00
3ac556b541 fix negative grow element size 2025-10-27 12:53:13 +01:00
1ec6eb88c9 implemented @popup() macro 2025-10-27 11:02:11 +01:00
49666294dc fixed @compute_id() to accept non-constants 2025-10-27 11:01:50 +01:00
18a86e8aab vertical and horizontal lines, @row and @column return their ids like @div 2025-10-26 17:48:42 +01:00
4f7fa7d50c major project restructure 2025-10-25 17:32:41 +02:00
bd31f562fc moved renderer to submodule 2025-10-25 17:11:00 +02:00
7b036ccee2 moved renderer to it's own library and module 2025-10-25 15:13:06 +02:00
26f38b342b fix weird input behavior 2025-10-25 15:11:55 +02:00
a8b7171709 enable 01 and parallel compilation 2025-10-23 22:31:58 +02:00
ed2b36ef0f better highlight color 2025-10-23 22:31:32 +02:00
bb6a166f2a reset more input fields on frame end 2025-10-23 22:31:03 +02:00
793fd1aa28 implement sprite scaling 2025-10-23 22:30:18 +02:00
c63d08c462 add @inline to font_load to fix segfault 2025-10-22 15:00:43 +02:00
a677d3f1f0 removed dependency on libgrapheme 2025-10-22 14:56:47 +02:00
7ff787f71f separate font module and font now allocates in arena 2025-10-22 14:35:35 +02:00
be51e37231 home and end 2025-10-20 23:37:36 +02:00
fe6f32c769 move around with ctrl 2025-10-20 16:15:57 +02:00
a512fe6c71 fix selection edge cases 2025-10-19 22:53:57 +02:00
2eec1fb710 better text edit 2025-10-19 21:10:51 +02:00
546f3628c7 semi-working text 2025-10-18 17:40:27 +02:00
ce9d1e6684 string layout with custom iterator 2025-10-16 22:38:56 +02:00
05a6d4803e move cursor with mouse 2025-10-14 10:44:13 +02:00
e3c0bac9ca rewrote string layout 2025-10-13 23:55:41 +02:00
5dfbad2399 notes 2025-10-13 00:26:36 +02:00
3b66e51cc6 Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-10-12 13:22:06 +02:00
eb62e9ad72 Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-10-12 13:19:52 +02:00
34d078b524 optional alpha channel in color properties 2025-10-12 13:19:49 +02:00
b9e91c3119 implemented a frame skip request 2025-10-11 22:32:00 +02:00
b5ef86d092 actually correct input handling 2025-10-10 22:31:28 +02:00
fb3a964f7f first draft 2025-10-08 22:16:17 +02:00
884105a4e2 faster sort of command queue 2025-10-07 17:47:24 +02:00
89f19ccf2e Update README.md 2025-10-06 23:46:27 +02:00
bbfc306984 sort cmd queue 2025-10-06 23:23:35 +02:00
6b8083ab5c remove fifo from imports 2025-10-05 23:14:16 +02:00
2bb907d523 use std::collections::list instead of custom fifo 2025-10-05 01:24:56 +02:00
d7cab085f7 removed vtree, contracts on mtree 2025-10-04 19:58:34 +02:00
01c2fa3367 contracts in font.c3 2025-10-04 19:42:59 +02:00
167676f478 use $feature() to enable debug features 2025-10-03 15:31:44 +02:00
8cecb57d93 use a tuple in get_elem 2025-10-03 15:19:52 +02:00
32a57d5293 popups 2025-10-02 23:19:42 +02:00
e7cfa3517f removed ugui_prefix in filenames 2025-09-30 22:27:59 +02:00
1f66c23919 use @row and @column macros 2025-09-30 22:19:14 +02:00
a9642f28bd add convenience function to draw a semi-transparent rectangle 2025-09-30 22:17:08 +02:00
6a7dd998a6 offset first character of a line to make alignment prettier 2025-09-30 22:16:42 +02:00
225f61079d Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-09-30 21:39:36 +02:00
76cef2caa0 added @row and @column macros 2025-09-30 21:39:00 +02:00
96fda0c5e9 re-implemented scrollbars 2025-09-29 23:51:02 +02:00
b99229b48d fix absoulte grow children 2025-09-29 23:24:19 +02:00
d47b835020 fix cursor placement 2025-09-27 12:43:38 +02:00
f1b6321d3d optional anchor in text_box 2025-09-27 12:43:21 +02:00
63b3d05b19 up and down in textedit 2025-09-25 23:16:18 +02:00
24216e4ab4 add separator widget 2025-09-24 22:49:51 +02:00
6a13245fd9 revert ccca2be49684cdcc905d1acc0b8d2c4cb126c4b4 2025-09-24 22:49:34 +02:00
fe9e2bdf49 moved test_renderer.c3 2025-09-23 23:32:34 +02:00
ccca2be496 "fixed" segfault with >00
idk if this is a compiler error but leaving the ascii pre-caching in results
in the pointer to the font structure to be corrupted before font.get_glyph()
2025-09-23 23:30:22 +02:00
6839a7e06c documentation of mtree 2025-09-23 23:30:11 +02:00
7f8b5196a5 new tree implementation
this about halves the time spent on level_order_it and drastically reduces the
time spent in children_it
2025-09-21 18:17:39 +02:00
c046c6af52 some comments 2025-09-20 00:27:24 +02:00
df18be7bf6 bools for VSYNC and FPS_LIMIT 2025-09-17 22:49:46 +02:00
48d1b29537 first draft for absolute positioning 2025-09-17 22:45:03 +02:00
915f395b5a corrected handling of newline in layout_string() 2025-09-16 17:06:19 +02:00
be1476d107 renderer now uses a single pipeline for ugui 2025-09-15 18:53:42 +02:00
622b648d26 corrected layout offset 2025-09-14 20:32:31 +02:00
d33d72a074 fixed check_key_combo 2025-09-13 20:02:21 +02:00
34b92c93b4 re-implemented text box
also includes
	- small layout fix for grow elements
	- ElemEvents now includes has_focus flag
2025-09-13 19:53:50 +02:00
81cc3dae65 div_end returns the div Id 2025-09-12 22:45:23 +02:00
d35ef7ddaf re-implemented toggles 2025-09-12 22:44:33 +02:00
48a333e501 simplified code 2025-09-12 22:18:15 +02:00
71a959b9a1 re-added sliders 2025-09-12 20:23:07 +02:00
a00e39f36b Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-09-12 12:52:31 +02:00
e328a67d96 re-implemented checkbox 2025-09-12 12:48:29 +02:00
be951c616a fix dimensions being calculated wrong 2025-09-12 12:47:00 +02:00
636f162b10 fixed problem with >31 elements 2025-09-12 11:44:26 +02:00
2bd15ac981 Merge branch 'c3' of https://git.alemauri.eu/alema/ugui into c3 2025-09-11 19:00:46 +02:00
f8befeea4d idk 2025-09-11 19:00:41 +02:00
9d96d2eb74 moved widgets to their own folder 2025-09-09 20:01:10 +02:00
c3a6390404 draw text correctly 2025-09-09 19:10:04 +02:00
db63b2c6b1 layout actually works now 2025-09-08 23:43:59 +02:00
869c7871f9 minor changes 2025-09-06 12:50:36 +02:00
3d7be2a2df implement operator overloading for rects 2025-09-05 19:56:59 +02:00
335624fcbe in layout use a combined version of margin, border and padding 2025-09-05 18:53:47 +02:00
0f7d5a6506 working example of the new layout system 2025-09-05 13:10:16 +02:00
2619873ca7 tested new layout system 2025-09-03 23:32:04 +02:00
24ac28e0d9 update sdl3.c3l 2025-08-29 19:25:18 +02:00
5e5c912092 use the specified allocator for the element caches 2025-08-29 19:24:52 +02:00
4a690fdeb5 actually useful calculator example 2025-08-20 17:33:25 +02:00
62ebd6592d fix wrong scissor 2025-08-20 17:32:45 +02:00
00299bec0b fix calculator demo
turns out it was an incorrect handling of  scissor test
2025-08-16 10:10:06 +02:00
be00c87c6a implement a convenient macro to start and end a div 2025-08-14 22:16:54 +02:00
e8bb35811a renamed position_element() to layout_element() 2025-07-14 13:16:04 +02:00
278e4988e9 use containing_rect() in position_element() 2025-07-14 13:10:14 +02:00
78fc1c1e87 cleaner get_parent() 2025-07-14 13:05:30 +02:00
8d79f13fd6 added another demo ui 2025-07-14 12:59:07 +02:00
7713cd7da9 prettier radius 2025-07-14 12:58:15 +02:00
00aa01109e crash if last element is not root 2025-07-14 12:57:53 +02:00
5e68671828 fixed divs-in-divs 2025-07-14 12:57:29 +02:00
8367f6b617 better css lexer 2025-07-13 21:43:57 +02:00
dd073385c8 fix slider styling 2025-07-13 20:12:48 +02:00
80d17d7b33 unified button element 2025-07-13 20:08:18 +02:00
b48c413d2d changed text (glyph) placement 2025-07-10 11:05:39 +02:00
c1c1247af4 update TODO 2025-07-07 11:46:40 +02:00
5ae9b05223 moved style to style.css 2025-07-07 11:37:56 +02:00
9afb0d2acd better default style handling 2025-07-06 23:50:36 +02:00
c1bf8e891b fixed regression in scrollbars in divs 2025-07-06 01:41:57 +02:00
777e974841 use new style system 2025-07-05 16:37:08 +02:00
9fc1d90455 simple style import with a subset of css 2025-07-04 11:17:42 +02:00
a390a8908c changed Id to uint since builtin hash functions hash to uints 2025-07-01 16:48:30 +02:00
c49a689304 get_elem now also pushes into the tree and check the correct type 2025-07-01 16:03:11 +02:00
849b267f91 renamed get_element_by_tree_idx to get_active_div 2025-07-01 15:33:22 +02:00
1de4fd78b8 better checkboxes 2025-07-01 15:28:08 +02:00
586e81935c improved the api to not require explicit labels everywhere 2025-06-30 18:24:50 +02:00
972c9b581d text input box 2025-06-30 13:10:00 +02:00
6d2594db2d test id generation with macros 2025-06-30 13:08:52 +02:00
9827a899f1 get_line_height and get_cursor_position 2025-06-30 13:08:27 +02:00
88d9b65028 updated build configuration 2025-06-30 13:07:42 +02:00
c98c00bfb9 ecode editor config 2025-06-25 10:47:44 +02:00
fc3fa32ddd more smoothing on rounded corners 2025-06-25 10:47:23 +02:00
cd83c528ee fixed vulkan validation errors 2025-06-19 15:24:46 +02:00
5b0169cd94 better input handling 2025-06-17 18:23:36 +02:00
b411718c94 add fps counter 2025-06-15 23:47:09 +02:00
05232c1d24 do a single upload pass to reduce badwidth 2025-06-15 23:35:37 +02:00
0223536ac8 Merge branch 'c3-instanced' into c3 2025-06-15 20:54:57 +02:00
f30db0dd47 implemented instanced rendering 2025-06-15 18:54:35 +02:00
865c7dabaa indirect rendering 2025-06-15 00:26:27 +02:00
0ef2bceeec handle scissor and vsync 2025-06-14 15:09:34 +02:00
cda2f27a49 Batch uploads to the gpu 2025-06-14 14:49:53 +02:00
10ee643a0c disable vsync 2025-06-12 20:01:33 +02:00
00f5e71666 enable cycling when mapping the transfer buffers to avoid corruption 2025-06-12 19:56:43 +02:00
458c45d2b9 using the new renderer 2025-06-12 19:49:43 +02:00
c4a3dd3a26 tweak ugui api, add some usefult functions 2025-06-12 18:56:57 +02:00
2014c67bfd tweak renderer API 2025-06-12 18:56:20 +02:00
177e52b0d0 rounded quads baby! 2025-06-10 12:54:03 +02:00
3a0904023a updated sdl3.c3l location 2025-06-10 12:53:52 +02:00
c4c9716d61 remove mqoi dependency in ugui manifest 2025-06-09 16:35:23 +02:00
6208711292 use the language's qoi decoder 2025-06-09 16:06:49 +02:00
39bd7fb8bc removed module raylib.c3l (non vendor) 2025-06-09 15:51:16 +02:00
47eb3ff907 moved vendor libraries to lib/ as a submodule 2025-06-09 15:50:29 +02:00
21aa70d340 create binary directory before copying 2025-06-09 15:10:38 +02:00
bd8c73ecd5 sdl3.c3l as a submodule 2025-06-09 12:30:25 +02:00
6e65700f38 scary quads and nice sprites 2025-06-07 12:42:57 +02:00
e3d87525d4 draw multiple quads 2025-06-07 10:35:08 +02:00
3002123ef7 removed bad error handling and replaced it with worse error handling 2025-06-03 22:36:18 +02:00
ac3fcae649 sorting the command buffer 2025-06-03 22:03:14 +02:00
c9b74aebc7 implement z index in command buffer 2025-06-03 18:15:46 +02:00
f344c989db test different draw calls for one render pass (failed) 2025-06-03 18:15:33 +02:00
6c5acd6f23 enable foreach for FIFO 2025-06-03 18:15:12 +02:00
24bc2c67bc changed the pipeline to use 16 bit int as coords
thank you renderdoc for making me feel less stupid
2025-06-03 09:16:51 +02:00
712ce50631 A lot of work
* moved all ugui code to lib/ugui.c3l and made it a library/module
* started work on a sdl3 renderer, with shaders etc
* added the new sdl3.c3l library as a dependency
* makefile is for the renderer
2025-06-01 16:44:31 +02:00
2380c7693c add sdl3 dependency 2025-05-21 23:38:59 +02:00
79a2d66880 update project to c3 0.7.1 2025-05-05 16:23:26 +02:00
34e75f8c06 larger font cache 2025-02-08 12:51:10 +01:00
7c6f7d31d2 quick and dirty checkbox 2025-02-07 23:46:21 +01:00
52f3929a42 renamed ATlAS_RGBA32 to ATLAS_R8G8B8A8 2025-02-06 23:51:15 +01:00
e09107af98 simpler main 2025-02-06 23:44:03 +01:00
588a417413 checkbox and msdf sprite rendering 2025-02-04 22:40:44 +01:00
14359a9b7e first checkbox 2025-02-03 23:07:32 +01:00
196a2474fd todos 2025-02-03 23:07:14 +01:00
c53b9eed5e MAYBE correct layout with next_row and next_column 2025-02-01 01:01:13 +01:00
92614e4d8b removed comments 2025-01-31 23:15:40 +01:00
d94e430807 added library submodules 2025-01-31 12:37:15 +01:00
de64746fdf renamed libraries 2025-01-31 12:20:19 +01:00
0531f58a56 less cached elements by default 2025-01-30 22:27:46 +01:00
f516a68cee correct font atlas size 2025-01-30 22:27:24 +01:00
07857fcd44 switch to c3s' vendor raylib 5.5 2025-01-30 19:38:53 +01:00
fbe631b4b4 better sprites 2025-01-30 18:36:47 +01:00
b317951c32 first working prototype of sprite drawing 2025-01-29 01:10:18 +01:00
9aa0d58d68 better to_rgba() macro 2024-12-28 16:59:12 +01:00
78e2c64da6 implemented adaptive size divs 2024-12-26 22:58:43 +01:00
16adfd7cc5 idk look at the changes 2024-12-25 12:30:35 +01:00
1746f7d940 update TODO 2024-12-23 15:56:40 +01:00
169b5e1dfd cull commands that result in zero-area bounding boxes 2024-12-23 15:49:46 +01:00
87de68028a update todo 2024-12-20 20:58:12 +01:00
04843fe714 do not use xor to combine keys since when more than two layers deep this would result in identical keys 2024-12-20 20:17:53 +01:00
a0c6a3b2cb work on button with label 2024-12-20 19:50:58 +01:00
ca691d1294 enlarge scrollbars when focused 2024-12-20 02:17:41 +01:00
f7985f8c7f track focused and hovered elements 2024-12-20 01:45:10 +01:00
4bd827ce5c set ids <facepalm> 2024-12-19 19:42:02 +01:00
d31b4eab53 update todo 2024-12-19 15:27:14 +01:00
1088083e1e stuff, mostly renaming variables 2024-12-19 15:24:39 +01:00
601a396aa8 scrollbar fixup 2024-12-19 00:29:30 +01:00
a481269022 work on div sliders, major changes
* Ids are now keyed based on the parent's id, this means that an element can have
  the same label when placed in different divs
* Divs now enable the scissor test, this way the elements cannot draw outside of
  the parent div bounds
* Introduced a LAYOUT_ABSOLUTE that disables all layout logic, for internal use
* Divs now draw scrollbars using the slider_hor and slider_ver elements
2024-12-18 20:04:23 +01:00
499f6dc79b removed force update 2024-12-18 15:10:17 +01:00
740ea0c6be merged ugui_impl and ugui_data to ugui_core 2024-12-18 15:02:46 +01:00
8d4b353e88 a lot of work on sliders 2024-12-18 14:58:40 +01:00
c0e9565bf6 idk some stuff 2024-12-17 11:26:59 +01:00
c1a7b4fcdb display timing statistics 2024-12-16 17:06:46 +01:00
7d9a8a1363 pre-cache ascii range in font atlas 2024-12-16 17:06:33 +01:00
2e0c6333d3 draw border around floating divs 2024-12-16 17:06:16 +01:00
3a7655a3f0 specify texture id in the sprite command 2024-12-16 17:05:44 +01:00
7b7aac8df4 schrift use ZString where necessary 2024-12-16 14:08:08 +01:00
2e60e4c5b8 fix font alpha channel 2024-12-16 14:07:44 +01:00
bca29c537c scissor command 2024-12-15 22:29:07 +01:00
6d8300f9d9 better font atlas implementation 2024-12-15 21:39:26 +01:00
66 changed files with 5074 additions and 2916 deletions

4
.gitignore vendored
View File

@ -2,3 +2,7 @@
*.a *.a
build/* build/*
**/.ccls-cache **/.ccls-cache
perf.data*
*.rdc
test_renderer
resources/shaders/compiled/**

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "lib/raylib.c3l"]
path = lib/raylib.c3l
url = https://github.com/NexushasTaken/raylib.c3l

View File

@ -1,184 +0,0 @@
## 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 Normal file
View File

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

165
LICENSE
View File

@ -0,0 +1,165 @@
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.

View File

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

133
TODO
View File

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

View File

View File

@ -1,3 +0,0 @@
all:
make -C thirdparty/libgrapheme
cp thirdparty/libgrapheme/libgrapheme.a linux-x64/libgrapheme.a

View File

@ -1,46 +0,0 @@
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");

View File

@ -1,9 +0,0 @@
{
"provides": "grapheme",
"targets": {
"linux-x64": {
"dependencies": [],
"linked-libraries": ["grapheme", "c"]
}
}
}

View File

@ -1,44 +0,0 @@
{
// Language version of C3.
"langrev": "1",
// Warnings used for all targets.
"warnings": ["no-unused"],
// Directories where C3 library files may be found.
"dependency-search-paths": [".."],
// Libraries to use for all targets.
"dependencies": ["grapheme"],
// Authors, optionally with email.
"authors": ["Alessandro Mauri <alemauri001@gmail.com"],
// Version using semantic versioning.
"version": "0.1.0",
// Sources compiled for all targets.
"sources": [],
// C sources if the project also compiles C sources
// relative to the project file.
// "c-sources": [ "csource/**" ],
// Output location, relative to project file.
"output": "build",
// Architecture and OS target.
// You can use 'c3c --list-targets' to list all valid targets.
// "target": "windows-x64",
"features": [
// See rcore.c3
//"SUPPORT_INTERNAL_MEMORY_MANAGEMENT",
//"SUPPORT_STANDARD_FILEIO",
//"SUPPORT_FILE_SYSTEM_FUNCTIONS",
//"SUPPORT_DATA_ENCODER",
// See text.c3
//"SUPPORT_TEXT_CODEPOINTS_MANAGEMENT",
//"SUPPORT_TEXT_C_STRING_MANAGEMENT",
//"SUPPORT_RANDOM_GENERATION",
//"SUPPORT_RAYGUI",
//"RAYGUI_NO_ICONS",
//"RAYGUI_CUSTOM_ICONS",
],
// Global settings.
// CPU name, used for optimizations in the LLVM backend.
"cpu": "generic",
// Optimization: "O0", "O1", "O2", "O3", "O4", "O5", "Os", "Oz".
"opt": "O0"
// See resources/examples/project_all_settings.json and 'c3c --list-project-properties' to see more properties.
}

@ -1 +0,0 @@
Subproject commit 65b354f0fcb1d925f4340dbb4415ea06e8af2bec

View File

@ -1,3 +0,0 @@
all:
make -C thirdparty/libschrift
cp thirdparty/libschrift/libschrift.a linux-x64/libschrift.a

View File

@ -1,58 +0,0 @@
module schrift;
def SftFont = void*;
def SftUChar = uint;
def 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 char* sft_version() @extern("sft_version");
extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem");
extern fn SftFont loadfile(char* 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");

View File

@ -1,9 +0,0 @@
{
"provides" : "schrift",
"targets" : {
"linux-x64" : {
"dependencies" : [],
"linked-libraries" : ["schrift", "c"]
}
}
}

View File

@ -1,45 +0,0 @@
{
// Language version of C3.
"langrev": "1",
// Warnings used for all targets.
"warnings": [ "no-unused" ],
// Directories where C3 library files may be found.
"dependency-search-paths": [ ".." ],
// Libraries to use for all targets.
"dependencies": [ "schrift" ],
// Authors, optionally with email.
"authors": [ "Alessandro Mauri <alemauri001@gmail.com" ],
// Version using semantic versioning.
"version": "0.1.0",
// Sources compiled for all targets.
"sources": [ ],
// C sources if the project also compiles C sources
// relative to the project file.
// "c-sources": [ "csource/**" ],
// Output location, relative to project file.
"output": "build",
// Architecture and OS target.
// You can use 'c3c --list-targets' to list all valid targets.
// "target": "windows-x64",
"features": [
// See rcore.c3
//"SUPPORT_INTERNAL_MEMORY_MANAGEMENT",
//"SUPPORT_STANDARD_FILEIO",
//"SUPPORT_FILE_SYSTEM_FUNCTIONS",
//"SUPPORT_DATA_ENCODER",
// See text.c3
//"SUPPORT_TEXT_CODEPOINTS_MANAGEMENT",
//"SUPPORT_TEXT_C_STRING_MANAGEMENT",
//"SUPPORT_RANDOM_GENERATION",
//"SUPPORT_RAYGUI",
//"RAYGUI_NO_ICONS",
//"RAYGUI_CUSTOM_ICONS",
],
// Global settings.
// CPU name, used for optimizations in the LLVM backend.
"cpu": "generic",
// Optimization: "O0", "O1", "O2", "O3", "O4", "O5", "Os", "Oz".
"opt": "O0",
// See resources/examples/project_all_settings.json and 'c3c --list-project-properties' to see more properties.
}

@ -1 +0,0 @@
Subproject commit 24737d2922b23df4a5692014f5ba03da0c296112

@ -1 +0,0 @@
Subproject commit c7ebe054ce16136c1128fab54fcce4921044293e

11
manifest.json Normal file
View File

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

View File

@ -1,55 +0,0 @@
{
// Language version of C3.
"langrev": "1",
// Warnings used for all targets.
"warnings": [ "no-unused" ],
// Directories where C3 library files may be found.
"dependency-search-paths": [ "lib" ],
// Libraries to use for all targets.
"dependencies": [ "raylib", "schrift", "grapheme" ],
"features": [
// See rcore.c3
//"SUPPORT_INTERNAL_MEMORY_MANAGEMENT",
//"SUPPORT_STANDARD_FILEIO",
//"SUPPORT_FILE_SYSTEM_FUNCTIONS",
//"SUPPORT_DATA_ENCODER",
// See text.c3
//"SUPPORT_TEXT_CODEPOINTS_MANAGEMENT",
//"SUPPORT_TEXT_C_STRING_MANAGEMENT",
//"SUPPORT_RANDOM_GENERATION",
//"SUPPORT_RAYGUI",
//"RAYGUI_NO_ICONS",
//"RAYGUI_CUSTOM_ICONS",
],
// Authors, optionally with email.
"authors": [ "John Doe <ale@shitposting.expert>" ],
// Version using semantic versioning.
"version": "0.1.0",
// Sources compiled for all targets.
"sources": [ "src/**" ],
// C sources if the project also compiles C sources
// relative to the project file.
// "c-sources": [ "csource/**" ],
// Include directories for C sources relative to the project file.
// "c-include-dirs": [ "csource/include" ],
// Output location, relative to project file.
"output": "build",
// Architecture and OS target.
// You can use 'c3c --list-targets' to list all valid targets.
// "target": "windows-x64",
// Targets.
"targets": {
"ugui": {
// Executable or library.
"type": "executable",
// Additional libraries, sources
// and overrides of global settings here.
},
},
// Global settings.
// CPU name, used for optimizations in the LLVM backend.
"cpu": "generic",
// Optimization: "O0", "O1", "O2", "O3", "O4", "O5", "Os", "Oz".
"opt": "O0",
// See resources/examples/project_all_settings.json and 'c3c --list-project-properties' to see more properties.
}

View File

View File

131
src/atlas.c3 Normal file
View File

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

View File

@ -1,4 +1,4 @@
module cache(<Key, Value, SIZE>); module cache{Key, Value, SIZE};
/* LRU Cache /* LRU Cache
* The cache uses a pool (array) to store all the elements, each element has * The cache uses a pool (array) to store all the elements, each element has
@ -9,21 +9,19 @@ 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::collections::bitset; import std::collections::bitset;
import std::collections::map; import std::collections::map;
def BitArr = bitset::BitSet(<SIZE>) @private; alias BitArr = bitset::BitSet{SIZE};
def IdTable = map::HashMap(<Key, usz>) @private; alias IdTable = map::HashMap{Key, usz};
def IdTableEntry = map::Entry(<Key, usz>) @private; 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;
@ -42,57 +40,75 @@ macro Cache.cycle(&cache) @private {
} }
} }
fn void! Cache.init(&cache) fn void? Cache.init(&cache, Allocator allocator)
{ {
cache.table.new_init(capacity: SIZE); cache.allocator = allocator;
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 = mem::new_array(Value, SIZE); cache.pool = allocator::new_array(allocator, Value, SIZE);
} }
fn void Cache.free(&cache) fn void Cache.free(&cache)
{ {
(void)cache.table.free(); (void)cache.table.free();
(void)mem::free(cache.pool); (void)allocator::free(cache.allocator, cache.pool);
} }
fn Value*! Cache.search(&cache, Key id) fn Value*? Cache.search(&cache, Key id)
{ {
// get_entry() faults on miss // get_entry() faults on miss
IdTableEntry* entry = cache.table.get_entry(id)!; IdTableEntry* entry = cache.table.get_entry(id)!;
/* MISS */ /* MISS, wrong key */
if (entry.key != id) { if (entry.key != id) {
return SearchResult.MISSING?; cache.table.remove(id)!;
return NOT_FOUND?;
} }
/* MISS, the data is not valid (not present) */ /* MISS, the data is not valid (not present) */
if (!cache.present[entry.value]) { if (!cache.present[entry.value]) {
// if the data is not present but it is still in the table, remove it // if the data is not present but it is still in the table, remove it
cache.table.remove(id)!; cache.table.remove(id)!;
return SearchResult.MISSING?; return NOT_FOUND?;
} }
/* 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]);
} }
fn void Cache.remove(&cache, Key id)
{
IdTableEntry*? entry = cache.table.get_entry(id);
if (catch entry) {
return;
}
// found, remove it
cache.present[entry.value] = false;
(void)cache.table.remove(id);
}
/* Look for a free spot in the present bitmap and return its index */ /* Look for a free spot in the present bitmap and return its index */
/* If there is no free space left then just return the first position */ /* 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.clz() != BITS) { if (d != $typeof(d).max) {
return idx*BITS + BITS-d.clz(); usz spot = 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;
@ -109,17 +125,17 @@ fn Value*! Cache.insert_at(&cache, Value *g, Key id, usz index) @private
} }
// Insert an element in the cache, returns the index // Insert an element in the cache, returns the index
fn Value*! Cache.insert_new(&cache, Value* g, Key id) fn Value*? Cache.insert_new(&cache, Value* g, Key id)
{ {
usz index = cache.get_free_spot(); usz index = cache.get_free_spot();
return cache.insert_at(g, id, index); return cache.insert_at(g, id, index);
} }
fn Value*! Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null) fn Value*? Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null)
{ {
Value*! c = cache.search(id); Value*? c = cache.search(id);
if (catch e = c) { if (catch e = c) {
if (e != SearchResult.MISSING) { if (e != NOT_FOUND) {
return e?; return e?;
} else { } else {
// if the element is new (inserted) set the is_new flag // if the element is new (inserted) set the is_new flag

217
src/cmd.c3 Normal file
View File

@ -0,0 +1,217 @@
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()})!!;

396
src/core.c3 Normal file
View File

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

View File

@ -1,49 +0,0 @@
module fifo(<Type>);
import std::core::mem;
fault FifoErr {
FULL,
EMPTY,
}
// 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 FifoErr.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 FifoErr.EMPTY?;
}
Type *ret = &fifo.arr[fifo.out];
fifo.count--;
fifo.out = (fifo.out + 1) % fifo.arr.len;
return ret;
}

227
src/font.c3 Normal file
View File

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

237
src/input.c3 Normal file
View File

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

353
src/layout.c3 Normal file
View File

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

View File

@ -1,238 +0,0 @@
import std::io;
import vtree;
import cache;
import ugui;
import rl;
import std::time;
import std::collections::ringbuffer;
def Times = ringbuffer::RingBuffer(<time::NanoDuration, 128>);
fn void Times.print_stats(&times)
{
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/128.0);
io::printfn("min=%s, max=%s, avg=%s", min, max, avg);
}
fn int main(String[] args)
{
ugui::Ctx ui;
ui.init()!!;
ui.font.load("/usr/share/fonts/TTF/HackNerdFontMono-Regular.ttf", 16)!!;
short width = 800;
short height = 450;
rl::set_config_flags(rl::FLAG_WINDOW_RESIZABLE);
rl::init_window(width, height, "Ugui Test");
ui.input_window_size(width, height)!!;
rl::set_target_fps(60);
rl::enable_event_waiting();
isz frame;
bool toggle = true;
time::Clock clock;
Times ui_times;
Times draw_times;
// Main loop
while (!rl::window_should_close()) {
clock.mark();
/* Start Input Handling */
if (rl::is_window_resized()) {
width = (short)rl::get_screen_width();
height = (short)rl::get_screen_height();
ui.input_window_size(width, height)!!;
}
ui.input_changefocus(rl::is_window_focused());
rl::Vector2 mpos = rl::get_mouse_position();
ui.input_mouse_abs((short)mpos.x, (short)mpos.y);
ugui::MouseButtons buttons;
buttons.btn_left = rl::is_mouse_button_down(rl::MOUSE_BUTTON_LEFT);
buttons.btn_right = rl::is_mouse_button_down(rl::MOUSE_BUTTON_RIGHT);
buttons.btn_middle = rl::is_mouse_button_down(rl::MOUSE_BUTTON_MIDDLE);
ui.input_mouse_button(buttons);
/* End Input Handling */
/* Start UI Handling */
ui.frame_begin()!!;
// main div, fill the whole window
ui.div_begin("main", ugui::Rect{.w=ui.width/2})!!;
{|
ui.layout_set_row()!!;
if (ui.button("button0", ugui::Rect{0,0,30,30})!!.mouse_press) {
io::printn("press button0");
toggle = !toggle;
ui.force_update()!!;
}
//ui.layout_next_column()!!;
if (ui.button("button1", ugui::Rect{0,0,30,30})!!.mouse_press) {
io::printn("press button1");
}
//ui.layout_next_column()!!;
if (toggle) {
if (ui.button("button2", ugui::Rect{0,0,30,30})!!.mouse_release) {
io::printn("release button2");
}
}
if (ui.slider_ver("slider", ugui::Rect{0,0,30,100})!!.update) {
ugui::Elem* e = ui.get_elem_by_label("slider")!!;
io::printfn("slider: %f", e.slider.value);
}
ui.text_unbounded("text1", "Ciao Mamma\nAbilità ⚡")!!;
|};
ui.div_end()!!;
ui.div_begin("second", ugui::DIV_FILL)!!;
ugui::Elem* de = ui.get_elem_by_label("second")!!;
de.div.scroll.can_y = true;
{|
ui.layout_set_column()!!;
if (ui.slider_ver("slider_other", ugui::Rect{0,0,30,100})!!.update) {
ugui::Elem* e = ui.get_elem_by_label("slider_other")!!;
io::printfn("other slider: %f", e.slider.value);
}
ui.button("button10", ugui::Rect{0,0,50,50})!!;
ui.button("button11", ugui::Rect{0,0,50,50})!!;
ui.button("button12", ugui::Rect{0,0,50,50})!!;
ui.button("button13", ugui::Rect{0,0,50,50})!!;
ui.button("button14", ugui::Rect{0,0,50,50})!!;
ui.button("button15", ugui::Rect{0,0,50,50})!!;
ui.button("button16", ugui::Rect{0,0,50,50})!!;
ui.button("button17", ugui::Rect{0,0,50,50})!!;
|};
ui.div_end()!!;
ui.frame_end()!!;
/* End UI Handling */
ui_times.push(clock.mark());
ui_times.print_stats();
/* Start UI Drawing */
rl::begin_drawing();
// ClearBackground(BLACK);
rl::Color c;
rl::Rectangle r;
static rl::Image font_atlas;
static rl::Texture2D font_texture;
for (Cmd* cmd; (cmd = ui.cmd_queue.dequeue() ?? null) != null;) {
switch (cmd.type) {
case ugui::CmdType.CMD_RECT:
c = rl::Color{
.r = cmd.rect.color.r,
.g = cmd.rect.color.g,
.b = cmd.rect.color.b,
.a = cmd.rect.color.a,
};
r = rl::Rectangle{
.x = cmd.rect.rect.x,
.y = cmd.rect.rect.y,
.height = cmd.rect.rect.h,
.width = cmd.rect.rect.w,
};
float rad = cmd.rect.radius;
// for some weird-ass reason the straight forward inverse formula does not work
float roundness = r.width > r.height ? (2.1*rad)/r.height : (2.1*rad)/r.width;
rl::draw_rectangle_rounded(r, roundness, 0, c);
case ugui::CmdType.CMD_UPDATE_ATLAS:
rl::unload_image(font_atlas);
font_atlas.data = cmd.update_atlas.raw_buffer;
font_atlas.width = cmd.update_atlas.width;
font_atlas.height = cmd.update_atlas.height;
font_atlas.mipmaps = 1;
//font_atlas.format = rl::PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE;
font_atlas.format = 1;
if (rl::is_texture_ready(font_texture)) {
rl::unload_texture(font_texture);
}
font_texture = rl::load_texture_from_image(font_atlas);
//rl::draw_texture(font_texture, 0, 0, rl::WHITE);
case ugui::CmdType.CMD_SPRITE:
rl::Rectangle source = {
.x = cmd.sprite.texture_rect.x,
.y = cmd.sprite.texture_rect.y,
.width = cmd.sprite.texture_rect.w,
.height = cmd.sprite.texture_rect.h,
};
rl::Vector2 position = {
.x = cmd.sprite.rect.x,
.y = cmd.sprite.rect.y,
};
rl::draw_texture_rec(font_texture, source, position, rl::WHITE);
//rl::draw_rectangle(cmd.sprite.rect.x,
// cmd.sprite.rect.y,
// cmd.sprite.rect.w,
// cmd.sprite.rect.h,
// rl::WHITE
//);
default:
io::printfn("Unknown cmd type: %s", cmd.type);
}
}
draw_times.push(clock.mark());
draw_times.print_stats();
rl::end_drawing();
/* End Drawing */
}
rl::close_window();
ui.font.free();
ui.free();
return 0;
}
/*
fn void! test_vtree() @test
{
vtree::VTree(<String>) vt;
vt.init(10)!!;
defer vt.free();
assert(vt.size() == 10, "Size is incorrect");
isz ref = vt.add("Ciao Mamma", 0)!!;
String s = vt.get(ref)!!;
assert(s == "Ciao Mamma", "String is incorrect");
isz par = vt.parentof(0)!!;
assert(ref == par, "Not Root");
vt.print();
}
def StrCache = cache::Cache(<int, String, 256>);
fn void! test_cache() @test
{
StrCache cc;
cc.init()!!;
defer cc.free();
String*! r = cc.search(1);
if (catch ex = r) {
if (ex != SearchResult.MISSING) {
return ex?;
}
}
r = cc.get_or_insert(&&"Ciao Mamma", 1)!;
assert(*r!! == "Ciao Mamma", "incorrect string");
}
*/

367
src/mtree.c3 Normal file
View File

@ -0,0 +1,367 @@
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
);
}
}
}

268
src/shapes.c3 Normal file
View File

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

107
src/sprite.c3 Normal file
View File

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

588
src/string.c3 Normal file
View File

@ -0,0 +1,588 @@
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;
}

461
src/style.c3 Normal file
View File

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

288
src/textedit.c3 Normal file
View File

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

View File

@ -1,183 +0,0 @@
module ugui;
import std::core::mem;
import std::math::random;
random::SimpleRandom atlas_seed;
fault UgAtlasError {
CANNOT_PLACE,
INVALID_FORMAT,
MISSING_ATLAS,
}
enum AtlasFormat {
ATLAS_GRAYSCALE,
ATLAS_ALPHA,
}
// black and white atlas
struct Atlas {
AtlasFormat format;
Id id;
ushort width, height;
char[] buffer;
Point row;
ushort row_h;
}
// FIXME: allocate atlases dynamically
fn void! Ctx.init_atlases(&ctx)
{
atlas_seed.set_seed("ciao mamma");
ctx.atlas = mem::new_array(Atlas, MAX_ATLAS);
}
fn void Ctx.free_atlases(&ctx)
{
foreach (a: ctx.atlas) {
a.free();
}
mem::free(ctx.atlas);
}
// FIXME: this could be slow
fn Atlas*! Ctx.get_atlas(&ctx, Id id)
{
foreach (i, a: ctx.atlas) {
if (a.id == id) {
return &ctx.atlas[i];
}
}
return UgAtlasError.MISSING_ATLAS?;
}
// TODO: use the same allocator as all the other parts of the code
fn Atlas*! Ctx.request_new_atlas(&ctx, AtlasFormat format, Id *rid, ushort width, ushort height)
{
usz idx;
bool free = false;
for (; idx < ctx.atlas.len; idx++) {
if (ctx.atlas[idx].id == 0) {
free = true;
break;
}
}
if (free) {
*rid = ctx.unique_atlas_id();
ctx.atlas[idx].new(format, *rid, width, height)!;
return &ctx.atlas[idx];
} else {
// TODO: reallocate the atlas vector
return UgAtlasError.MISSING_ATLAS?;
}
}
// FIXME: WTF
fn Id Ctx.unique_atlas_id(&ctx) {
Id id = atlas_seed.next_long();
bool ok = true;
foreach (a: ctx.atlas) {
if (a.id == id) {
ok = false;
}
}
return ok ? id : ctx.unique_atlas_id();
}
fn void Ctx.discard_atlas(&ctx, Id id)
{
foreach (&a: ctx.atlas) {
if (a.id == id) {
a.free();
*a = Atlas{};
}
}
}
// Try to place a sprite into an atlas of the correct type and return it's id
// new_width and new_height are used in case a new atlas has to be requested, in that
// case they are used as width and height for the new atlas
// FIXME: maybe there has to be a default value
fn Id! Ctx.place_sprite(&ctx, Point* uv, AtlasFormat format, char[] pixels, ushort w, ushort h, ushort new_width, ushort new_height)
{
// try to place the sprite into an existing atlas
foreach (&a: ctx.atlas) {
if (a.id != 0 && a.format == format) {
Point! ouv = a.place(pixels, w, h);
if (catch ouv) {
continue;
}
*uv = ouv;
return a.id;
}
}
// fail... try to request a new atlas and place it there
Id id;
Atlas* atlas = ctx.request_new_atlas(format, &id, new_width, new_height)!;
*uv = atlas.place(pixels, w, h)!;
return id;
}
fn void! Atlas.new(&atlas, AtlasFormat format, Id id, ushort width, ushort height)
{
atlas.format = format;
atlas.id = id;
atlas.width = width;
atlas.height = height;
usz bpp = 1;
switch (format) {
case ATLAS_GRAYSCALE: nextcase;
case ATLAS_ALPHA:
bpp = 1;
default:
return UgAtlasError.INVALID_FORMAT?;
}
usz size = ((usz)atlas.width*atlas.height) * bpp;
atlas.buffer = mem::new_array(char, size);
}
fn void Atlas.free(&atlas)
{
free(atlas.buffer);
}
// place a rect inside the atlas
// uses a row first algorithm
// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/
// FIXME: untested with non-1bpp buffer depths
fn Point! Atlas.place(&atlas, char[] pixels, ushort w, ushort h)
{
Point p;
// TODO: simplify this and use rect_collision() and such
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
atlas.row.x = 0;
atlas.row.y = atlas.row.y + atlas.row_h;
atlas.row_h = 0;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
return UgAtlasError.CANNOT_PLACE?;
}
}
for (usz y = 0; y < h; y++) {
for (usz x = 0; x < w; x++) {
atlas.buffer[(usz)(p.y+y)*atlas.width + (p.x+x)] = pixels[y*w + x];
}
}
atlas.row.x += w;
if (h > atlas.row_h) {
atlas.row_h = h;
}
return p;
}

View File

@ -1,40 +0,0 @@
module ugui;
import std::io;
// draw a button, return the events on that button
fn ElemEvents! Ctx.button(&ctx, String label, Rect size)
{
Id id = label.hash();
Elem *parent = ctx.get_parent()!;
Elem *c_elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// 1. Fill the element fields
// this resets the flags
c_elem.type = ETYPE_BUTTON;
Color bg_color = uint_to_rgba(0x0000ffff);
// if the element is new or the parent was updated then redo layout
if (c_elem.flags.is_new || parent.flags.updated) {
// 2. Layout
c_elem.bounds = ctx.position_element(parent, size, true);
// TODO: 3. Fill the button specific fields
}
c_elem.events = ctx.get_elem_events(c_elem);
if (parent.flags.has_focus) {
if (c_elem.events.mouse_hover) {
c_elem.flags.has_focus = true;
bg_color = uint_to_rgba(0x00ff00ff);
}
}
// Draw the button
ctx.push_rect(c_elem.bounds, bg_color, do_border: true, do_radius: true)!;
return c_elem.events;
}

View File

@ -1,99 +0,0 @@
module ugui;
import std::ascii;
// FIXME: is this really the best solution?
// "rect" is the bounding box of the element, which includes the border and the padding (so not just the content)
fn void! Ctx.push_rect(&ctx, Rect rect, Color color, bool do_border = false, bool do_padding = false, bool do_radius = false)
{
// FIXME: this should be culled higher up, maybe
if (rect.w <= 0 || rect.h <= 0) {
return;
}
Rect border = ctx.style.border;
Rect padding = ctx.style.padding;
ushort radius = ctx.style.radius;
Color border_color = ctx.style.brcolor;
if (do_border) {
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = rect,
.rect.color = border_color,
.rect.radius = do_radius ? radius : 0,
};
ctx.cmd_queue.enqueue(&cmd)!;
}
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = {
.x = rect.x + (do_border ? border.x : 0) + (do_padding ? padding.x : 0),
.y = rect.y + (do_border ? border.y : 0) + (do_padding ? padding.y : 0),
.w = rect.w - (do_border ? border.x+border.w : 0) - (do_padding ? padding.x+padding.w : 0),
.h = rect.h - (do_border ? border.y+border.h : 0) - (do_padding ? padding.y+padding.h : 0),
},
.rect.color = color,
.rect.radius = do_radius ? radius : 0,
};
ctx.cmd_queue.enqueue(&cmd)!;
}
// TODO: add texture id
fn void! Ctx.push_sprite(&ctx, Rect bounds, Rect texture)
{
Cmd cmd = {
.type = CMD_SPRITE,
.sprite.rect = bounds,
.sprite.texture_rect = texture,
};
ctx.cmd_queue.enqueue(&cmd)!;
}
fn void! Ctx.push_string(&ctx, Rect bounds, String text)
{
if (text.len == 0) {
return;
}
short baseline = (short)ctx.font.ascender;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
short line_gap = (short)ctx.font.linegap;
Point orig = {
.x = bounds.x,
.y = bounds.y,
};
short line_len;
Codepoint cp;
usz off, x;
while ((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,
};
if (rect_collision(gb, bounds)) {
ctx.push_sprite(gb, gt)!;
}
line_len += gp.adv;
} else if (cp == '\n'){
orig.y += line_height + line_gap;
line_len = 0;
} else {
continue;
}
}
}

View File

@ -1,243 +0,0 @@
module ugui;
import std::core::string;
import vtree;
import cache;
import fifo;
struct Rect {
short x, y, w, h;
}
struct Point {
short x, y;
}
struct Color{
char r, g, b, a;
}
// element ids are just long ints
def Id = usz;
enum ElemType {
ETYPE_NONE,
ETYPE_DIV,
ETYPE_BUTTON,
ETYPE_SLIDER,
ETYPE_TEXT,
}
bitstruct ElemFlags : uint {
bool updated : 0;
bool has_focus : 1;
bool is_new : 2;
}
bitstruct ElemEvents : uint {
bool key_press : 0;
bool key_release : 1;
bool key_hold : 2;
bool mouse_hover : 3;
bool mouse_press : 4;
bool mouse_release : 5;
bool mouse_hold : 6;
bool update : 7;
}
enum DivLayout {
LAYOUT_ROW,
LAYOUT_COLUMN,
LAYOUT_FLOATING,
}
// div element
struct Div {
DivLayout layout;
struct scroll {
bool can_x;
bool can_y;
bool on_x;
bool on_y;
float value_x;
float value_y;
}
Rect children_bounds;
Point origin_r, origin_c;
Color color_bg;
}
// slider element
struct Slider {
float value;
Rect handle;
}
struct Text {
char* str;
}
// element structure
struct Elem {
Id id;
ElemFlags flags;
ElemEvents events;
Rect bounds;
ElemType type;
union {
Div div;
Slider slider;
Text text;
}
}
// relationships between elements are stored in a tree, it stores just the ids
def IdTree = vtree::VTree(<Id>) @private;
// elements themselves are kept in a cache
const uint MAX_ELEMENTS = 1024;
def ElemCache = cache::Cache(<Id, Elem, MAX_ELEMENTS>) @private;
def CmdQueue = fifo::Fifo(<Cmd>);
fault UgError {
INVALID_SIZE,
EVENT_UNSUPPORTED,
UNEXPECTED_ELEMENT,
}
macro 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)
};
}
const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 };
macro abs(a) { return a < 0 ? -a : a; }
macro clamp(x, min, max) { return x < min ? min : (x > max ? max : x); }
const uint STACK_STEP = 10;
const uint MAX_ELEMS = 128;
const uint MAX_CMDS = 256;
const uint ROOT_ID = 1;
const uint MAX_ATLAS = 2;
// command type
enum CmdType {
CMD_RECT,
CMD_UPDATE_ATLAS,
CMD_SPRITE,
}
// command to draw a rect
// TODO: implement radius
struct CmdRect {
Rect rect;
ushort radius;
Color color;
}
// FIXME: For now only support black and white atlas, so PIXELFORMAT_UNCOMPRESSED_GRAYSCALE
struct CmdUpdateAtlas {
char* raw_buffer;
short width, height;
}
// TODO:
// 1. Add atlases as a data type
// 2. Each atlas has an id
struct CmdSprite {
Rect rect;
Rect texture_rect;
}
// command structure
struct Cmd {
CmdType type;
union {
CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
}
}
enum Layout {
ROW,
COLUMN,
FLOATING
}
// global style, similar to the css box model
struct Style { // css box model
Rect padding;
Rect border;
Rect margin;
Color bgcolor; // background color
Color fgcolor; // foreground color
Color brcolor; // border color
ushort radius;
}
struct Ctx {
Layout layout;
IdTree tree;
ElemCache cache;
CmdQueue cmd_queue;
// total size in pixels of the context
ushort width, height;
Style style;
Atlas[] atlas;
Font font;
bool has_focus;
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;
}
}
isz active_div; // tree node indicating the current active div
}
macro 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 true if rect a contains b
macro rect_contains(Rect a, Rect b)
{
return (a.x <= b.x && a.y <= b.y && a.x+a.w >= b.x+b.w && a.y+a.h >= b.y+b.h);
}
macro rect_intersection(Rect a, Rect b)
{
return Rect{
.x = (short)max(a.x, b.x),
.y = (short)max(a.y, b.y),
.w = (short)min(a.x+a.w, b.x+b.w) - (short)max(a.x, b.x),
.h = (short)min(a.y+a.h, b.y+b.h) - (short)max(a.y, b.y),
};
}
// rect intersection not null
macro rect_collision(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);
}

View File

@ -1,115 +0,0 @@
module ugui;
import std::io;
fn void! Ctx.div_begin(&ctx, String label, Rect size)
{
Id id = label.hash();
Elem *parent = ctx.get_parent()!;
Elem* c_elem = ctx.get_elem(id)!;
isz div_node = ctx.tree.add(id, ctx.active_div)!;
ctx.active_div = div_node;
// 1. Fill the element fields
c_elem.type = ETYPE_DIV;
// do layout and update flags only if the element was updated
if (c_elem.flags.is_new || parent.flags.updated) {
// 2. layout the element
c_elem.bounds = ctx.position_element(parent, size);
if (c_elem.flags.is_new) {
c_elem.div.children_bounds = c_elem.bounds;
c_elem.div.color_bg = ctx.style.bgcolor;
c_elem.div.scroll.can_x = false;
c_elem.div.scroll.can_y = false;
c_elem.div.scroll.value_x = 0;
c_elem.div.scroll.value_y = 0;
}
// 3. Mark the element as updated
c_elem.flags.updated = true;
// 4. Fill the div fields
c_elem.div.origin_c = Point{
.x = c_elem.bounds.x,
.y = c_elem.bounds.y,
};
c_elem.div.origin_r = c_elem.div.origin_c;
c_elem.div.layout = parent.div.layout;
}
// Add the background to the draw stack
ctx.push_rect(c_elem.bounds, c_elem.div.color_bg)!;
// TODO: check active
// TODO: check resizeable
// check and draw scroll bars
if (!rect_contains(c_elem.bounds, c_elem.div.children_bounds)) {
Point cbc = {
.x = c_elem.div.children_bounds.x + c_elem.div.children_bounds.w,
.y = c_elem.div.children_bounds.y + c_elem.div.children_bounds.h,
};
Point bc = {
.x = c_elem.bounds.x + c_elem.bounds.w,
.y = c_elem.bounds.y + c_elem.bounds.h,
};
// vertical overflow, check and draw scroll bar
if (cbc.y > bc.y && c_elem.div.scroll.can_y) {
// set the scrollbar flag, is used in layout
c_elem.div.scroll.on_y = true;
Rect vslider = {
.x = c_elem.bounds.x + c_elem.bounds.w - 10,
.y = c_elem.bounds.y,
.w = 10,
.h = c_elem.bounds.h,
};
float vh = max((float)bc.y / cbc.y, (float)0.15);
Rect vhandle = {
.x = c_elem.bounds.x + c_elem.bounds.w - 10,
.y = (short)(c_elem.bounds.y + (int)(c_elem.bounds.h*(1-vh) * c_elem.div.scroll.value_y)),
.w = 10,
.h = (short)(c_elem.bounds.h * vh),
};
c_elem.events = ctx.get_elem_events(c_elem);
if (parent.flags.has_focus && c_elem.events.mouse_hover && c_elem.events.mouse_hold && point_in_rect(ctx.input.mouse.pos, vhandle)) {
short y = (short)clamp(ctx.input.mouse.pos.y - vhandle.h/2, vslider.y, vslider.y + vslider.h - vhandle.h);
vhandle.y = y;
float v = (float)(vhandle.y-vslider.y) / (float)(vslider.h-vhandle.h);
c_elem.div.scroll.value_y = v;
c_elem.flags.updated = true;
c_elem.div.origin_c = Point{
.x = c_elem.bounds.x,
.y = c_elem.bounds.y,
};
c_elem.div.origin_r = c_elem.div.origin_c;
}
ctx.push_rect(vslider, uint_to_rgba(0x999999ff))!;
ctx.push_rect(vhandle, uint_to_rgba(0x9999ffff))!;
} else {
c_elem.div.scroll.on_y = false;
}
}
// if the bounds are outside of the view then allocate space for scrollbars
DivLayout old_layout = c_elem.div.layout;
c_elem.div.layout = LAYOUT_FLOATING;
c_elem.div.layout = old_layout;
if (parent.flags.has_focus) {
if (point_in_rect(ctx.input.mouse.pos, c_elem.bounds)) {
c_elem.flags.has_focus = true;
}
}
}
fn void! Ctx.div_end(&ctx)
{
// the active_div returns to the parent of the current one
ctx.active_div = ctx.tree.parentof(ctx.active_div)!;
}

View File

@ -1,155 +0,0 @@
module ugui;
import schrift;
import std::collections::map;
import std::core::mem;
import std::io;
// 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 | | |
* | :i. V@V | | h
* | :oM@@M | | |
* | :@@@MM@M | | |
* | @@o o@M | | |
* |<->:@@. M@M | | |
* |ox @@@o@@@@ | | |
* | :M@@V:@@.| | v
* +-------------*---+ -
* |<---- w ---->|
* |<------ adv ---->|
*/
struct Glyph {
Codepoint code;
ushort u, v;
ushort w, h;
short adv, ox, oy;
Id atlas_id;
}
const uint FONT_CACHED = 512;
def GlyphTable = map::HashMap(<Codepoint, Glyph>) @private;
fault UgFontError {
TTF_LOAD_FAILED,
MISSING_GLYPH,
BAD_GLYPH_METRICS,
RENDER_ERROR,
}
struct Font {
schrift::Sft sft;
String path;
GlyphTable table;
float size;
float ascender, descender, linegap; // Line Metrics
}
fn void! Ctx.font_load(&cxt, String path, uint height, float scale = 1)
{
Font* font = ctx.font;
font.table.new_init(capacity: FONT_CACHED);
font.size = height*scale;
font.sft = schrift::Sft{
.xScale = (double)font.size,
.yScale = (double)font.size,
.flags = schrift::SFT_DOWNWARD_Y,
};
font.sft.font = schrift::loadfile(path);
if (font.sft.font == null) {
return UgFontError.TTF_LOAD_FAILED?;
}
schrift::SftLMetrics lmetrics;
schrift::lmetrics(&font.sft, &lmetrics);
font.ascender = (float)lmetrics.ascender;
font.descender = (float)lmetrics.descender;
font.linegap = (float)lmetrics.lineGap;
//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);
}
fn Glyph*! Ctx.get_glyph(&ctx, Codepoint code, bool* is_new = null)
{
Font* font = &ctx.font;
Glyph*! gp;
gp = font.table.get_ref(code);
if (catch excuse = gp) {
if (excuse != SearchResult.MISSING) {
return excuse?;
}
} else {
if (is_new) { *is_new = false; }
return gp;
}
// missing glyph, render and place into an atlas
Glyph glyph;
schrift::SftGlyph gid;
schrift::SftGMetrics gmtx;
if (schrift::lookup(&font.sft, code, &gid) < 0) {
return UgFontError.MISSING_GLYPH?;
}
if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) {
return UgFontError.BAD_GLYPH_METRICS?;
}
schrift::SftImage img = {
.width = gmtx.minWidth,
.height = gmtx.minHeight,
};
char[] pixels = mem::new_array(char, (usz)img.width * img.height);
img.pixels = pixels;
if (schrift::render(&font.sft, gid, img) < 0) {
return UgFontError.RENDER_ERROR?;
}
glyph.code = code;
glyph.w = (ushort)img.width;
glyph.h = (ushort)img.height;
glyph.ox = (short)gmtx.leftSideBearing;
glyph.oy = (short)gmtx.yOffset;
glyph.adv = (short)gmtx.advanceWidth;
//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
// like the letter 'A'
ushort size = (ushort)font.size*256;
Point uv;
Id id = ctx.place_sprite(&uv, ATLAS_ALPHA, pixels, glyph.w, glyph.h, size, size)!;
glyph.atlas_id = id;
glyph.u = uv.x;
glyph.v = uv.y;
mem::free(pixels);
font.table.set(code, glyph);
if (is_new) { *is_new = true; }
return font.table.get_ref(code);
}
// FIXME: maybe some atlases should be exclusive to fonts
fn void Font.free(&font)
{
schrift::freefont(font.sft.font);
}

View File

@ -1,152 +0,0 @@
module ugui;
import std::io;
// return a pointer to the parent of the current active div
fn Elem*! Ctx.get_parent(&ctx)
{
// FIXME: if the tree held pointers to the elements then no more
// redundant cache search
Id parent_id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(parent_id);
}
// get or push an element from the cache, return a pointer to it
// resets all flags except is_new which is set accordingly
macro Ctx.get_elem(&ctx, Id id)
{
Elem empty_elem;
bool is_new;
Elem* c_elem;
c_elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!;
c_elem.flags = (ElemFlags)0;
c_elem.flags.is_new = is_new;
return c_elem;
}
// this searches an element in the cache by label, it does not create a new element
// if it does't find one
macro Ctx.get_elem_by_label(&ctx, String label)
{
Id id = label.hash();
return ctx.cache.search(id);
}
macro Ctx.get_elem_by_tree_idx(&ctx, isz idx) @private
{
Id id = ctx.tree.get(ctx.active_div)!;
return ctx.cache.search(id);
}
fn void! Ctx.init(&ctx)
{
ctx.tree.init(MAX_ELEMENTS)!;
defer catch { (void)ctx.tree.free(); }
ctx.cache.init()!;
defer catch { (void)ctx.cache.free(); }
ctx.cmd_queue.init(MAX_ELEMENTS)!;
defer catch { (void)ctx.cmd_queue.free(); }
ctx.layout = Layout.ROW;
ctx.active_div = 0;
// TODO: add style config
ctx.style.margin = Rect{2, 2, 2, 2};
ctx.style.border = Rect{2, 2, 2, 2};
ctx.style.padding = Rect{1, 1, 1, 1};
ctx.style.radius = 5;
ctx.style.bgcolor = uint_to_rgba(0x282828ff);
ctx.style.fgcolor = uint_to_rgba(0xfbf1c7ff);
ctx.style.brcolor = uint_to_rgba(0xd79921ff);
}
fn void Ctx.free(&ctx)
{
(void)ctx.tree.free();
(void)ctx.cache.free();
(void)ctx.cmd_queue.free();
}
fn void! Ctx.frame_begin(&ctx)
{
// 2. Get the root element from the cache and update it
Elem* c_elem = ctx.get_elem(ROOT_ID)!;
// The root should have the updated flag only if the size of the window
// was changed between frames, this propagates an element size recalculation
// down the element tree
c_elem.flags.updated = ctx.input.events.resize | ctx.input.events.force_update;
ctx.input.events.force_update = false;
// if the window has focus then the root element also has focus, no other
// computation needed, child elements need to check the mouse positon and
// other stuff
c_elem.flags.has_focus = ctx.has_focus;
if (c_elem.flags.is_new || c_elem.flags.updated) {
Elem def_root = {
.id = ROOT_ID,
.type = ETYPE_DIV,
.bounds = {
.w = ctx.width,
.h = ctx.height,
},
.div = {
.layout = LAYOUT_ROW,
.children_bounds = {
.w = ctx.width,
.h = ctx.height,
}
},
.flags = c_elem.flags,
};
*c_elem = def_root;
}
// 3. Push the root element into the element tree
ctx.active_div = ctx.tree.add(ROOT_ID, 0)!;
// The root element does not push anything to the stack
// TODO: add a background color taken from a theme or config
}
fn void! Ctx.force_update(&ctx)
{
ctx.input.events.force_update = true;
}
fn void! Ctx.frame_end(&ctx)
{
// 1. clear the tree
ctx.tree.prune(0)!;
// 2. clear input fields
bool f = ctx.input.events.force_update;
ctx.input.events = (InputEvents)0;
ctx.input.events.force_update = f;
// draw mouse position
$if 1:
Cmd cmd = {
.type = CMD_RECT,
.rect.rect = {
.x = ctx.input.mouse.pos.x - 2,
.y = ctx.input.mouse.pos.y - 2,
.w = 4,
.h = 4,
},
.rect.color = uint_to_rgba(0xff00ffff)
};
ctx.cmd_queue.enqueue(&cmd)!;
$endif
}
<*
* @ensure elem != null
*>
fn bool Ctx.is_hovered(&ctx, Elem *elem)
{
return point_in_rect(ctx.input.mouse.pos, elem.bounds);
}

View File

@ -1,114 +0,0 @@
module ugui;
import std::io;
// TODO: this could be a bitstruct
bitstruct InputEvents : uint {
bool resize : 0; // window size was changed
bool change_focus : 1; // window focus changed
bool mouse_move : 2; // mouse was moved
bool mouse_btn : 3; // mouse button pressed or released
bool force_update : 4;
}
// Window size was changed
fn void! Ctx.input_window_size(&ctx, short width, short height)
{
if (width <= 0 || height <= 0) {
return UgError.INVALID_SIZE?;
}
ctx.width = width;
ctx.height = height;
ctx.input.events.resize = true;
}
// Window gained/lost 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
// update the input flag only if the focus changed
if (ctx.has_focus != has_focus) {
ctx.input.events.change_focus = true;
}
ctx.has_focus = has_focus;
}
bitstruct MouseButtons : uint {
bool btn_left : 0;
bool btn_middle : 1;
bool btn_right : 2;
bool btn_4 : 3;
bool btn_5 : 4;
}
macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down;
macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down;
macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down;
const MouseButtons BTN_NONE = (MouseButtons)0u;
const MouseButtons BTN_ANY = (MouseButtons)(uint.max);
const MouseButtons BTN_LEFT = {.btn_left = true};
const MouseButtons BTN_MIDDLE = {.btn_middle = true};
const MouseButtons BTN_RIGHT = {.btn_right = true};
const MouseButtons BTN_4 = {.btn_4 = true};
const MouseButtons BTN_5 = {.btn_5 = true};
// 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_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 ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
{
// TODO: add the other events
// FIXME: active should be elsewhere
bool active = elem.events.mouse_hold || ctx.is_hovered(elem);
ElemEvents ev = {
.mouse_hover = ctx.is_hovered(elem),
.mouse_press = active & ctx.is_mouse_pressed(BTN_ANY),
.mouse_release = active & ctx.is_mouse_released(BTN_ANY),
.mouse_hold = active & ctx.is_mouse_down(BTN_ANY),
};
return ev;
}
// Mouse Button moved
fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons)
{
ctx.input.mouse.updated = ctx.input.mouse.down ^ buttons;
ctx.input.mouse.down = buttons;
ctx.input.events.mouse_btn = true;
}
// Mouse was moved, report absolute position
fn void Ctx.input_mouse_abs(&ctx, short x, short y)
{
ctx.input.mouse.pos.x = clamp(x, 0u16, ctx.width);
ctx.input.mouse.pos.y = clamp(y, 0u16, ctx.height);
short dx, dy;
dx = x - ctx.input.mouse.pos.x;
dy = y - ctx.input.mouse.pos.y;
ctx.input.mouse.delta.x = dx;
ctx.input.mouse.delta.y = dy;
ctx.input.events.mouse_move = true;
}
// Mouse was moved, report relative motion
fn void Ctx.input_mouse_delta(&ctx, short dx, short dy)
{
ctx.input.mouse.delta.x = dx;
ctx.input.mouse.delta.y = dy;
short mx, my;
mx = ctx.input.mouse.pos.x + dx;
my = ctx.input.mouse.pos.y + dy;
ctx.input.mouse.pos.x = clamp(mx, 0u16, ctx.width);
ctx.input.mouse.pos.y = clamp(my, 0u16, ctx.height);
ctx.input.events.mouse_move = true;
}

View File

@ -1,177 +0,0 @@
module ugui;
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 UgError.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 UgError.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 UgError.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) {
// what?
return UgError.UNEXPECTED_ELEMENT?;
}
parent.div.origin_r = Point{
.x = parent.bounds.x,
.y = parent.div.origin_c.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) {
// what?
return UgError.UNEXPECTED_ELEMENT?;
}
parent.div.origin_c = Point{
.x = parent.div.origin_r.x,
.y = parent.bounds.y,
};
parent.div.origin_r = parent.div.origin_c;
}
// 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, bool style = false)
{
Rect placement;
Point origin;
Div* div = &parent.div;
// 1. Select the right origin
switch (div.layout) {
case LAYOUT_ROW:
origin = div.origin_r;
case LAYOUT_COLUMN:
origin = div.origin_c;
case LAYOUT_FLOATING: // none
default:
// Error
}
// the bottom-right border of the element box
Point pl_corner;
// 2. Calculate the placement
placement.x = (short)max(origin.x + rect.x, 0);
placement.y = (short)max(origin.y + rect.y, 0);
placement.w = rect.w > 0 ? rect.w : (short)max(parent.bounds.w - (placement.x - parent.bounds.x), 0);
placement.h = rect.h > 0 ? rect.h : (short)max(parent.bounds.h - (placement.y - parent.bounds.y), 0);
pl_corner.x = placement.x + placement.w;
pl_corner.y = placement.y + placement.h;
// 2.1 apply style, css box model
if (style) {
Rect margin = ctx.style.margin;
Rect border = ctx.style.border;
Rect padding = ctx.style.padding;
placement.x += margin.x;
placement.y += margin.y;
if (rect.w != 0) { placement.w += border.x+border.w + padding.x+padding.w; }
if (rect.h != 0) { placement.h += border.y+border.h + padding.y+padding.h; }
pl_corner.x = placement.x + placement.w + margin.w;
pl_corner.y = placement.y + placement.h + margin.h;
}
// 3. Update the origins of the parent
div.origin_r = Point{
.x = pl_corner.x,
.y = origin.y,
};
div.origin_c = Point{
.x = origin.x,
.y = pl_corner.y,
};
// 4. Calculate the "scrolled" view
Point off;
Rect* cb = &div.children_bounds;
if (div.scroll.can_x && div.scroll.on_x) {
off.x = (short)(cb.w * div.scroll.value_x);
}
if (div.scroll.can_y && div.scroll.on_y) {
off.y = (short)((float)(cb.h - parent.bounds.h) * div.scroll.value_y);
}
Rect view = {
.x = parent.bounds.x + off.x,
.y = parent.bounds.y + off.y,
.w = parent.bounds.w,
.h = parent.bounds.h,
};
// 5. check if the placement overflows the children ounds, if so update them
if (!point_in_rect(pl_corner, *cb)) {
if (pl_corner.y > cb.y+cb.h) {
cb.h = pl_corner.y - cb.y;
}
if (pl_corner.x > cb.x+cb.w) {
cb.w += pl_corner.x - (cb.x + cb.w);
}
}
// 6. check if the placement is inside the view
if (rect_collision(placement, view)) {
return Rect{
.x = placement.x - off.x,
.y = placement.y - off.y,
.w = placement.w,
.h = placement.h,
};
} else {
return Rect{};
}
}

View File

@ -1,108 +0,0 @@
module ugui;
import std::io;
/* handle
* +----+-----+---------------------+
* | |#####| |
* +----+-----+---------------------+
*/
fn ElemEvents! Ctx.slider_hor(&ctx, String label, Rect size)
{
Id id = label.hash();
Elem *parent = ctx.get_parent()!;
Elem *c_elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// 1. Fill the element fields
c_elem.type = ETYPE_SLIDER;
// if the element is new or the parent was updated then redo layout
if (c_elem.flags.is_new || parent.flags.updated) {
// 2. Layout
c_elem.bounds = ctx.position_element(parent, size, true);
c_elem.slider.handle = Rect{
.x = (short)(c_elem.bounds.x + (int)(c_elem.bounds.w*(1.0-0.25) * c_elem.slider.value)),
.y = c_elem.bounds.y,
.w = (short)(c_elem.bounds.w * 0.25),
.h = c_elem.bounds.h,
};
}
c_elem.events = ctx.get_elem_events(c_elem);
if (parent.flags.has_focus && c_elem.events.mouse_hover) {
if (point_in_rect(ctx.input.mouse.pos, c_elem.slider.handle) && c_elem.events.mouse_hold) {
short x = (short)clamp(ctx.input.mouse.pos.x - c_elem.slider.handle.w/2, c_elem.bounds.x, c_elem.bounds.x + c_elem.bounds.w - c_elem.slider.handle.w);
float v = (float)(c_elem.slider.handle.x-c_elem.bounds.x) / (float)(c_elem.bounds.w-c_elem.slider.handle.w);
c_elem.slider.handle.x = x;
c_elem.slider.value = v;
c_elem.events.update = true;
}
}
// Draw the slider background and handle
Color bg_color = uint_to_rgba(0x0000ffff);
Color handle_color = uint_to_rgba(0x0ff000ff);
ctx.push_rect(c_elem.bounds, bg_color)!;
ctx.push_rect(c_elem.slider.handle, handle_color)!;
return c_elem.events;
}
/*
* +-+
* | |
* | |
* +-+
* |#| handle
* |#|
* +-+
* | |
* | |
* +-+
*/
fn ElemEvents! Ctx.slider_ver(&ctx, String label, Rect size)
{
Id id = label.hash();
Elem *parent = ctx.get_parent()!;
Elem *c_elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// 1. Fill the element fields
c_elem.type = ETYPE_SLIDER;
// if the element is new or the parent was updated then redo layout
if (c_elem.flags.is_new || parent.flags.updated) {
// 2. Layout
c_elem.bounds = ctx.position_element(parent, size, true);
c_elem.slider.handle = Rect{
.x = c_elem.bounds.x,
.y = (short)(c_elem.bounds.y + (int)(c_elem.bounds.h*(1.0-0.25) * c_elem.slider.value)),
.w = c_elem.bounds.w,
.h = (short)(c_elem.bounds.h * 0.25),
};
}
c_elem.events = ctx.get_elem_events(c_elem);
if (parent.flags.has_focus && c_elem.events.mouse_hover) {
if (point_in_rect(ctx.input.mouse.pos, c_elem.slider.handle) && c_elem.events.mouse_hold) {
short y = (short)clamp(ctx.input.mouse.pos.y - c_elem.slider.handle.h/2, c_elem.bounds.y, c_elem.bounds.y + c_elem.bounds.h - c_elem.slider.handle.h);
float v = (float)(c_elem.slider.handle.y-c_elem.bounds.y) / (float)(c_elem.bounds.h-c_elem.slider.handle.h);
c_elem.slider.handle.y = y;
c_elem.slider.value = v;
c_elem.events.update = true;
}
}
// Draw the slider background and handle
Color bg_color = uint_to_rgba(0x0000ffff);
Color handle_color = uint_to_rgba(0x0ff000ff);
ctx.push_rect(c_elem.bounds, bg_color)!;
ctx.push_rect(c_elem.slider.handle, handle_color)!;
return c_elem.events;
}

View File

@ -1,103 +0,0 @@
module ugui;
import std::io;
import std::ascii;
import grapheme;
<*
@require off != null
*>
fn Codepoint str_to_codepoint(char[] str, usz* off)
{
Codepoint cp;
isz b = grapheme::decode_utf8(str, str.len, &cp);
if (b == 0 || b > str.len) {
return 0;
}
*off = b;
return cp;
}
fn Rect! Ctx.get_text_bounds(&ctx, String text, bool* update_atlas)
{
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.get_glyph(cp, &n)!;
line_len += gp.adv;
if (n) { *update_atlas = true; }
} 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 void! Ctx.text_unbounded(&ctx, String label, String text)
{
Id id = label.hash();
Elem *parent = ctx.get_parent()!;
Elem *c_elem = ctx.get_elem(id)!;
// add it to the tree
ctx.tree.add(id, ctx.active_div)!;
// 1. Fill the element fields
// this resets the flags
c_elem.type = ETYPE_TEXT;
short baseline = (short)ctx.font.ascender;
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
short line_gap = (short)ctx.font.linegap;
bool update_atlas;
// if the element is new or the parent was updated then redo layout
if (c_elem.flags.is_new || parent.flags.updated) {
Rect text_size = ctx.get_text_bounds(text, &update_atlas)!;
// 2. Layout
c_elem.bounds = ctx.position_element(parent, text_size, true);
// 3. Fill the button specific fields
c_elem.text.str = text;
}
if (update_atlas) {
// FIXME: atlas here is hardcoded, look at the todo in ugui_data
Cmd up = {
.type = CMD_UPDATE_ATLAS,
.update_atlas = {
.raw_buffer = ctx.font.atlas[0].buffer,
.width = ctx.font.atlas[0].width,
.height = ctx.font.atlas[0].height,
},
};
ctx.cmd_queue.enqueue(&up)!;
}
Cmd bounds = {
.type = CMD_RECT,
.rect.rect = c_elem.bounds,
.rect.color = uint_to_rgba(0x000000ff),
};
ctx.cmd_queue.enqueue(&bounds)!;
ctx.push_string(c_elem.bounds, text)!;
}

View File

@ -1,333 +0,0 @@
module vtree(<ElemType>);
import std::core::mem;
import std::io;
struct VTree {
usz elements;
ElemType[] vector; // vector of element ids
isz[] refs, ordered_refs;
}
fault VTreeError {
CANNOT_SHRINK,
INVALID_REFERENCE,
TREE_FULL,
REFERENCE_NOT_PRESENT,
INVALID_ARGUMENT,
}
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 ElemType{0};
$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] = @zero();
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 VTreeError.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 VTreeError.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 VTreeError.INVALID_REFERENCE?;
}
// no space left
if (tree.elements >= tree.size()) {
return VTreeError.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 VTreeError.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 VTreeError.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 VTreeError.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;
}
// find the size of the subtree starting from ref
fn usz! VTree.subtree_size(&tree, isz ref)
{
if (!tree.ref_is_valid(ref)) {
return VTreeError.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 VTreeError.INVALID_ARGUMENT?;
}
// if the cursor is out of bounds then we are done for sure
if (!tree.ref_is_valid(*cursor)) {
return VTreeError.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 VTreeError.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 VTreeError.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 VTreeError.INVALID_REFERENCE?;
}
if (!tree.ref_is_present(ref)) {
return VTreeError.REFERENCE_NOT_PRESENT?;
}
return tree.refs[ref];
}
fn ElemType! VTree.get(&tree, isz ref)
{
if (!tree.ref_is_valid(ref)) {
return VTreeError.INVALID_REFERENCE?;
}
if (!tree.ref_is_present(ref)) {
return VTreeError.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("}");
}
}

226
src/widgets/button.c3 Normal file
View File

@ -0,0 +1,226 @@
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)!;
}

252
src/widgets/div.c3 Normal file
View File

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

56
src/widgets/separator.c3 Normal file
View File

@ -0,0 +1,56 @@
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)!;
}

185
src/widgets/slider.c3 Normal file
View File

@ -0,0 +1,185 @@
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);

41
src/widgets/sprite.c3 Normal file
View File

@ -0,0 +1,41 @@
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)!;
}

97
src/widgets/text.c3 Normal file
View File

@ -0,0 +1,97 @@
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;
}

View File

View File

@ -1,14 +0,0 @@
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;
}

View File

@ -1,10 +0,0 @@
import std::io;
import std::collections::bitset;
def Bits = bitset::BitSet(<128>);
fn void main()
{
Bits b;
io::printn($typeof(b.data[0]).sizeof);
}

View File

@ -1,14 +0,0 @@
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;
}

View File

@ -1,6 +0,0 @@
import rl;
fn int main(void)
{
return 0;
}

View File

@ -1,26 +0,0 @@
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;
}

View File

@ -1,7 +0,0 @@
import std::io;
import vtree;
fn int main()
{
return 0;
}

View File

@ -1,226 +0,0 @@
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);
}