diff --git a/.ecode/project_build.json b/.ecode/project_build.json
deleted file mode 100644
index 593ac60..0000000
--- a/.ecode/project_build.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "Debug": {
- "build": [
- {
- "args": "-C resources/shaders",
- "command": "make",
- "working_dir": ""
- },
- {
- "args": "build -g",
- "command": "c3c",
- "working_dir": ""
- }
- ],
- "build_types": [],
- "clean": [
- {
- "args": "clean",
- "command": "c3c",
- "working_dir": ""
- }
- ],
- "config": {
- "clear_sys_env": false
- },
- "os": [
- "linux"
- ],
- "output_parser": {
- "config": {
- "relative_file_paths": true
- }
- },
- "run": [
- {
- "args": "",
- "command": "build/ugui",
- "name": "Custom Executable",
- "working_dir": ""
- }
- ]
- }
-}
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 74cac9a..e69de29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,16 +0,0 @@
-[submodule "lib/grapheme.c3l/thirdparty/libgrapheme"]
- path = lib/grapheme.c3l/thirdparty/libgrapheme
- url = git://git.suckless.org/libgrapheme
- ignore = dirty
-[submodule "lib/schrift.c3l/thirdparty/libschrift"]
- path = lib/schrift.c3l/thirdparty/libschrift
- url = https://github.com/tomolt/libschrift
- ignore = dirty
-[submodule "lib/sdl3.c3l"]
- path = lib/sdl3.c3l
- url = https://git.alemauri.eu/alema/sdl3.c3l
- ignore = dirty
-[submodule "lib/vendor"]
- path = lib/vendor
- url = https://github.com/c3lang/vendor
- ignore = dirty
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
deleted file mode 100644
index b533650..0000000
--- a/ARCHITECTURE.md
+++ /dev/null
@@ -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
diff --git a/LAYOUT b/LAYOUT
new file mode 100644
index 0000000..700b43d
--- /dev/null
+++ b/LAYOUT
@@ -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.
diff --git a/LICENSE b/LICENSE
index e69de29..0a04128 100644
--- a/LICENSE
+++ b/LICENSE
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ 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.
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 4f151b9..0000000
--- a/Makefile
+++ /dev/null
@@ -1,3 +0,0 @@
-test_renderer: test_renderer.c3 src/renderer.c3 resources/shaders/source/*
- scripts/compile_shaders.sh
- c3c compile -g -O0 test_renderer.c3 src/renderer.c3 --libdir lib --lib sdl3 --lib ugui
diff --git a/README.md b/README.md
index e69de29..151118a 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1 @@
+Welcome to the ugui library.
diff --git a/TODO b/TODO
index b857d31..f2fa0fa 100644
--- a/TODO
+++ b/TODO
@@ -4,21 +4,22 @@
[x] Implement div.view and scrollbars
[x] Port font system from C to C3 (rewrite1)
[ ] Update ARCHITECTURE.md
-[ ] Write a README.md
-[ ] Use an arena allocator for cache
+[x] Write a README.md
+[x] Use an arena allocator for cache
[ ] Do not redraw if there was no update (no layout and no draw)
[ ] Do command buffer damage tracking based on a context grid (see rxi writeup)
-[x] Better handling of the active and focused widgets, try
-to maintain focus until mouse release (fix scroll bars)
+[x] Better handling of the active and focused widgets, try to maintain focus until mouse release (fix scroll bars)
[x] Clip element bounds to parent div, specifically text
[ ] Resizeable divs
[x] Implement a z index and sort command buffer based on that
[ ] Ctx.set_z_index()
[x] Sort command buffer on insertion
-[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot of boiler plate and reuse it
-[x] The id combination in gen_id() uses an intger division, which is costly, use another combination function that is non-linear and doesn't use division
+[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
-[ ] Maybe cache codepoint converted strings
+[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
@@ -28,29 +29,52 @@ to maintain focus until mouse release (fix scroll bars)
[ ] .png
[ ] .jpg
[ ] gif support?
-[ ] layout_set_max_rows() and layout_set_max_columns()
+[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
-[ ] The way the element structures are implemented wastes a lot of memory since
+[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
-[ ] Border and padding do not go well together if the library issues two rect commands, the visible
- border is effectively the border size plus the padding since there is a gap between the border
- rect and the internal rect. A better solution is to leave it up to the renderer to draw the rect
- correctly
+[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...
+[ ] Selectable divs
+[ ] Selectable text
+[ ] Copy buffer
## Layout
[x] Flexbox
-[ ] Center elements to the row/column
-[ ] Text wrapping / reflow
-[ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
+[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
+[ ] Grids
## Input
@@ -58,7 +82,9 @@ to maintain focus until mouse release (fix scroll bars)
[x] Mouse scroll wheel
[ ] Touch input
[x] Do not set input event to true if the movement was zero (like no mouse movement)
-[ ] Use input event flags, for example to consume the input event
+[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
@@ -66,9 +92,10 @@ to maintain focus until mouse release (fix scroll bars)
- border width
- border radius
[x] add a command to update an atlas
-[ ] New window command, useful for popups
-[ ] Text command returns the text bounds, this way we can avoid the pattern
+[x] New window command, useful for popups
+[x] Text command returns the text bounds, this way we can avoid the pattern
draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ...
+[ ] Rounded rectangle with different radius for each corner
## Atlas
@@ -91,11 +118,6 @@ to maintain focus until mouse release (fix scroll bars)
[x] Checkbox
[ ] Selectable text box
-## Main / exaple
-
-[ ] Create maps from ids to textures and images instead of hardcoding them
-
-
## API
[ ] Introduce a Layout structure that specifies the positioning of elements inside
diff --git a/lib/.gitkeep b/lib/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/grapheme.c3l/Makefile b/lib/grapheme.c3l/Makefile
deleted file mode 100644
index 09193f1..0000000
--- a/lib/grapheme.c3l/Makefile
+++ /dev/null
@@ -1,4 +0,0 @@
-all:
- make -C thirdparty/libgrapheme
- mkdir -p linux-x64
- cp thirdparty/libgrapheme/libgrapheme.a linux-x64/libgrapheme.a
diff --git a/lib/grapheme.c3l/libgrapheme.c3i b/lib/grapheme.c3l/libgrapheme.c3i
deleted file mode 100644
index 6b1c7fc..0000000
--- a/lib/grapheme.c3l/libgrapheme.c3i
+++ /dev/null
@@ -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");
diff --git a/lib/grapheme.c3l/manifest.json b/lib/grapheme.c3l/manifest.json
deleted file mode 100644
index 97f04d0..0000000
--- a/lib/grapheme.c3l/manifest.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "provides": "grapheme",
- "targets": {
- "linux-x64": {
- "dependencies": [],
- "linked-libraries": ["grapheme", "c"]
- }
- }
-}
diff --git a/lib/grapheme.c3l/project.json b/lib/grapheme.c3l/project.json
deleted file mode 100644
index 4b2d212..0000000
--- a/lib/grapheme.c3l/project.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "langrev": "1",
- "warnings": ["no-unused"],
- "dependency-search-paths": [".."],
- "dependencies": ["grapheme"],
- "authors": ["Alessandro Mauri ", "Laslo Hunhold "],
- "version": "0.1.0",
- "sources": [],
- "output": "build",
- "target": "linux-x64",
- "features": [],
- "cpu": "generic",
- "opt": "O0"
-}
diff --git a/lib/grapheme.c3l/thirdparty/libgrapheme b/lib/grapheme.c3l/thirdparty/libgrapheme
deleted file mode 160000
index 65b354f..0000000
--- a/lib/grapheme.c3l/thirdparty/libgrapheme
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 65b354f0fcb1d925f4340dbb4415ea06e8af2bec
diff --git a/lib/schrift.c3l/Makefile b/lib/schrift.c3l/Makefile
deleted file mode 100644
index 1de46d5..0000000
--- a/lib/schrift.c3l/Makefile
+++ /dev/null
@@ -1,4 +0,0 @@
-all:
- make -C thirdparty/libschrift
- mkdir -p linux-x64
- cp thirdparty/libschrift/libschrift.a linux-x64/libschrift.a
diff --git a/lib/schrift.c3l/libschrift.c3 b/lib/schrift.c3l/libschrift.c3
deleted file mode 100644
index 699d296..0000000
--- a/lib/schrift.c3l/libschrift.c3
+++ /dev/null
@@ -1,58 +0,0 @@
-module schrift;
-
-alias SftFont = void*;
-alias SftUChar = uint;
-alias SftGlyph = uint;
-
-const int SFT_DOWNWARD_Y = 0x01;
-
-struct Sft
-{
- SftFont font;
- double xScale;
- double yScale;
- double xOffset;
- double yOffset;
- int flags;
-}
-
-struct SftLMetrics
-{
- double ascender;
- double descender;
- double lineGap;
-}
-
-struct SftGMetrics
-{
- double advanceWidth;
- double leftSideBearing;
- int yOffset;
- int minWidth;
- int minHeight;
-}
-
-struct SftKerning
-{
- double xShift;
- double yShift;
-}
-
-struct SftImage
-{
- void *pixels;
- int width;
- int height;
-}
-
-extern fn ZString sft_version() @extern("sft_version");
-
-extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem");
-extern fn SftFont loadfile(ZString filename) @extern("sft_loadfile");
-extern fn void freefont(SftFont font) @extern("sft_freefont");
-
-extern fn int lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics");
-extern fn int lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup");
-extern fn int gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics");
-extern fn int kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning");
-extern fn int render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render");
diff --git a/lib/schrift.c3l/manifest.json b/lib/schrift.c3l/manifest.json
deleted file mode 100644
index efb7f23..0000000
--- a/lib/schrift.c3l/manifest.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "provides" : "schrift",
- "targets" : {
- "linux-x64" : {
- "dependencies" : [],
- "linked-libraries" : ["schrift", "c"]
- }
- }
-}
diff --git a/lib/schrift.c3l/project.json b/lib/schrift.c3l/project.json
deleted file mode 100644
index 92b049c..0000000
--- a/lib/schrift.c3l/project.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "langrev": "1",
- "warnings": [ "no-unused" ],
- "dependency-search-paths": [ ".." ],
- "dependencies": [ "schrift" ],
- "authors": [ "Alessandro Mauri ", "Thomas Oltmann " ],
- "version": "0.1.0",
- "sources": [ ],
- "output": "build",
- "target": "linux-x64",
- "features": [],
- "cpu": "generic",
- "opt": "O0",
-}
diff --git a/lib/schrift.c3l/thirdparty/libschrift b/lib/schrift.c3l/thirdparty/libschrift
deleted file mode 160000
index 24737d2..0000000
--- a/lib/schrift.c3l/thirdparty/libschrift
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 24737d2922b23df4a5692014f5ba03da0c296112
diff --git a/lib/sdl3.c3l b/lib/sdl3.c3l
deleted file mode 160000
index 076355e..0000000
--- a/lib/sdl3.c3l
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d
diff --git a/lib/ugui.c3l/LICENSE b/lib/ugui.c3l/LICENSE
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/ugui.c3l/README.md b/lib/ugui.c3l/README.md
deleted file mode 100644
index 151118a..0000000
--- a/lib/ugui.c3l/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Welcome to the ugui library.
diff --git a/lib/ugui.c3l/linux-x64/.gitkeep b/lib/ugui.c3l/linux-x64/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/ugui.c3l/scripts/.gitkeep b/lib/ugui.c3l/scripts/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/ugui.c3l/src/fifo.c3 b/lib/ugui.c3l/src/fifo.c3
deleted file mode 100644
index d9fc52e..0000000
--- a/lib/ugui.c3l/src/fifo.c3
+++ /dev/null
@@ -1,91 +0,0 @@
-module fifo::faults;
-faultdef FULL, EMPTY;
-
-module fifo{Type};
-
-import std::core::mem;
-import std::sort;
-
-// TODO: specify the allocator
-
-struct Fifo {
- Type[] arr;
- usz out;
- usz count;
-}
-
-fn void? Fifo.init(&fifo, usz size)
-{
- fifo.arr = mem::new_array(Type, size);
- fifo.out = 0;
- fifo.count = 0;
-}
-
-fn void Fifo.free(&fifo)
-{
- (void)mem::free(fifo.arr);
-}
-
-fn void? Fifo.enqueue(&fifo, Type *elem)
-{
- if (fifo.count >= fifo.arr.len) {
- return fifo::faults::FULL?;
- }
- usz in = (fifo.out + fifo.count) % fifo.arr.len;
- fifo.arr[in] = *elem;
- fifo.count++;
-}
-
-fn Type*? Fifo.dequeue(&fifo)
-{
- if (fifo.count == 0) {
- return fifo::faults::EMPTY?;
- }
- Type *ret = &fifo.arr[fifo.out];
- fifo.count--;
- fifo.out = (fifo.out + 1) % fifo.arr.len;
- return ret;
-}
-
-macro Type Fifo.get(&fifo, usz i) @operator([])
-{
- return fifo.arr[(fifo.out + i) % fifo.arr.len];
-}
-
-fn void Fifo.set(&fifo, usz i, Type val) @operator([]=)
-{
- fifo.arr[(fifo.out + i) % fifo.arr.len] = val;
-}
-
-macro Type* Fifo.get_ref(&fifo, usz i) @operator(&[])
-{
- return &fifo.arr[(fifo.out + i) % fifo.arr.len];
-}
-
-macro usz Fifo.len(&fifo) @operator(len)
-{
- return fifo.count;
-}
-
-fn void? Fifo.sort(&fifo)
-{
- Type[] arr = mem::new_array(Type, fifo.count);
- defer mem::free(arr);
-
- foreach(i, c: fifo) {
- arr[i] = c;
- }
-
- // doesn't keep ordering
- //sort::quicksort(arr);
-
- // seems to keep the right order but we will never know...
- // also since most things are already ordered the time is closer to O(n) than to O(n^2)
- sort::insertionsort(arr);
-
- fifo.count = 0;
- fifo.out = 0;
- foreach (&c: arr) {
- fifo.enqueue(c)!;
- }
-}
\ No newline at end of file
diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3
deleted file mode 100644
index 7635973..0000000
--- a/lib/ugui.c3l/src/ugui_button.c3
+++ /dev/null
@@ -1,185 +0,0 @@
-module ugui;
-
-import std::io;
-
-// button element
-struct ElemButton {
- int filler;
-}
-
-
-// draw a button, return the events on that button
-macro Ctx.button(&ctx, Rect size, bool state = false, ...)
- => ctx.button_id(@compute_id($vasplat), size, state);
-fn ElemEvents? Ctx.button_id(&ctx, Id id, Rect size, bool active)
-{
- id = ctx.gen_id(id)!;
-
- Elem *parent = ctx.get_parent()!;
- Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
- Style* style_norm = ctx.styles.get_style(@str_hash("button"));
-
- elem.bounds = ctx.position_element(parent, size, style_norm);
-
- // if the bounds are null the element is outside the div view,
- // no interaction should occur so just return
- if (elem.bounds.is_null()) { return {}; }
-
- Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
-
- elem.events = ctx.get_elem_events(elem);
- bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
-
- // Draw the button
- ctx.push_rect(elem.bounds, parent.div.z_index, is_active ? style_active : style_norm)!;
-
- return elem.events;
-}
-
-macro Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool active = false, ...)
- => ctx.button_label_id(@compute_id($vasplat), label, size, active);
-fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool active)
-{
- id = ctx.gen_id(id)!;
-
- Elem *parent = ctx.get_parent()!;
- Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
-
- short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
- Rect text_size = ctx.get_text_bounds(label)!;
- Rect btn_size = text_size.add({0,0,10,10});
- Style* style_norm = ctx.styles.get_style(@str_hash("button"));
-
- // 2. Layout
- elem.bounds = ctx.position_element(parent, btn_size, style_norm);
- if (elem.bounds.is_null()) { return {}; }
-
- Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
-
- elem.events = ctx.get_elem_events(elem);
-
- bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
- Style* style = is_active ? style_active : style_norm;
-
- // Draw the button
- text_size.x = elem.bounds.x;
- text_size.y = elem.bounds.y;
- Point off = ctx.center_text(text_size, elem.bounds);
- text_size.x += off.x;
- text_size.y += off.y;
- ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
- ctx.push_string(text_size, label, parent.div.z_index, style.fg)!;
-
- return elem.events;
-}
-
-macro Ctx.button_icon(&ctx, String icon, String on_icon = "", bool active = false, ...)
- => ctx.button_icon_id(@compute_id($vasplat), icon, on_icon, active);
-fn ElemEvents? Ctx.button_icon_id(&ctx, Id id, String icon, String on_icon, bool active)
-{
- id = ctx.gen_id(id)!;
-
- Elem *parent = ctx.get_parent()!;
- Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
-
- Sprite* def_sprite = ctx.sprite_atlas.get(icon)!;
- Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){};
- Rect max_size = def_sprite.rect().max(on_sprite.rect());
-
- Style* style_norm = ctx.styles.get_style(@str_hash("button"));
-
- elem.bounds = ctx.position_element(parent, max_size, style_norm);
-
- // if the bounds are null the element is outside the div view,
- // no interaction should occur so just return
- if (elem.bounds.is_null()) { return {}; }
-
- Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
-
- elem.events = ctx.get_elem_events(elem);
-
- bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
- Style* style = is_active ? style_active : style_norm;
-
- Id tex_id = ctx.sprite_atlas.id;
- if (active && on_icon != "") {
- ctx.push_sprite(elem.bounds, on_sprite.uv(), tex_id, parent.div.z_index, type: on_sprite.type)!;
- } else {
- ctx.push_sprite(elem.bounds, def_sprite.uv(), tex_id, parent.div.z_index, type: def_sprite.type)!;
- }
-
- // Draw the button
- ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
-
- return elem.events;
-}
-
-// FIXME: this should be inside the style
-macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...)
- => ctx.checkbox_id(@compute_id($vasplat), desc, off, active, tick_sprite);
-fn void? Ctx.checkbox_id(&ctx, Id id, String description, Point off, bool* active, String tick_sprite)
-{
- id = ctx.gen_id(id)!;
-
- Elem *parent = ctx.get_parent()!;
- Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
- Style* style = ctx.styles.get_style(@str_hash("checkbox"));
-
- Rect size = {off.x, off.y, style.size, style.size};
- elem.bounds = ctx.position_element(parent, size, style);
-
- // if the bounds are null the element is outside the div view,
- // no interaction should occur so just return
- if (elem.bounds.is_null()) return;
-
- elem.events = ctx.get_elem_events(elem);
- if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
-
- if (tick_sprite != {}) {
- ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
- if (*active) {
- ctx.draw_sprite_raw(tick_sprite, elem.bounds, center: true)!;
- }
- } else {
- ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
- if (*active) {
- ushort x = style.size / 4;
- Rect check = elem.bounds.add({x, x, -x*2, -x*2});
- Style s = *style;
- s.bg = s.primary;
- s.margin = s.border = s.padding = {};
- ctx.push_rect(check, parent.div.z_index, &s)!;
- }
- }
-}
-
-// FIXME: this should be inside the style
-macro Ctx.toggle(&ctx, String desc, Point off, bool* active)
- => ctx.toggle_id(@compute_id($vasplat), desc, off, active);
-fn void? Ctx.toggle_id(&ctx, Id id, String description, Point off, bool* active)
-{
- id = ctx.gen_id(id)!;
-
- Elem *parent = ctx.get_parent()!;
- Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
- Style* style = ctx.styles.get_style(@str_hash("toggle"));
-
- Rect size = {off.x, off.y, style.size*2, style.size};
- elem.bounds = ctx.position_element(parent, size, style);
-
- // if the bounds are null the element is outside the div view,
- // no interaction should occur so just return
- if (elem.bounds.is_null()) return;
-
- elem.events = ctx.get_elem_events(elem);
- if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
-
- // Draw the button
- // FIXME: THIS IS SHIT
- ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
- Rect t = elem.bounds.add({*active ? (style.size+3) : +3, +3, -style.size-6, -6});
- Style s = *style;
- s.bg = s.primary;
- s.margin = s.border = s.padding = {};
- ctx.push_rect(t, parent.div.z_index, &s)!;
-}
diff --git a/lib/ugui.c3l/src/ugui_cmd.c3 b/lib/ugui.c3l/src/ugui_cmd.c3
deleted file mode 100644
index 2919436..0000000
--- a/lib/ugui.c3l/src/ugui_cmd.c3
+++ /dev/null
@@ -1,206 +0,0 @@
-module ugui;
-
-import std::ascii;
-import std::io;
-
-// command type
-enum CmdType {
- CMD_RECT,
- CMD_UPDATE_ATLAS,
- CMD_SPRITE,
- CMD_SCISSOR,
-}
-
-// command to draw a rect
-struct CmdRect {
- Rect rect;
- ushort radius;
- Color color;
-}
-
-struct CmdUpdateAtlas {
- Id id;
- char* raw_buffer;
- short width, height, bpp;
-}
-
-struct CmdSprite {
- Id texture_id;
- SpriteType type;
- Rect rect;
- Rect texture_rect;
- Color hue;
-}
-
-// if rect is zero Rect{0} then reset the scissor
-struct CmdScissor {
- Rect rect;
-}
-
-// command structure
-struct Cmd (Printable) {
- CmdType type;
- int z_index;
- union {
- CmdRect rect;
- CmdUpdateAtlas update_atlas;
- CmdSprite sprite;
- CmdScissor scissor;
- }
-}
-
-fn int Cmd.compare_to(Cmd a, Cmd b)
-{
- if (a.z_index == b.z_index) return 0;
- return a.z_index > b.z_index ? 1 : -1;
-}
-
-// implement the Printable interface
-fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
-{
- return f.printf("Cmd{ type: %s, z_index: %d }", cmd.type, cmd.z_index);
-}
-
-macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max})
-{
- bool no_area = rect.w <= 0 || rect.h <= 0;
- return no_area || !rect.collides(clip);
-}
-
-// FIXME: this whole thing could be done at compile time, maybe
-macro Ctx.push_cmd(&ctx, Cmd *cmd, int z_index)
-{
- cmd.z_index = z_index;
- Rect rect;
- switch (cmd.type) {
- case CMD_RECT: rect = cmd.rect.rect;
- case CMD_SPRITE: rect = cmd.sprite.rect;
- default: return ctx.cmd_queue.enqueue(cmd);
- }
- if (cull_rect(rect, ctx.div_scissor)) return;
- return ctx.cmd_queue.enqueue(cmd);
-}
-
-fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
-{
- Cmd sc = {
- .type = CMD_SCISSOR,
- .scissor.rect = rect.intersection(ctx.div_scissor),
- };
- ctx.push_cmd(&sc, z_index)!;
-}
-
-fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
-{
- Rect border = style.border;
- Rect padding = style.padding;
- ushort radius = style.radius;
- Color bg = style.bg;
- Color border_color = style.secondary;
-
- if (!border.is_null()) {
- Cmd cmd = {
- .type = CMD_RECT,
- .rect.rect = rect,
- .rect.color = border_color,
- .rect.radius = radius,
- };
- ctx.push_cmd(&cmd, z_index)!;
- }
-
- Cmd cmd = {
- .type = CMD_RECT,
- .rect.rect = {
- .x = rect.x + border.x + padding.x,
- .y = rect.y + border.y + padding.y,
- .w = rect.w - (border.x+border.w) - (padding.x+padding.w),
- .h = rect.h - (border.y+border.h) - (padding.y+padding.h),
- },
- .rect.color = bg,
- .rect.radius = radius,
- };
- if (cull_rect(cmd.rect.rect, ctx.div_scissor)) return;
- ctx.push_cmd(&cmd, z_index)!;
-}
-
-fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL)
-{
- Cmd cmd = {
- .type = CMD_SPRITE,
- .sprite.type = type,
- .sprite.rect = bounds,
- .sprite.texture_rect = texture,
- .sprite.texture_id = texture_id,
- .sprite.hue = hue,
- };
- ctx.push_cmd(&cmd, z_index)!;
-}
-
-fn void? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue)
-{
- if (text.len == 0) {
- return;
- }
-
- ctx.push_scissor(bounds, z_index)!;
-
- short baseline = (short)ctx.font.ascender;
- short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
- short line_gap = (short)ctx.font.linegap;
- Id texture_id = ctx.font.id; // or ctx.font.atlas.id
- Point orig = {
- .x = bounds.x,
- .y = bounds.y,
- };
-
- short line_len;
- Codepoint cp;
- usz off, x;
- while (off < text.len && (cp = str_to_codepoint(text[off..], &x)) != 0) {
- off += x;
- Glyph* gp;
- if (!ascii::is_cntrl((char)cp)) {
- gp = ctx.font.get_glyph(cp)!;
- Rect gb = {
- .x = orig.x + line_len + gp.ox,
- .y = orig.y + gp.oy + baseline,
- .w = gp.w,
- .h = gp.h,
- };
- Rect gt = {
- .x = gp.u,
- .y = gp.v,
- .w = gp.w,
- .h = gp.h,
- };
- // push the sprite only if it collides with the bounds
- if (!cull_rect(gb, bounds)) ctx.push_sprite(gb, gt, texture_id, z_index, hue)!;
- line_len += gp.adv;
- } else if (cp == '\n'){
- orig.y += line_height + line_gap;
- line_len = 0;
- } else {
- continue;
- }
- }
-
- // FIXME: we never get here if an error was thrown before
- ctx.push_scissor({}, z_index)!;
-}
-
-fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
-{
- Cmd up = {
- .type = CMD_UPDATE_ATLAS,
- .update_atlas = {
- .id = atlas.id,
- .raw_buffer = atlas.buffer,
- .width = atlas.width,
- .height = atlas.height,
- .bpp = (ushort)atlas.type.bpp(),
- },
- };
- // update the atlases before everything else
- ctx.push_cmd(&up, -1)!;
-}
-
diff --git a/lib/ugui.c3l/src/ugui_core.c3 b/lib/ugui.c3l/src/ugui_core.c3
deleted file mode 100644
index d25385b..0000000
--- a/lib/ugui.c3l/src/ugui_core.c3
+++ /dev/null
@@ -1,330 +0,0 @@
-module ugui;
-
-import vtree;
-import cache;
-import fifo;
-
-import std::io;
-import std::core::string;
-import std::core::mem::allocator;
-
-
-// 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;
-}
-
-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;
- bool text_input : 8;
-}
-
-// element structure
-struct Elem {
- Id id;
- isz tree_idx;
- ElemFlags flags;
- ElemEvents events;
- Rect bounds;
- ElemType type;
- union {
- ElemDiv div;
- ElemButton button;
- ElemSlider slider;
- ElemText text;
- ElemSprite sprite;
- }
-}
-
-
-// relationships between elements are stored in a tree, it stores just the ids
-alias IdTree = vtree::VTree{Id};
-
-// elements themselves are kept in a cache
-const uint MAX_ELEMENTS = 256;
-alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS};
-
-alias CmdQueue = fifo::Fifo{Cmd};
-
-faultdef INVALID_SIZE, EVENT_UNSUPPORTED, UNEXPECTED_ELEMENT, WRONG_ELEMENT_TYPE, WRONG_ID;
-
-const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 };
-
-const uint STACK_STEP = 10;
-const uint MAX_ELEMS = 128;
-const uint MAX_CMDS = 2048;
-const uint ROOT_ID = 1;
-const uint TEXT_MAX = 64;
-
-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 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;
- // scroll wheel
- Point scroll;
- }
- struct keyboard {
- char[TEXT_MAX] text;
- usz text_len;
- ModKeys modkeys;
- }
- }
-
- Id hover_id;
- Id focus_id;
-
- Rect div_scissor; // the current div bounds used for scissor test
- isz 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.get(ctx.active_div)!;
- return ctx.cache.search(parent_id);
-}
-
-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)
-{
- 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)$vaconst[$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 Elem*? Ctx.get_elem(&ctx, Id id, ElemType type)
-{
- Elem empty_elem;
- bool is_new;
- Elem* elem;
- elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!;
- elem.flags = (ElemFlags)0;
- elem.flags.is_new = is_new;
- elem.id = id;
- if (is_new == false && elem.type != type) {
- return WRONG_ELEMENT_TYPE?;
- } else {
- elem.type = type;
- }
- elem.tree_idx = ctx.tree.add(id, ctx.active_div)!;
- return elem;
-}
-
-// 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)
-{
- 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_CMDS)!;
- defer catch { (void)ctx.cmd_queue.free(); }
-
- ctx.styles.init(allocator::heap());
- 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)!;
- 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.div.layout = LAYOUT_ROW;
- elem.div.z_index = 0;
- elem.div.children_bounds = elem.bounds;
- elem.div.scroll_x.enabled = false;
- elem.div.scroll_y.enabled = false;
- elem.div.pcb = {};
- elem.div.origin_c = {};
- elem.div.origin_r = {};
-
- ctx.div_scissor = {0, 0, ctx.width, ctx.height};
-
- // The root element does not push anything to the stack
- // TODO: add a background color taken from a theme or config
-}
-
-const int DEBUG = 1;
-
-fn void? Ctx.frame_end(&ctx)
-{
- // FIXME: this is not guaranteed to be root. the user might forget to close a div or some other element
- Elem* root = ctx.get_active_div()!;
- root.div.layout = LAYOUT_ROW;
-
- // 1. clear the tree
- ctx.tree.nuke();
-
- // 2. clear input fields
- ctx.input.events = (InputEvents)0;
- ctx.input.keyboard.text_len = 0;
-
- // 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;
- }
-
-// debug
-$if DEBUG == 1:
- // 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.enqueue(&cmd)!;
-$endif
-
- // sort the command buffer by the z-index
- ctx.cmd_queue.sort()!;
-}
-
-<*
- * @ensure elem != null
-*>
-macro bool Ctx.is_hovered(&ctx, Elem *elem)
-{
- return ctx.input.mouse.pos.in_rect(elem.bounds);
-}
-
-macro bool Ctx.elem_focus(&ctx, Elem *elem)
-{
- return ctx.focus_id == elem.id;
-}
-
-// TODO: add other events
-// FIXME: this does not work with touch
-// FIXME: hacked together, please do better
-fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
-{
- bool hover = ctx.is_hovered(elem);
- bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT));
-
- if (ctx.is_mouse_pressed(BTN_ANY) && !hover){
- focus = false;
- if (ctx.focus_id == elem.id) ctx.focus_id = 0;
- }
-
- if (hover) { ctx.hover_id = elem.id; }
- if (focus) { ctx.focus_id = elem.id; }
-
- ElemEvents ev = {
- .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),
- .text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT),
- };
- return ev;
-}
diff --git a/lib/ugui.c3l/src/ugui_div.c3 b/lib/ugui.c3l/src/ugui_div.c3
deleted file mode 100644
index 8b9e292..0000000
--- a/lib/ugui.c3l/src/ugui_div.c3
+++ /dev/null
@@ -1,153 +0,0 @@
-module ugui;
-
-import std::io;
-import std::math;
-
-// div element
-struct ElemDiv {
- Layout layout;
- struct scroll_x {
- bool enabled;
- bool on;
- float value;
- }
- struct scroll_y {
- bool enabled;
- bool on;
- float value;
- }
- ushort scroll_size;
- int z_index;
- Rect children_bounds; // current frame children bounds
- Rect pcb; // previous frame children bounds
- Point origin_r, origin_c;
-}
-
-// begin a widget container, or div, the size determines the offset (x,y) width and height.
-// if the width or height are zero the width or height are set to the maximum available.
-// if the width or height are negative the width or height will be calculated based on the children size
-// sort similar to a flexbox, and the minimum size is set by the negative of the width or height
-// FIXME: there is a bug if the size.w or size.h == -0
-macro Ctx.div_begin(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...)
- => ctx.div_begin_id(@compute_id($vasplat), size, scroll_x, scroll_y);
-fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y)
-{
- id = ctx.gen_id(id)!;
-
- Elem* parent = ctx.get_parent()!;
- Elem* elem = ctx.get_elem(id, ETYPE_DIV)!;
- ctx.active_div = elem.tree_idx;
-
- Style* style = ctx.styles.get_style(@str_hash("default"));
- Style* slider_style = ctx.styles.get_style(@str_hash("slider"));
-
- elem.div.scroll_x.enabled = scroll_x;
- elem.div.scroll_y.enabled = scroll_y;
- elem.div.scroll_size = slider_style.size ? slider_style.size : (style.size ? style.size : DEFAULT_STYLE.size);
- elem.div.z_index = parent.div.z_index + 1;
-
- // 2. layout the element
- Rect wanted_size = {
- .x = size.x,
- .y = size.y,
- .w = size.w < 0 ? max(elem.div.pcb.w, (short)-size.w) : size.w,
- .h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h,
- };
- elem.bounds = ctx.position_element(parent, wanted_size, style);
- elem.div.children_bounds = {};
-
- // update the ctx scissor
- ctx.div_scissor = elem.bounds;
- ctx.push_scissor(elem.bounds, elem.div.z_index)!;
-
- // 4. Fill the div fields
- elem.div.origin_c = {
- .x = elem.bounds.x,
- .y = elem.bounds.y
- };
- elem.div.origin_r = elem.div.origin_c;
- elem.div.layout = parent.div.layout;
-
- // Add the background to the draw stack
- bool do_border = parent.div.layout == LAYOUT_FLOATING;
- ctx.push_rect(elem.bounds, elem.div.z_index, style)!;
-
- elem.events = ctx.get_elem_events(elem);
-
- // TODO: check active
- // TODO: check resizeable
-}
-
-fn void? Ctx.div_end(&ctx)
-{
- // swap the children bounds
- Elem* parent = ctx.get_parent()!;
- Elem* elem = ctx.get_active_div()!;
- elem.div.pcb = elem.div.children_bounds;
-
- // FIXME: this causes all elements inside the div to loose focus since the mouse press happens
- // both inside the element and inside the div bounds
- //elem.events = ctx.get_elem_events(elem);
-
- Rect cb = elem.div.pcb;
- // children bounds bottom-right corner
- Point cbc = {
- .x = cb.x + cb.w,
- .y = cb.y + cb.h,
- };
- // div bounds bottom-right corner
- Point bc = {
- .x = elem.bounds.x + elem.bounds.w,
- .y = elem.bounds.y + elem.bounds.h,
- };
-
- // set the scrollbar flag, is used in layout
- // horizontal overflow
- elem.div.scroll_x.on = cbc.x > bc.x && elem.div.scroll_x.enabled;
- // vertical overflow
- elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled;
-
- Id hsid_raw = @str_hash("div_scrollbar_horizontal");
- Id vsid_raw = @str_hash("div_scrollbar_vertical");
- Id hsid_real = ctx.gen_id(@str_hash("div_scrollbar_horizontal"))!;
- Id vsid_real = ctx.gen_id(@str_hash("div_scrollbar_vertical"))!;
- short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid_real || ctx.is_hovered(ctx.find_elem(vsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
- short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid_real || ctx.is_hovered(ctx.find_elem(hsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
-
- if (elem.div.scroll_y.on) {
- if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
- elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f;
- elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f);
- }
- Rect vslider = {
- .x = elem.bounds.x + elem.bounds.w - wdim,
- .y = elem.bounds.y,
- .w = wdim,
- .h = elem.bounds.h - hdim,
- };
- Layout prev_l = elem.div.layout;
- elem.div.layout = LAYOUT_ABSOLUTE;
- ctx.slider_ver_id(vsid_raw, vslider, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
- elem.div.layout = prev_l;
- }
-
- if (elem.div.scroll_x.on) {
- if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
- elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f;
- elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
- }
- Rect hslider = {
- .x = elem.bounds.x,
- .y = elem.bounds.y + elem.bounds.h - hdim,
- .w = elem.bounds.w - wdim,
- .h = hdim,
- };
- Layout prev_l = elem.div.layout;
- elem.div.layout = LAYOUT_ABSOLUTE;
- ctx.slider_hor_id(hsid_raw, hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!;
- elem.div.layout = prev_l;
- }
-
- // the active_div returns to the parent of the current one
- ctx.active_div = ctx.tree.parentof(ctx.active_div)!;
-}
diff --git a/lib/ugui.c3l/src/ugui_input.c3 b/lib/ugui.c3l/src/ugui_input.c3
deleted file mode 100644
index ac70110..0000000
--- a/lib/ugui.c3l/src/ugui_input.c3
+++ /dev/null
@@ -1,204 +0,0 @@
-module ugui;
-
-import grapheme;
-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;
-}
-
-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;
- // arrow keys
- bool up : 14;
- bool down : 15;
- bool left : 16;
- bool right : 17;
-}
-
-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);
-
-fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String keys)
-{
- bool is_mod = (bool)(ctx.input.keyboard.modkeys & mod);
- bool is_keys = true;
- String haystack = (String)ctx.input.keyboard.text[0..ctx.input.keyboard.text_len];
- char[2] needle;
- foreach (c: keys) {
- needle[0] = c;
- is_keys = is_keys && haystack.contains((String)needle[..]);
- }
- return is_mod && is_keys;
-}
-
-// Window size was changed
-fn void? Ctx.input_window_size(&ctx, short width, short height)
-{
- if (width <= 0 || height <= 0) {
- return INVALID_SIZE?;
- }
- ctx.input.events.resize = ctx.width != width || ctx.height != height;
- ctx.width = width;
- ctx.height = height;
-}
-
-// 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
- ctx.input.events.change_focus = ctx.has_focus != has_focus;
- ctx.has_focus = has_focus;
-}
-
-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;
-
-// 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;
-
-// Mouse Buttons down
-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 = (uint)ctx.input.mouse.down != 0 || (uint)ctx.input.mouse.updated != 0;
-}
-
-// Mouse was moved, report absolute position
-fn void Ctx.input_mouse_abs(&ctx, short x, short y)
-{
- ctx.input.mouse.pos.x = math::clamp(x, (short)0, ctx.width);
- ctx.input.mouse.pos.y = math::clamp(y, (short)0, 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 = dx != 0 || dy != 0;
-}
-
-// 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 = math::clamp(mx, (short)0, ctx.width);
- ctx.input.mouse.pos.y = math::clamp(my, (short)0, ctx.height);
-
- ctx.input.events.mouse_move = dx != 0 || dy != 0;
-}
-
-fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0)
-{
- ctx.input.mouse.scroll.x = (short)((float)-x*scale);
- ctx.input.mouse.scroll.y = (short)((float)-y*scale);
- ctx.input.events.mouse_scroll = x !=0 || y != 0;
-}
-
-// append utf-8 encoded text to the context text input
-fn void Ctx.input_text_utf8(&ctx, char[] text)
-{
- if (text.len == 0) { return; }
-
- usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len;
- usz len = text.len > remaining ? remaining : text.len;
- char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..];
- s[..len-1] = text[..len-1];
- ctx.input.keyboard.text_len += len;
- ctx.input.events.text_input = true;
-}
-
-fn void Ctx.input_text_unicode(&ctx, char[] text)
-{
- if (text.ptr == null || text.len == 0) { return; }
-
- char[32] tmp;
- usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len;
- char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..];
-
- usz off;
- foreach (idx, cp: text) {
- if (off >= remaining) { break; }
- usz enc = grapheme::encode_utf8(cp, tmp[..], tmp.len);
- s[off..off+enc] = tmp[..enc];
- off += enc;
- }
- ctx.input.keyboard.text_len += off;
-
- ctx.input.events.text_input = true;
-}
-
-fn void Ctx.input_char(&ctx, char c)
-{
- char[1] b = {c};
- ctx.input_text_utf8(b[..]);
-}
-
-// Modifier keys, like control or backspace
-// TODO: make this call repetible to input modkeys one by one
-fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys)
-{
- ctx.input.keyboard.modkeys = modkeys;
- ctx.input.events.mod_key = (uint)ctx.input.keyboard.modkeys != 0;
-}
diff --git a/lib/ugui.c3l/src/ugui_layout.c3 b/lib/ugui.c3l/src/ugui_layout.c3
deleted file mode 100644
index ab7db01..0000000
--- a/lib/ugui.c3l/src/ugui_layout.c3
+++ /dev/null
@@ -1,196 +0,0 @@
-module ugui;
-
-enum Layout {
- LAYOUT_ROW,
- LAYOUT_COLUMN,
- LAYOUT_FLOATING,
- LAYOUT_ABSOLUTE,
-}
-
-fn void? Ctx.layout_set_row(&ctx)
-{
- Id parent_id = ctx.tree.get(ctx.active_div)!;
- Elem *parent = ctx.cache.search(parent_id)!;
-
- if (parent.type != ETYPE_DIV) {
- // what?
- return UNEXPECTED_ELEMENT?;
- }
-
- parent.div.layout = LAYOUT_ROW;
-}
-
-fn void? Ctx.layout_set_column(&ctx)
-{
- Id parent_id = ctx.tree.get(ctx.active_div)!;
- Elem *parent = ctx.cache.search(parent_id)!;
-
- if (parent.type != ETYPE_DIV) {
- // what?
- return UNEXPECTED_ELEMENT?;
- }
-
- parent.div.layout = LAYOUT_COLUMN;
-}
-
-fn void? Ctx.layout_set_floating(&ctx)
-{
- Id parent_id = ctx.tree.get(ctx.active_div)!;
- Elem *parent = ctx.cache.search(parent_id)!;
-
- if (parent.type != ETYPE_DIV) {
- // what?
- return UNEXPECTED_ELEMENT?;
- }
-
- parent.div.layout = LAYOUT_FLOATING;
-}
-
-fn void? Ctx.layout_next_row(&ctx)
-{
- Id parent_id = ctx.tree.get(ctx.active_div)!;
- Elem *parent = ctx.cache.search(parent_id)!;
-
- if (parent.type != ETYPE_DIV) {
- return UNEXPECTED_ELEMENT?;
- }
-
- parent.div.origin_r = {
- .x = parent.bounds.x,
- .y = parent.div.children_bounds.bottom_right().y,
- };
- parent.div.origin_c = parent.div.origin_r;
-}
-
-fn void? Ctx.layout_next_column(&ctx)
-{
- Id parent_id = ctx.tree.get(ctx.active_div)!;
- Elem *parent = ctx.cache.search(parent_id)!;
-
- if (parent.type != ETYPE_DIV) {
- return UNEXPECTED_ELEMENT?;
- }
-
- parent.div.origin_c = {
- .x = parent.div.children_bounds.bottom_right().x,
- .y = parent.bounds.y,
- };
- parent.div.origin_r = parent.div.origin_c;
-}
-
-
-macro Rect Elem.get_view(&elem)
-{
- Rect off;
- if (elem.div.scroll_x.enabled && elem.div.scroll_x.on) {
- off.x = (short)((float)(elem.div.pcb.w - elem.bounds.w) * elem.div.scroll_x.value);
- off.w = -elem.div.scroll_size;
- }
- if (elem.div.scroll_y.enabled && elem.div.scroll_y.on) {
- off.y = (short)((float)(elem.div.pcb.h - elem.bounds.h) * elem.div.scroll_y.value);
- off.h = -elem.div.scroll_size;
- }
- return elem.bounds.add(off);
-}
-
-macro Point Elem.get_view_off(&elem)
-{
- return elem.get_view().sub(elem.bounds).position();
-}
-
-// position the rectangle inside the parent according to the layout
-// parent: parent div
-// rect: the requested size
-// style: apply style
-<*
-@require ctx != null
-@require parent.type == ETYPE_DIV
-*>
-fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, Style* style)
-{
- ElemDiv* div = &parent.div;
-
- Rect parent_bounds, parent_view;
- Rect child_placement, child_occupied;
-
- // 1. Select the right origin
- Point origin;
- switch (div.layout) {
- case LAYOUT_ROW:
- origin = div.origin_r;
- case LAYOUT_COLUMN:
- origin = div.origin_c;
- case LAYOUT_FLOATING: // none, relative to zero zero
- case LAYOUT_ABSOLUTE: // absolute position, this is a no-op, return the rect
- return rect;
- default: // error
- return {};
- }
-
- // 2. Compute the parent's view
- parent_bounds = parent.bounds;
- parent_view = parent.get_view();
-
- // 3. Compute the placement and occupied area
-
- // grow rect (wanted size) when widht or height are less than zero
- bool adapt_x = rect.w <= 0;
- bool adapt_y = rect.h <= 0;
- if (adapt_x) rect.w = parent_bounds.w - parent_bounds.x - origin.x;
- if (adapt_y) rect.h = parent_bounds.h - parent_bounds.y - origin.y;
-
- // offset placement and area
- child_placement = child_placement.off(origin.add(rect.position()));
- child_occupied = child_occupied.off(origin.add(rect.position()));
-
- Rect margin = style.margin;
- Rect border = style.border;
- Rect padding = style.padding;
-
- // padding, grows both the placement and occupied area
- child_placement = child_placement.grow(padding.position().add(padding.size()));
- child_occupied = child_occupied.grow(padding.position().add(padding.size()));
- // border, grows both the placement and occupied area
- child_placement = child_placement.grow(border.position().add(border.size()));
- child_occupied = child_occupied.grow(border.position().add(border.size()));
- // margin, offsets the placement and grows the occupied area
- child_placement = child_placement.off(margin.position());
- child_occupied = child_occupied.grow(margin.position().add(margin.size()));
-
- // oh yeah also adjust the rect if i was to grow
- if (adapt_x) rect.w -= padding.x+padding.w + border.x+border.w + margin.x+margin.w;
- if (adapt_y) rect.h -= padding.y+padding.h + border.y+border.h + margin.y+margin.h;
-
- // set the size
- child_placement = child_placement.grow(rect.size());
- child_occupied = child_occupied.grow(rect.size());
-
- // 4. Update the parent's origin
- div.origin_r = {
- .x = child_occupied.bottom_right().x,
- .y = origin.y,
- };
- div.origin_c = {
- .x = origin.x,
- .y = child_occupied.bottom_right().y,
- };
-
- // 5. Update the parent's children bounds
- if (!child_occupied.bottom_right().in_rect(div.children_bounds)) {
- // right overflow
- if (child_occupied.bottom_right().x > div.children_bounds.bottom_right().x) {
- div.children_bounds.w += child_occupied.bottom_right().x - div.children_bounds.bottom_right().x;
- }
- // bottom overflow
- if (child_occupied.bottom_right().y > div.children_bounds.bottom_right().y) {
- div.children_bounds.h += child_occupied.bottom_right().y - div.children_bounds.bottom_right().y;
- }
- }
-
- // 99. return the placement
- if (child_placement.collides(parent_view)) {
- return child_placement.off(parent.get_view_off().neg());
- } else {
- return {};
- }
-}
diff --git a/lib/ugui.c3l/src/ugui_slider.c3 b/lib/ugui.c3l/src/ugui_slider.c3
deleted file mode 100644
index a631dee..0000000
--- a/lib/ugui.c3l/src/ugui_slider.c3
+++ /dev/null
@@ -1,132 +0,0 @@
-module ugui;
-
-import std::io;
-import std::math;
-
-// slider element
-struct ElemSlider {
- Rect handle;
-}
-
-
-/* handle
- * +----+-----+---------------------+
- * | |#####| |
- * +----+-----+---------------------+
- */
-macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
- => ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent);
-<*
-@require value != null
-*>
-fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
-{
- id = ctx.gen_id(id)!;
-
- Elem* parent = ctx.get_parent()!;
- Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!;
- Style* style = ctx.styles.get_style(@str_hash("slider"));
-
- // 2. Layout
- elem.bounds = ctx.position_element(parent, size, style);
-
- // handle width
- short hw = (short)(elem.bounds.w * hpercent);
- Rect handle = {
- .x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value),
- .y = elem.bounds.y,
- .w = hw,
- .h = elem.bounds.h,
- };
- elem.slider.handle = handle;
-
- Point m = ctx.input.mouse.pos;
- elem.events = ctx.get_elem_events(elem);
-
- if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
- *value = calc_value(elem.bounds.x, m.x, elem.bounds.w, hw);
- elem.slider.handle.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value);
- elem.events.update = true;
- }
-
- // Draw the slider background and handle
- Style s = *style;
- Rect padding = s.padding;
- s.padding = {};
- ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
- s.bg = s.primary;
- s.padding = padding;
- s.border = {};
- ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
-
- return elem.events;
-}
-
-
-/*
- * +--+
- * | |
- * | |
- * +--+
- * |##| handle
- * |##|
- * +--+
- * | |
- * | |
- * +--+
- */
-macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
- => ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent);
-fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
-{
- id = ctx.gen_id(id)!;
-
- Elem *parent = ctx.get_parent()!;
- Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!;
- Style* style = ctx.styles.get_style(@str_hash("slider"));
-
- // 1. Fill the element fields
- if (elem.flags.is_new) {
- elem.type = ETYPE_SLIDER;
- } else if (elem.type != ETYPE_SLIDER) {
- return WRONG_ELEMENT_TYPE?;
- }
-
- // 2. Layout
- elem.bounds = ctx.position_element(parent, size, style);
-
- // handle height
- short hh = (short)(elem.bounds.h * hpercent);
- Rect handle = {
- .x = elem.bounds.x,
- .y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value),
- .w = elem.bounds.w,
- .h = hh,
- };
- elem.slider.handle = handle;
-
- Point m = ctx.input.mouse.pos;
- elem.events = ctx.get_elem_events(elem);
-
- if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
- *value = calc_value(elem.bounds.y, m.y, elem.bounds.h, hh);
- elem.slider.handle.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value);
- elem.events.update = true;
- }
-
- // Draw the slider background and handle
- Style s = *style;
- Rect padding = s.padding;
- s.padding = {};
- ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
- s.bg = s.primary;
- s.padding = padding;
- s.border = {};
- ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
-
- return elem.events;
-}
-
-macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value);
-macro float calc_value(short off, short mouse, short dim, short slider)
- => math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f);
diff --git a/lib/ugui.c3l/src/ugui_text.c3 b/lib/ugui.c3l/src/ugui_text.c3
deleted file mode 100644
index 7d2d422..0000000
--- a/lib/ugui.c3l/src/ugui_text.c3
+++ /dev/null
@@ -1,90 +0,0 @@
-module ugui;
-
-import std::io;
-
-struct ElemText {
- char[] str;
- usz cursor; // cursor offset
-}
-
-macro Ctx.text_unbounded(&ctx, String text, ...)
- => ctx.text_unbounded_id(@compute_id($vasplat), text);
-fn void? Ctx.text_unbounded_id(&ctx, Id id, String text)
-{
- id = ctx.gen_id(id)!;
-
- Elem *parent = ctx.get_parent()!;
- Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
- Style* style = ctx.styles.get_style(@str_hash("text"));
-
- elem.text.str = text;
-
- // if the element is new or the parent was updated then redo layout
- Rect text_size = ctx.get_text_bounds(text)!;
- // 2. Layout
- elem.bounds = ctx.position_element(parent, text_size, style);
- if (elem.bounds.is_null()) { return; }
-
- ctx.push_string(elem.bounds, text, parent.div.z_index, style.fg)!;
-}
-
-macro Ctx.text_box(&ctx, Rect size, char[] text, usz* text_len, ...)
- => ctx.text_box_id(@compute_id($vasplat), size, text, text_len);
-fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_len)
-{
- id = ctx.gen_id(id)!;
-
- Elem *parent = ctx.get_parent()!;
- Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
- Style* style = ctx.styles.get_style(@str_hash("text-box"));
-
- elem.text.str = text;
-
- // layout the text box
- elem.bounds = ctx.position_element(parent, size, style);
-
- // check input and update the text
- elem.events = ctx.get_elem_events(elem);
-
- if (elem.events.text_input) {
- usz l = ctx.input.keyboard.text_len;
- char[] t = ctx.input.keyboard.text[..l];
-
- if (l != 0 && l < text.len - *text_len) {
- text[*text_len..*text_len+l] = t[..];
- *text_len += l;
- }
-
- if (ctx.input.keyboard.modkeys.bkspc) {
- *text_len = *text_len > 0 ? *text_len-1 : 0;
- }
-
- }
- elem.text.cursor = *text_len;
-
- // draw the box
- short line_height = (short)ctx.font.line_height();
- Rect text_box = elem.bounds.sub({0,0,0,line_height});
- Rect input_box = {
- .x = elem.bounds.x,
- .y = elem.bounds.y + elem.bounds.h - line_height,
- .w = elem.bounds.w,
- .h = line_height,
- };
- Rect cursor;
- Point b = ctx.get_cursor_position((String)text[:elem.text.cursor])!;
- cursor = {
- .x = b.x,
- .y = b.y,
- .w = 3,
- .h = line_height,
- };
- cursor = cursor.off(elem.bounds.position());
-
- ctx.push_rect(text_box, parent.div.z_index, style)!;
- ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg)!;
- ctx.push_rect(input_box, parent.div.z_index, style)!;
- ctx.push_rect(cursor, parent.div.z_index, style)!;
-
- return elem.events;
-}
diff --git a/lib/ugui.c3l/src/vtree.c3 b/lib/ugui.c3l/src/vtree.c3
deleted file mode 100644
index d18b749..0000000
--- a/lib/ugui.c3l/src/vtree.c3
+++ /dev/null
@@ -1,338 +0,0 @@
-module vtree::faults;
-faultdef CANNOT_SHRINK, INVALID_REFERENCE, TREE_FULL, REFERENCE_NOT_PRESENT, INVALID_ARGUMENT;
-
-module vtree{ElemType};
-
-import std::core::mem;
-import std::io;
-
-struct VTree {
- usz elements;
- ElemType[] vector; // vector of element ids
- isz[] refs, ordered_refs;
-}
-
-
-macro VTree.ref_is_valid(&tree, isz ref) { return (ref >= 0 && ref < tree.refs.len); }
-macro VTree.ref_is_present(&tree, isz ref) { return tree.refs[ref] >= 0; }
-macro VTree.size(&tree) { return tree.refs.len; }
-
-// macro to zero an elemen
-macro @zero()
-{
- $if $assignable(0, ElemType):
- return 0;
- $else
- return {};
- $endif
-}
-
-fn void? VTree.init(&tree, usz size)
-{
- tree.vector = mem::new_array(ElemType, size);
- defer catch { (void)mem::free(tree.vector); }
-
- tree.refs = mem::new_array(isz, size);
- defer catch { (void)mem::free(tree.refs); }
-
- tree.ordered_refs = mem::new_array(isz, size);
- defer catch { (void)mem::free(tree.ordered_refs); }
-
- // set all refs to -1, meaning invalid (free) element
- tree.refs[..] = -1;
-
- tree.elements = 0;
-}
-
-fn void VTree.free(&tree)
-{
- (void)mem::free(tree.vector);
- (void)mem::free(tree.refs);
- (void)mem::free(tree.ordered_refs);
-}
-
-fn void VTree.pack(&tree)
-{
- // TODO: add a PACKED flag to skip this
-
- isz free_spot = -1;
- for (usz i = 0; i < tree.size(); i++) {
- if (tree.refs[i] == -1) {
- free_spot = i;
- continue;
- }
-
- // find a item that can be packed
- if (free_spot >= 0 && tree.refs[i] >= 0) {
- isz old_ref = i;
-
- // move the item
- tree.vector[free_spot] = tree.vector[i];
- tree.refs[free_spot] = tree.refs[i];
-
- tree.vector[i] = {};
- tree.refs[i] = -1;
-
- // and move all references
- for (usz j = 0; j < tree.size(); j++) {
- if (tree.refs[j] == old_ref) {
- tree.refs[j] = free_spot;
- }
- }
-
- // mark the free spot as used
- free_spot = -1;
- }
- }
-}
-
-fn void? VTree.resize(&tree, usz newsize)
-{
- // return error when shrinking with too many elements
- if (newsize < tree.elements) {
- return vtree::faults::CANNOT_SHRINK?;
- }
-
- // pack the vector when shrinking to avoid data loss
- if ((int)newsize < tree.size()) {
- // FIXME: packing destroys all references to elements of vec
- // so shrinking may cause dangling pointers
- return vtree::faults::CANNOT_SHRINK?;
- }
-
- usz old_size = tree.size();
-
- tree.vector = ((ElemType*)mem::realloc(tree.vector, newsize*ElemType.sizeof))[:newsize];
- defer catch { (void)mem::free(tree.vector); }
-
- tree.refs = ((isz*)mem::realloc(tree.refs, newsize*isz.sizeof))[:newsize];
- defer catch { (void)mem::free(tree.refs); }
-
- tree.ordered_refs = ((isz*)mem::realloc(tree.ordered_refs, newsize*isz.sizeof))[:newsize];
- defer catch { (void)mem::free(tree.ordered_refs); }
-
- if (newsize > tree.size()) {
- tree.vector[old_size..newsize-1] = @zero();
- tree.refs[old_size..newsize-1] = -1;
- }
-}
-
-// add an element to the tree, return it's ref
-fn isz? VTree.add(&tree, ElemType elem, isz parent)
-{
- // invalid parent
- if (!tree.ref_is_valid(parent)) {
- return vtree::faults::INVALID_REFERENCE?;
- }
-
- // no space left
- if (tree.elements >= tree.size()) {
- return vtree::faults::TREE_FULL?;
- }
-
- // check if the parent exists
- // if there are no elements in the tree the first add will set the root
- if (!tree.ref_is_present(parent) && tree.elements != 0) {
- return vtree::faults::REFERENCE_NOT_PRESENT?;
- }
-
- // get the first free spot
- isz free_spot = -1;
- for (usz i = 0; i < tree.size(); i++) {
- if (tree.refs[i] == -1) {
- free_spot = i;
- break;
- }
- }
- if (free_spot < 0) {
- return vtree::faults::TREE_FULL?;
- }
-
- // finally add the element
- tree.vector[free_spot] = elem;
- tree.refs[free_spot] = parent;
- tree.elements++;
-
- return free_spot;
-}
-
-// prune the tree starting from the ref
-// returns the number of pruned elements
-fn usz? VTree.prune(&tree, isz ref)
-{
- if (!tree.ref_is_valid(ref)) {
- return vtree::faults::INVALID_REFERENCE?;
- }
-
- if (!tree.ref_is_present(ref)) {
- return 0;
- }
-
- tree.vector[ref] = @zero();
- tree.refs[ref] = -1;
- tree.elements--;
-
- usz count = 1;
- for (usz i = 0; tree.elements > 0 && i < tree.size(); i++) {
- if (tree.refs[i] == ref) {
- count += tree.prune(i)!;
- }
- }
-
- return count;
-}
-
-fn usz VTree.nuke(&tree)
-{
- tree.vector[0..] = @zero();
- tree.refs[0..] = -1;
- usz x = tree.elements;
- tree.elements = 0;
- return x;
-}
-
-// find the size of the subtree starting from ref
-fn usz? VTree.subtree_size(&tree, isz ref)
-{
- if (!tree.ref_is_valid(ref)) {
- return vtree::faults::INVALID_REFERENCE?;
- }
-
- if (!tree.ref_is_present(ref)) {
- return 0;
- }
-
- usz count = 1;
- for (usz i = 0; i < tree.size(); i++) {
- // only root has the reference to itself
- if (tree.refs[i] == ref && ref != i) {
- count += tree.subtree_size(i)!;
- }
- }
-
- return count;
-}
-
-// iterate through the first level children, use a cursor like strtok_r
-fn isz? VTree.children_it(&tree, isz parent, isz *cursor)
-{
- if (cursor == null) {
- return vtree::faults::INVALID_ARGUMENT?;
- }
-
- // if the cursor is out of bounds then we are done for sure
- if (!tree.ref_is_valid(*cursor)) {
- return vtree::faults::INVALID_REFERENCE?;
- }
-
- // same for the parent, if it's invalid it can't have children
- if (!tree.ref_is_valid(parent) || !tree.ref_is_present(parent)) {
- return vtree::faults::INVALID_REFERENCE?;
- }
-
- // find the first child, update the cursor and return the ref
- for (isz i = *cursor; i < tree.size(); i++) {
- if (tree.refs[i] == parent) {
- *cursor = i + 1;
- return i;
- }
- }
-
- // if no children are found return -1
- *cursor = -1;
- return -1;
-}
-
-/* iterates trough every leaf of the subtree in the following manner
- * node [x], x: visit order
- * [0]
- * / | \
- * / [2] [3]
- * [1] |
- * / \ [6]
- * [4] [5]
- */
-fn isz? VTree.level_order_it(&tree, isz ref, isz *cursor)
-{
- if (cursor == null) {
- return vtree::faults::INVALID_ARGUMENT?;
- }
-
- isz[] queue = tree.ordered_refs;
-
- // TODO: this could also be done when adding or removing elements
- // first call, create a ref array ordered like we desire
- if (*cursor == -1) {
- *cursor = 0;
- queue[..] = -1;
-
- // iterate through the queue appending found children
- isz pos, off;
- do {
- // printf ("ref=%d\n", ref);
- for (isz i = 0; i < tree.size(); i++) {
- if (tree.refs[i] == ref) {
- queue[pos++] = i;
- }
- }
-
- for (; ref == queue[off] && off < tree.size(); off++);
- ref = queue[off];
-
- } while (tree.ref_is_valid(ref));
- // This line is why tree.ordered_refs has to be size+1
- queue[off + 1] = -1;
- }
-
- // PRINT_ARR(queue, tree.size());
- // return -1;
-
- // on successive calls just iterate through the queue until we find an
- // invalid ref, if the user set the cursor to -1 it means it has found what
- // he needed, so free
- if (*cursor < 0) {
- return -1;
- } else if (tree.ref_is_valid(*cursor)) {
- return queue[(*cursor)++];
- }
-
- return -1;
-}
-
-fn isz? VTree.parentof(&tree, isz ref)
-{
- if (!tree.ref_is_valid(ref)) {
- return vtree::faults::INVALID_REFERENCE?;
- }
-
- if (!tree.ref_is_present(ref)) {
- return vtree::faults::REFERENCE_NOT_PRESENT?;
- }
-
- return tree.refs[ref];
-}
-
-fn ElemType? VTree.get(&tree, isz ref)
-{
- if (!tree.ref_is_valid(ref)) {
- return vtree::faults::INVALID_REFERENCE?;
- }
-
- if (!tree.ref_is_present(ref)) {
- return vtree::faults::REFERENCE_NOT_PRESENT?;
- }
-
- return tree.vector[ref];
-}
-
-fn void VTree.print(&tree)
-{
- for (isz i = 0; i < tree.size(); i++) {
- if (tree.refs[i] == -1) {
- continue;
- }
- io::printf("[%d] {parent=%d, data=", i, tree.refs[i]);
- io::print(tree.vector[i]);
- io::printn("}");
- }
-}
diff --git a/lib/vendor b/lib/vendor
deleted file mode 160000
index db00622..0000000
--- a/lib/vendor
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit db006221a8af625630fdb8b56707f3d07d4314a2
diff --git a/docs/.gitkeep b/linux-x64/.gitkeep
similarity index 100%
rename from docs/.gitkeep
rename to linux-x64/.gitkeep
diff --git a/lib/ugui.c3l/manifest.json b/manifest.json
similarity index 76%
rename from lib/ugui.c3l/manifest.json
rename to manifest.json
index 813ed39..e34f342 100644
--- a/lib/ugui.c3l/manifest.json
+++ b/manifest.json
@@ -4,7 +4,7 @@
"targets" : {
"linux-x64" : {
"link-args" : [],
- "dependencies" : ["schrift", "grapheme"],
+ "dependencies" : ["schrift"],
"linked-libraries" : []
}
}
diff --git a/project.json b/project.json
deleted file mode 100644
index 5f672d7..0000000
--- a/project.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "langrev": "1",
- "warnings": ["no-unused"],
- "dependency-search-paths": ["lib", "lib/vendor/libraries"],
- "dependencies": ["sdl3", "ugui"],
- "features": [],
- "authors": ["Alessandro Mauri "],
- "version": "0.1.0",
- "sources": ["src/**"],
- "output": "build",
- "target": "linux-x64",
- "targets": {
- "ugui": {
- "type": "executable"
- }
- },
- "cpu": "native",
- "opt": "O0",
- "debug-info": "full"
-}
diff --git a/resources/.gitkeep b/resources/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/resources/hack-nerd.ttf b/resources/hack-nerd.ttf
deleted file mode 100644
index f397f20..0000000
Binary files a/resources/hack-nerd.ttf and /dev/null differ
diff --git a/resources/shaders/Makefile b/resources/shaders/Makefile
deleted file mode 100644
index cdc4a01..0000000
--- a/resources/shaders/Makefile
+++ /dev/null
@@ -1,33 +0,0 @@
-SOURCE_DIR := ./source
-COMPILED_DIR := ./compiled
-
-SOURCE_FILES := $(wildcard $(SOURCE_DIR)/*.glsl)
-
-COMPILED_FILES := $(patsubst $(SOURCE_DIR)/%.glsl,$(COMPILED_DIR)/%.spv,$(SOURCE_FILES))
-
-all: $(COMPILED_FILES)
- @echo "Compiling shaders from $(SOURCE_DIR) -> $(COMPILED_DIR)"
-
-$(COMPILED_DIR)/%.spv: $(SOURCE_DIR)/%.glsl
- @mkdir -p $(COMPILED_DIR)
- @stage=$$(basename $< .glsl | cut -d. -f2); \
- if [ "$$stage" = "frag" ] || [ "$$stage" = "vert" ]; then \
- echo "$$stage $(notdir $<) > $(notdir $@)"; \
- glslc -O0 -g -fshader-stage=$$stage $< -o $@; \
- else \
- echo "Skipping $<: unsupported stage $$stage"; \
- fi
-
-$(COMPILED_DIR):
- mkdir -p $(COMPILED_DIR)
-
-.PHONY: clean
-clean:
- rm -rf $(COMPILED_DIR)
-
-.PHONY: tree
-tree:
- tree $(COMPILED_DIR)
-
-.PHONY: compile_all
-compile_all: clean all tree
diff --git a/resources/shaders/source/font.frag.glsl b/resources/shaders/source/font.frag.glsl
deleted file mode 100644
index 56207b4..0000000
--- a/resources/shaders/source/font.frag.glsl
+++ /dev/null
@@ -1,21 +0,0 @@
-#version 450
-
-layout(set = 3, binding = 0) uniform Viewport {
- ivec2 view;
-};
-layout(set = 2, binding = 0) uniform sampler2D tx;
-
-layout(location = 0) in vec2 uv;
-layout(location = 1) in vec4 color;
-
-layout(location = 0) out vec4 fragColor;
-
-void main()
-{
- ivec2 ts = textureSize(tx, 0);
- vec2 fts = vec2(ts);
- vec2 real_uv = uv / fts;
-
- vec4 opacity = texture(tx, real_uv);
- fragColor = vec4(color.rgb, color.a*opacity.r);
-}
\ No newline at end of file
diff --git a/resources/shaders/source/msdf.frag.glsl b/resources/shaders/source/msdf.frag.glsl
deleted file mode 100644
index f46be74..0000000
--- a/resources/shaders/source/msdf.frag.glsl
+++ /dev/null
@@ -1,35 +0,0 @@
-#version 450
-
-layout(set = 3, binding = 0) uniform Viewport {
- ivec2 view;
-};
-layout(set = 2, binding = 0) uniform sampler2D tx;
-
-const float PX_RANGE = 4.0f;
-
-layout(location = 0) in vec2 uv;
-layout(location = 1) in vec4 color;
-
-layout(location = 0) out vec4 fragColor;
-
-float screen_px_range(vec2 uv) {
- vec2 unit_range = vec2(PX_RANGE)/vec2(textureSize(tx, 0));
- vec2 texel_size = vec2(1.0)/fwidth(uv);
- return max(0.5*dot(unit_range, texel_size), 1.0);
-}
-
-float median(float r, float g, float b) {
- return max(min(r, g), min(max(r, g), b));
-}
-
-void main() {
- ivec2 ts = textureSize(tx, 0);
- vec2 fts = vec2(ts);
- vec2 real_uv = uv / fts;
-
- vec3 msd = texture(tx, real_uv).rgb;
- float sd = median(msd.r, msd.g, msd.b);
- float distance = screen_px_range(real_uv)*(sd - 0.5);
- float opacity = clamp(distance + 0.5, 0.0, 1.0);
- fragColor = color * opacity;
-}
\ No newline at end of file
diff --git a/resources/shaders/source/rect.frag.glsl b/resources/shaders/source/rect.frag.glsl
deleted file mode 100644
index 4ae4ea4..0000000
--- a/resources/shaders/source/rect.frag.glsl
+++ /dev/null
@@ -1,27 +0,0 @@
-#version 450
-
-layout(set = 3, binding = 0) uniform Viewport {
- ivec2 view;
-};
-
-layout(location = 0) in vec4 in_color;
-layout(location = 1) in vec4 in_quad_size; // x,y, w,h
-layout(location = 2) in float in_radius;
-
-layout(location = 0) out vec4 fragColor;
-
-// SDF for a rounded rectangle given the centerpoint, half size and radius, all in pixels
-float sdf_rr(vec2 p, vec2 half_size, float radius) {
- vec2 q = abs(p) - half_size + radius;
- return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
-}
-
-void main()
-{
- vec2 centerpoint = in_quad_size.xy + in_quad_size.zw * 0.5;
- vec2 half_size = in_quad_size.zw * 0.5;
- float distance = sdf_rr(vec2(gl_FragCoord) - centerpoint, half_size, in_radius);
- float alpha = 1.0 - smoothstep(0.0, 1.5, distance);
-
- fragColor = vec4(in_color.rgb, in_color.a * alpha);
-}
\ No newline at end of file
diff --git a/resources/shaders/source/rect.vert.glsl b/resources/shaders/source/rect.vert.glsl
deleted file mode 100644
index 00785db..0000000
--- a/resources/shaders/source/rect.vert.glsl
+++ /dev/null
@@ -1,29 +0,0 @@
-#version 450
-
-layout(set = 1, binding = 0) uniform Viewport {
- ivec2 view;
-};
-
-layout(location = 0) in ivec2 position;
-layout(location = 1) in ivec4 attr; // quad x,y,w,h
-layout(location = 2) in ivec2 uv; // x,y in the texture
-layout(location = 3) in uvec4 color;
-
-layout(location = 0) out vec4 out_color;
-layout(location = 1) out vec4 out_quad_size;
-layout(location = 2) out float out_radius;
-
-void main()
-{
- // vertex position
- ivec2 px_pos = attr.xy + position.xy * attr.zw;
- vec2 clip_pos;
- clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0;
- clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0);
-
- gl_Position = vec4(clip_pos, 0.0, 1.0);
-
- out_color = vec4(color) / 255.0;
- out_quad_size = vec4(attr);
- out_radius = float(abs(uv.x));
-}
diff --git a/resources/shaders/source/sprite.frag.glsl b/resources/shaders/source/sprite.frag.glsl
deleted file mode 100644
index ade47cb..0000000
--- a/resources/shaders/source/sprite.frag.glsl
+++ /dev/null
@@ -1,18 +0,0 @@
-#version 450
-
-layout(set = 3, binding = 0) uniform Viewport {
- ivec2 view;
-};
-
-layout(location = 0) in vec2 uv;
-layout(location = 0) out vec4 fragColor;
-
-layout(set = 2, binding = 0) uniform sampler2D tx;
-
-void main()
-{
- ivec2 ts = textureSize(tx, 0);
- vec2 fts = vec2(ts);
- vec2 real_uv = uv / fts;
- fragColor = texture(tx, real_uv);
-}
\ No newline at end of file
diff --git a/resources/shaders/source/sprite.vert.glsl b/resources/shaders/source/sprite.vert.glsl
deleted file mode 100644
index 2dca841..0000000
--- a/resources/shaders/source/sprite.vert.glsl
+++ /dev/null
@@ -1,28 +0,0 @@
-#version 450
-
-layout(set = 1, binding = 0) uniform Viewport {
- ivec2 view;
-};
-
-layout(location = 0) in ivec2 position;
-layout(location = 1) in ivec4 attr; // quad x,y,w,h
-layout(location = 2) in ivec2 in_uv;
-layout(location = 3) in uvec4 color;
-
-layout(location = 0) out vec2 out_uv;
-layout(location = 1) out vec4 out_color;
-
-void main()
-{
- // vertex position
- ivec2 px_pos = attr.xy + position.xy * attr.zw;
- vec2 clip_pos;
- clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0;
- clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0);
-
- gl_Position = vec4(clip_pos, 0.0, 1.0);
-
- vec2 px_uv = in_uv.xy + position.xy * attr.zw;
- out_uv = vec2(px_uv);
- out_color = vec4(color) / 255.0;
-}
diff --git a/resources/style.css b/resources/style.css
deleted file mode 100644
index 902ee2e..0000000
--- a/resources/style.css
+++ /dev/null
@@ -1,72 +0,0 @@
-default {
- bg: #282828ff;
- fg: #fbf1c7ff;
- primary: #cc241dff;
- secondary: #458588ff;
- accent: #fabd2fff;
-}
-
-button {
- margin: 2 2 2 2;
- border: 2 2 2 2;
- padding: 1 1 1 1;
- radius: 10;
-
- bg: #3c3836ff;
- fg: #fbf1c7ff;
- primary: #cc241dff;
- secondary: #458588ff;
- accent: #fabd2fff;
-}
-
-button-active {
- margin: 2 2 2 2;
- border: 2 2 2 2;
- padding: 1 1 1 1;
- radius: 10;
-
- bg: #504945ff;
- fg: #fbf1c7ff;
- primary: #cc241dff;
- secondary: #cc241dff;
- accent: #fabd2fff;
-}
-
-checkbox {
- margin: 2 2 2 2;
- border: 2 2 2 2;
- padding: 1 1 1 1;
- radius: 10;
- size: 16;
- bg: #3c3836ff;
- fg: #fbf1c7ff;
- primary: #cc241dff;
- secondary: #458588ff;
- accent: #fabd2fff;
-}
-
-toggle {
- margin: 2 2 2 2;
- border: 2 2 2 2;
- padding: 1 1 1 1;
- radius: 10;
- size: 16;
- bg: #3c3836ff;
- fg: #fbf1c7ff;
- primary: #cc241dff;
- secondary: #458588ff;
- accent: #fabd2fff;
-}
-
-slider {
- margin: 2 2 2 2;
- padding: 2 2 2 2;
- border: 1 1 1 1;
- radius: 4;
- size: 8;
- bg: #3c3836ff;
- fg: #fbf1c7ff;
- primary: #cc241dff;
- secondary: #458588ff;
- accent: #fabd2fff;
-}
diff --git a/resources/tick_sdf.qoi b/resources/tick_sdf.qoi
deleted file mode 100644
index 51a8f81..0000000
Binary files a/resources/tick_sdf.qoi and /dev/null differ
diff --git a/resources/tux.qoi b/resources/tux.qoi
deleted file mode 100644
index 57d6333..0000000
Binary files a/resources/tux.qoi and /dev/null differ
diff --git a/resources/tux_inv.qoi b/resources/tux_inv.qoi
deleted file mode 100644
index 947fb28..0000000
Binary files a/resources/tux_inv.qoi and /dev/null differ
diff --git a/src/.gitkeep b/src/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/ugui.c3l/src/ugui_atlas.c3 b/src/atlas.c3
similarity index 70%
rename from lib/ugui.c3l/src/ugui_atlas.c3
rename to src/atlas.c3
index 7f3a40b..3bc2dbf 100644
--- a/lib/ugui.c3l/src/ugui_atlas.c3
+++ b/src/atlas.c3
@@ -4,9 +4,9 @@ import std::io;
faultdef CANNOT_PLACE, INVALID_TYPE;
-enum AtlasType {
- ATLAS_GRAYSCALE,
- ATLAS_R8G8B8A8,
+enum AtlasType : (sz bpp, typeid underlying) {
+ ATLAS_GRAYSCALE {1, char},
+ ATLAS_R8G8B8A8 {4, uint},
}
// black and white atlas
@@ -21,23 +21,6 @@ struct Atlas {
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)
@@ -52,10 +35,10 @@ macro @pixel_convert(p, AtlasType $in, AtlasType $out)
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);
+ 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));
+ return (ATLAS_R8G8B8A8.underlying)(x|(x<<8)|(x<<16)|(255<<24));
$default: $error "Unimplemented pixel format conversion";
$endswitch
$endif
@@ -69,7 +52,7 @@ fn void? Atlas.new(&atlas, Id id, AtlasType type, ushort width, ushort height)
atlas.width = width;
atlas.height = height;
- atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height*type.bpp());
+ atlas.buffer = mem::new_array(char, (sz)atlas.width*atlas.height*type.bpp);
}
fn void Atlas.free(&atlas)
@@ -103,26 +86,26 @@ fn Point? Atlas.place(&atlas, char[] pixels, ushort w, ushort h, ushort stride)
p = atlas.row;
} else {
atlas.row.x = 0;
- atlas.row.y = atlas.row.y + atlas.row_h;
+ atlas.row.y = (short)atlas.row.y + (short)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?;
+ 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 ..];
+ sz bpp = atlas.type.bpp;
+ for (sz y = 0; y < h; y++) {
+ for (sz x = 0; x < w; x++) {
+ char[] buf = atlas.buffer[(sz)(p.y+y)*atlas.width*bpp + (p.x+x)*bpp ..];
+ char[] pix = pixels[(sz)y*stride*bpp + x*bpp ..];
buf[0..bpp-1] = pix[0..bpp-1];
}
}
- atlas.row.x += w;
+ atlas.row.x += (short)w;
if (h > atlas.row_h) {
atlas.row_h = h;
}
diff --git a/lib/ugui.c3l/src/cache.c3 b/src/cache.c3
similarity index 74%
rename from lib/ugui.c3l/src/cache.c3
rename to src/cache.c3
index 130ffd1..53141a4 100644
--- a/lib/ugui.c3l/src/cache.c3
+++ b/src/cache.c3
@@ -1,4 +1,4 @@
-module cache{Key, Value, SIZE};
+module cache;
/* LRU Cache
* The cache uses a pool (array) to store all the elements, each element has
@@ -9,22 +9,19 @@ module cache{Key, Value, SIZE};
* 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::allocator;
+import std::core::mem::alloc;
import std::collections::bitset;
import std::collections::map;
alias BitArr = bitset::BitSet{SIZE};
-alias IdTable = map::HashMap{Key, usz};
-alias IdTableEntry = map::Entry{Key, usz};
+alias IdTable = map::HashMap{Key, sz};
+alias IdTableEntry = map::Entry{Key, sz};
const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0);
struct Cache {
+ Allocator allocator;
BitArr present, used;
IdTable table;
Value[] pool;
@@ -32,7 +29,7 @@ struct Cache {
}
// Every CACHE_CYCLES operations mark as not-present the unused elements
-macro Cache.cycle(&cache) @private {
+macro Cache.cycle(&cache) {
cache.cycle_count++;
if (cache.cycle_count > CACHE_NCYCLES) {
for (usz i = 0; i < cache.present.data.len; i++) {
@@ -43,19 +40,20 @@ macro Cache.cycle(&cache) @private {
}
}
-fn void? Cache.init(&cache)
+fn void? Cache.init(&cache, Allocator allocator)
{
- cache.table.init(allocator::heap(), capacity: SIZE);
+ cache.allocator = allocator;
+ cache.table.init(allocator, capacity: SIZE);
// FIXME: this shit is SLOW
foreach (idx, bit : cache.used) { cache.used[idx] = false; }
foreach (idx, bit : cache.present) { cache.present[idx] = false; }
- cache.pool = mem::new_array(Value, SIZE);
+ cache.pool = alloc::new_array(allocator, Value, SIZE);
}
fn void Cache.free(&cache)
{
(void)cache.table.free();
- (void)mem::free(cache.pool);
+ (void)alloc::free(cache.allocator, cache.pool);
}
fn Value*? Cache.search(&cache, Key id)
@@ -66,17 +64,18 @@ fn Value*? Cache.search(&cache, Key id)
/* MISS, wrong key */
if (entry.key != id) {
cache.table.remove(id)!;
- return NOT_FOUND?;
+ return NOT_FOUND~;
}
/* MISS, the data is not valid (not present) */
if (!cache.present[entry.value]) {
// if the data is not present but it is still in the table, remove it
cache.table.remove(id)!;
- return NOT_FOUND?;
+ return NOT_FOUND~;
}
/* HIT, set as recently used */
+ //io::printfn("HIT: %d [%d]", entry.value, entry.key);
cache.used[entry.value] = true;
return &(cache.pool[entry.value]);
}
@@ -95,20 +94,22 @@ fn void Cache.remove(&cache, Key id)
/* Look for a free spot in the present bitmap and return its index */
/* If there is no free space left then just return the first position */
-fn usz Cache.get_free_spot(&cache) @private
+fn sz Cache.get_free_spot(&cache)
{
- const BITS = $typeof(cache.present.data[0]).sizeof*8;
+ const BITS = @bitsizeof(cache.present.data[0]);
foreach (idx, d: cache.present.data) {
- if (d.clz() != BITS) {
- return idx*BITS + BITS-d.clz();
+ if (d != $Typeof(d)::max) {
+ sz spot = idx*BITS + BITS-d.clz();
+ if (cache.used[spot]) unreachable("free spot is not actually free: %d", spot);
+ return spot;
}
}
return 0;
}
-fn Value*? Cache.insert_at(&cache, Value *g, Key id, usz index) @private
+<* @require g != null *>
+fn Value*? Cache.insert_at(&cache, Value* g, Key id, sz index)
{
- // TODO: verify index, g and id
Value* spot;
/* Set used and present */
@@ -123,18 +124,20 @@ fn Value*? Cache.insert_at(&cache, Value *g, Key id, usz index) @private
}
// Insert an element in the cache, returns the index
+<* @require g != null *>
fn Value*? Cache.insert_new(&cache, Value* g, Key id)
{
- usz index = cache.get_free_spot();
+ sz index = cache.get_free_spot();
return cache.insert_at(g, id, index);
}
+<* @require g != null *>
fn Value*? Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null)
{
Value*? c = cache.search(id);
if (catch e = c) {
if (e != NOT_FOUND) {
- return e?;
+ return e~;
} else {
// if the element is new (inserted) set the is_new flag
if (is_new) *is_new = true;
diff --git a/src/cmd.c3 b/src/cmd.c3
new file mode 100644
index 0000000..4e98cc0
--- /dev/null
+++ b/src/cmd.c3
@@ -0,0 +1,216 @@
+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(tmem);
+
+ for (sz 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);
+ }
+ }
+
+ sz l = stack.len();
+ for (sz i; i < l; i++) {
+ queue.push(stack.pop())!;
+ }
+
+}
+
+// implement the Printable interface
+fn sz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
+{
+ sz 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 + (ushort)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 = (short)atlas.width,
+ .height = (short)atlas.height,
+ .bpp = (short)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()})!!;
diff --git a/src/core.c3 b/src/core.c3
new file mode 100644
index 0000000..9cd4fcc
--- /dev/null
+++ b/src/core.c3
@@ -0,0 +1,398 @@
+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;
+ sz 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)::size*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 < $vaarg.len; $i++:
+ id ^= (Id)$vaarg[$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(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, (short)ctx.width, (short)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((short)ctx.width);
+ elem.layout.h = @exact((short)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
+ Point mdelta = ctx.current_input.mouse.pos - ctx.input.mouse.pos;
+ ctx.input = ctx.current_input;
+ ctx.input.mouse.delta = mdelta;
+ ctx.current_input.events = {};
+ ctx.current_input.mouse.scroll = {};
+ ctx.current_input.mouse.updated = BTN_NONE;
+ ctx.current_input.keyboard.text_len = 0;
+
+ // DO THE LAYOUT
+ ctx.layout_element_tree()!;
+
+ foreach (idx, id : ctx.tree.elem_vec) {
+ if (!ctx.tree.is_used((int)idx)) continue;
+ Elem* c = ctx.find_elem(id);
+ // reset events
+ c.events = {};
+ // reset shown flag
+ // TODO: use shown_last_frame to avoid this loop entirely
+ c.flags.shown = false;
+ }
+
+ // Propagate input events to the right elements
+ ctx.set_elem_events(ctx.hover_id);
+ ctx.set_elem_events(ctx.focus_id);
+
+
+ // 1. clear the tree
+ 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),
+ };
+}
diff --git a/lib/ugui.c3l/src/ugui_font.c3 b/src/font.c3
similarity index 58%
rename from lib/ugui.c3l/src/ugui_font.c3
rename to src/font.c3
index 020ed75..0bc233e 100644
--- a/lib/ugui.c3l/src/ugui_font.c3
+++ b/src/font.c3
@@ -1,19 +1,26 @@
-module ugui;
+module ugui::font;
import schrift;
-import grapheme;
import std::collections::map;
import std::core::mem;
import std::core::mem::allocator;
import std::io;
-import std::ascii;
+import std::core::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)
* +-------------*---+ -
@@ -58,9 +65,18 @@ struct Font {
bool should_update; // should send update_atlas command, resets at frame_end()
}
-fn void? Font.load(&font, String name, ZString path, uint height, float scale)
+macro Rect Glyph.bounds(&g) => {.x = (short)g.ox, .y = (short)g.oy, .w = (short)g.w, .h = (short)g.h};
+macro Rect Glyph.uv(&g) => {.x = (short)g.u, .y = (short)g.v, .w = (short)g.w, .h = (short)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::heap(), capacity: FONT_CACHED);
+ font.table.init(allocator, capacity: FONT_CACHED);
font.id = name.hash();
font.size = height*scale;
@@ -74,7 +90,7 @@ fn void? Font.load(&font, String name, ZString path, uint height, float scale)
font.sft.font = schrift::loadfile(path);
if (font.sft.font == null) {
font.table.free();
- return TTF_LOAD_FAILED?;
+ return TTF_LOAD_FAILED~;
}
schrift::SftLMetrics lmetrics;
@@ -84,17 +100,19 @@ fn void? Font.load(&font, String name, ZString path, uint height, float scale)
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++) {
- font.get_glyph((Codepoint)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;
@@ -102,12 +120,13 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code)
if (catch excuse = gp) {
if (excuse != NOT_FOUND) {
- return excuse?;
+ return excuse~;
}
} else {
return gp;
}
+
// missing glyph, render and place into an atlas
Glyph glyph;
@@ -115,21 +134,21 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code)
schrift::SftGMetrics gmtx;
if (schrift::lookup(&font.sft, (SftUChar)code, &gid) < 0) {
- return MISSING_GLYPH?;
+ return MISSING_GLYPH~;
}
if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) {
- return BAD_GLYPH_METRICS?;
+ return BAD_GLYPH_METRICS~;
}
schrift::SftImage img = {
.width = gmtx.minWidth,
.height = gmtx.minHeight,
};
- char[] pixels = mem::new_array(char, (usz)img.width * img.height);
+ char[] pixels = mem::new_array(char, (usz)img.width * (usz)img.height);
img.pixels = pixels;
if (schrift::render(&font.sft, gid, img) < 0) {
- return RENDER_ERROR?;
+ return RENDER_ERROR~;
}
glyph.code = code;
@@ -142,8 +161,8 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code)
// 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;
+ glyph.u = (ushort)uv.x;
+ glyph.v = (ushort)uv.y;
mem::free(pixels);
@@ -153,6 +172,9 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code)
return font.table.get_ref(code);
}
+<*
+@param [&inout] font
+*>
fn void Font.free(&font)
{
font.atlas.free();
@@ -160,107 +182,44 @@ fn void Font.free(&font)
schrift::freefont(font.sft.font);
}
-fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0)
+
+// ---------------------------------------------------------------------------------- //
+// FONT LOAD AND QUERY //
+// ---------------------------------------------------------------------------------- //
+
+module ugui;
+
+<*
+@param [&inout] ctx
+@param [in] name
+@param [&in] path
+@require height > 0, scale > 0: "height and scale must be positive non-zero"
+*>
+fn void? Ctx.load_font(&ctx, Allocator allocator, String name, ZString path, uint height, float scale = 1.0)
{
- return ctx.font.load(name, path, height, scale);
+ return ctx.font.load(allocator, name, path, height, scale);
}
<*
-@require off != null
+@param [&in] ctx
+@param [in] label
+@require ctx.font.id != 0 : "font not loaded"
*>
-fn Codepoint str_to_codepoint(char[] str, usz* off)
-{
- Codepoint cp;
- isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp);
- if (b == 0 || b > str.len) {
- return 0;
- }
- *off = b;
- return cp;
-}
-
-fn Rect? Ctx.get_text_bounds(&ctx, String text)
-{
- Rect text_bounds;
- short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
- short line_gap = (short)ctx.font.linegap;
- text_bounds.h = line_height;
- Glyph* gp;
-
- // TODO: account for unicode codepoints
- short line_len;
- Codepoint cp;
- usz off, x;
- while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
- off += x;
- bool n;
- if (!ascii::is_cntrl((char)cp)) {
- gp = ctx.font.get_glyph(cp)!;
- line_len += gp.adv;
- } else if (cp == '\n'){
- text_bounds.h += line_height + line_gap;
- line_len = 0;
- } else {
- continue;
- }
- if (line_len > text_bounds.w) {
- text_bounds.w = line_len;
- }
- }
-
- return text_bounds;
-}
-
-fn Point? Ctx.get_cursor_position(&ctx, String text)
-{
- short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
- short line_gap = (short)ctx.font.linegap;
- Glyph* gp;
-
- // TODO: account for unicode codepoints
- Point line;
- Codepoint cp;
- usz off, x;
- while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
- off += x;
- bool n;
- if (!ascii::is_cntrl((char)cp)) {
- gp = ctx.font.get_glyph(cp)!;
- line.x += gp.adv;
- } else if (cp == '\n'){
- line.y += line_height + line_gap;
- line.x = 0;
- } else {
- continue;
- }
- }
-
- return line;
-}
-
-
-fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds)
-{
- short dw = bounds.w - text_bounds.w;
- short dh = bounds.h - text_bounds.h;
-
- return {.x = dw/2, .y = dh/2};
-}
-
// TODO: check if the font is present in the context
-fn Id Ctx.get_font_id(&ctx, String label)
-{
- return (Id)label.hash();
-}
+fn Id Ctx.get_font_id(&ctx, String label) => (Id)ctx.font.id;
+<*
+@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 WRONG_ID~;
}
return &ctx.font.atlas;
}
+<* @param [&in] font *>
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);
diff --git a/src/input.c3 b/src/input.c3
new file mode 100644
index 0000000..29dca3c
--- /dev/null
+++ b/src/input.c3
@@ -0,0 +1,240 @@
+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)(uint::max);
+//const ModKeys KMOD_ANY = (ModKeys)(ModKeys::inner::max);
+
+const MouseButtons BTN_NONE = {};
+const MouseButtons BTN_ANY = (MouseButtons)(uint::max);
+//const MouseButtons BTN_ANY = (MouseButtons)(MouseButtons::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)(uint::max);
+//const ModKeys KEY_ANY = (ModKeys)(ModKeys::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 != (ushort)width || ctx.height != (ushort)height;
+ ctx.width = (ushort)width;
+ ctx.height = (ushort)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, (short)ctx.width);
+ ctx.current_input.mouse.pos.y = math::clamp(y, (short)0, (short)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, (short)ctx.width);
+ ctx.current_input.mouse.pos.y = math::clamp(my, (short)0, (short)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;
+
+ sz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len;
+ sz 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;
+
+ sz 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 ..];
+
+ sz 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;
+}
diff --git a/src/layout.c3 b/src/layout.c3
new file mode 100644
index 0000000..a4a523c
--- /dev/null
+++ b/src/layout.c3
@@ -0,0 +1,365 @@
+module ugui;
+
+import std::math;
+import std::io;
+
+enum LayoutDirection {
+ ROW,
+ COLUMN,
+}
+
+enum Anchor {
+ TOP_LEFT,
+ LEFT,
+ BOTTOM_LEFT,
+ BOTTOM,
+ BOTTOM_RIGHT,
+ RIGHT,
+ TOP_RIGHT,
+ TOP,
+ CENTER
+}
+
+bitstruct ResizeDirection : char {
+ bool x : 0;
+ bool y : 1;
+}
+
+bitstruct ResizeAnchor : char {
+ bool top : 0;
+ bool bottom : 1;
+ bool left : 2;
+ bool right : 3;
+}
+
+struct Layout {
+ Size w, h; // size of the CONTENT, does not include margin, border and padding
+ struct children { // the children size includes the children's margin/border/pading
+ Size w, h;
+ }
+ TextSize text;
+ ushort grow_children;
+ short occupied;
+ Point origin;
+ Point scroll_offset;
+ // false: the element is laid out according to the parent
+ // true: the element is laid out separate from all other children and the relative position to
+ // the parent is the .origin field
+ bool absolute;
+ LayoutDirection dir; // the direction the children are laid out
+ Anchor anchor; // how the children are positioned
+ Rect content_offset; // combined effect of margin, border and padding
+}
+
+// Returns the width and height of a @fit() element based on it's wanted size (min/max)
+// and the content size, this function is used to both update the parent's children size and
+// give the dimensions of a fit element
+// TODO: test and cleanup this function
+macro Point Layout.get_dimensions(&el)
+{
+ Point dim;
+ // if the direction is ROW then the text is placed horizontally with the children
+ if (el.dir == ROW) {
+ Size content_width = el.children.w + el.text.width;
+ Size width = el.w.combine(content_width);
+ short final_width = width.greater();
+
+ short text_height;
+ if (el.text.area != 0) {
+ short text_width = (@exact(final_width) - el.children.w).combine(el.text.width).min;
+ text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
+ }
+
+ Size content_height = el.children.h.comb_max(@exact(text_height));
+ Size height = el.h.combine(content_height);
+ short final_height = height.greater();
+
+ dim = {
+ .x = final_width + el.content_offset.x + el.content_offset.w,
+ .y = final_height + el.content_offset.y + el.content_offset.h,
+ };
+ } else {
+ // if the direction is COLUMN the text and children are one on top of the other
+ Size content_width = el.children.w.comb_max(el.text.width);
+ Size width = el.w.combine(content_width);
+ short final_width = width.greater();
+
+ short text_height;
+ if (el.text.area != 0) {
+ short text_width = @exact(final_width).combine(el.text.width).min;
+ text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min;
+ }
+
+ Size content_height = el.children.h + @exact(text_height);
+ Size height = el.h.combine(content_height);
+ short final_height = height.greater();
+
+ dim = {
+ .x = final_width + el.content_offset.x + el.content_offset.w,
+ .y = final_height + el.content_offset.y + el.content_offset.h,
+ };
+ }
+
+ // GROSS HACK FOR EXACT DIMENSIONS
+ if (el.w.@is_exact()) dim.x = el.w.min + el.content_offset.x + el.content_offset.w;
+ if (el.h.@is_exact()) dim.y = el.h.min + el.content_offset.y + el.content_offset.h;
+
+ // GROSS HACK FOR GROW DIMENSIONS
+ // FIXME: does this always work?
+ if (el.w.@is_grow()) dim.x = 0;
+ if (el.h.@is_grow()) dim.y = 0;
+
+ return dim;
+}
+
+// The content space of the element
+macro Point Elem.content_space(&e)
+{
+ return {
+ .x = (short)max(e.bounds.w - e.layout.content_offset.x - e.layout.content_offset.w, 0),
+ .y = (short)max(e.bounds.h - e.layout.content_offset.y - e.layout.content_offset.h, 0),
+ };
+}
+
+// Update the parent element's children size
+fn void update_parent_size(Elem* child, Elem* parent)
+{
+ Layout* cl = &child.layout;
+ Layout* pl = &parent.layout;
+
+ // if the element has absolute position do not update the parent
+ if (cl.absolute) return;
+
+ Point child_size = cl.get_dimensions();
+
+ switch (pl.dir) {
+ case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds
+ pl.children.w += @exact(child_size.x);
+ pl.children.h = pl.children.h.comb_max(@exact(child_size.y));
+ if (child.layout.w.@is_grow()) parent.layout.grow_children++;
+ case COLUMN: // do the opposite on column
+ pl.children.w = pl.children.w.comb_max(@exact(child_size.x));
+ pl.children.h += @exact(child_size.y);
+ if (child.layout.h.@is_grow()) parent.layout.grow_children++;
+ }
+}
+
+macro Rect Elem.content_bounds(&elem) => elem.bounds.pad(elem.layout.content_offset);
+
+// Assign the width and height of an element in the directions that it doesn't need to grow
+fn void resolve_dimensions(Elem* e, Elem* p)
+{
+ Layout* el = &e.layout;
+ Layout* pl = &p.layout;
+
+ Point elem_dimensions = el.get_dimensions();
+ e.bounds.w = elem_dimensions.x;
+ e.bounds.h = elem_dimensions.y;
+
+ // if the element has absolute position do not update the parent
+ if (el.absolute) return;
+
+ switch (pl.dir) {
+ case ROW:
+ if (!el.w.@is_grow()) pl.occupied += e.bounds.w;
+ case COLUMN:
+ if (!el.h.@is_grow()) pl.occupied += e.bounds.h;
+ }
+}
+
+fn void resolve_grow_elements(Elem* e, Elem* p)
+{
+ // WIDTH
+ if (e.layout.w.@is_grow()) {
+ if (e.layout.absolute) { // absolute children do not need to share space
+ e.bounds.w = p.content_space().x;
+ } else if (p.layout.dir == ROW) { // grow along the axis, divide the parent size
+ short slot = (short)max(((p.content_space().x - p.layout.occupied) / p.layout.grow_children), 0);
+ e.bounds.w = slot;
+ p.layout.grow_children--;
+ p.layout.occupied += slot;
+ } else if (p.layout.dir == COLUMN) { // grow across the layout axis, inherit width of the parent
+ e.bounds.w = p.content_space().x + p.layout.scroll_offset.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 + p.layout.scroll_offset.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);
+ }
+ }
+}
diff --git a/src/main.c3 b/src/main.c3
deleted file mode 100644
index cd1f341..0000000
--- a/src/main.c3
+++ /dev/null
@@ -1,312 +0,0 @@
-import std::io;
-import vtree;
-import cache;
-import ugui;
-import std::time;
-import std::collections::ringbuffer;
-import std::core::string;
-import std::ascii;
-import sdlrenderer::ren;
-import sdl3::sdl;
-
-alias Times = ringbuffer::RingBuffer{time::NanoDuration[128]};
-
-fn void Times.print_stats(×)
-{
- 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);
-}
-
-struct TimeStats {
- time::NanoDuration min, max, avg;
-}
-
-fn TimeStats Times.get_stats(×)
-{
- 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);
-
- return {.min = min, .max = max, .avg = avg};
-}
-
-
-const char[*] MSDF_FS_PATH = "resources/shaders/compiled/msdf.frag.spv";
-const char[*] SPRITE_FS_PATH = "resources/shaders/compiled/sprite.frag.spv";
-const char[*] FONT_FS_PATH = "resources/shaders/compiled/font.frag.spv";
-const char[*] RECT_FS_PATH = "resources/shaders/compiled/rect.frag.spv";
-
-const char[*] SPRITE_VS_PATH = "resources/shaders/compiled/sprite.vert.spv";
-const char[*] RECT_VS_PATH = "resources/shaders/compiled/rect.vert.spv";
-
-const char[*] STYLESHEET_PATH = "resources/style.css";
-
-fn int main(String[] args)
-{
- ugui::Ctx ui;
- ui.init()!!;
- defer ui.free();
-
- ren::Renderer ren;
- ren.init("Ugui Test", 800, 600, true);
- defer ren.free();
- ui.input_window_size(800, 600)!!;
-
- //
- // FONT LOADING
- //
- {
- // import font in the ui context
- ui.load_font("font1", "resources/hack-nerd.ttf", 16)!!;
-
- // create the rendering pipeline
- ren.font_atlas_id = ui.get_font_id("font1");
- ren.load_spirv_shader_from_file("UGUI_PIPELINE_FONT", SPRITE_VS_PATH, FONT_FS_PATH, 1, 0);
- ren.create_pipeline("UGUI_PIPELINE_FONT", SPRITE);
-
- // send the atlas to the gpu
- Atlas* font_atlas = ui.get_font_atlas("font1")!!;
- ren.new_texture("font1", JUST_ALPHA, font_atlas.buffer, font_atlas.width, font_atlas.height);
- }
-
- //
- // ICON LOADING
- //
- {
- // create the atlas and upload some icons
- ui.sprite_atlas_create("icons", AtlasType.ATLAS_R8G8B8A8, 512, 512)!!;
- ui.import_sprite_file_qoi("tux", "resources/tux.qoi")!!;
- ui.import_sprite_file_qoi("tick", "resources/tick_sdf.qoi", SpriteType.SPRITE_MSDF)!!;
-
- // create the rendering pipelines
- ren.sprite_atlas_id = ui.get_sprite_atlas_id("icons");
- // normal sprite pipeline
- ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE", SPRITE_VS_PATH, SPRITE_FS_PATH, 1, 0);
- ren.create_pipeline("UGUI_PIPELINE_SPRITE", SPRITE);
- // msdf sprite pipeline
- ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE_MSDF", SPRITE_VS_PATH, MSDF_FS_PATH, 1, 0);
- ren.create_pipeline("UGUI_PIPELINE_SPRITE_MSDF", SPRITE);
-
- // upload the atlas to the gpu
- Atlas atlas = ui.sprite_atlas.atlas;
- ren.new_texture("icons", FULL_COLOR, atlas.buffer, atlas.width, atlas.height);
- }
-
- //
- // RECT PIPELINE
- //
- ren.load_spirv_shader_from_file("UGUI_PIPELINE_RECT", RECT_VS_PATH, RECT_FS_PATH, 0, 0);
- ren.create_pipeline("UGUI_PIPELINE_RECT", RECT);
-
-
- // CSS INPUT
- io::printfn("imported %d styles", ui.import_style_from_file(STYLESHEET_PATH));
-
- isz frame;
- double fps;
- bool toggle = true;
- time::Clock clock;
- time::Clock fps_clock;
- time::Clock sleep_clock;
- Times ui_times;
- Times draw_times;
-
-
- //
- // MAIN LOOP
- //
- sdl::start_text_input(ren.win);
-
- sdl::Event e;
- bool quit = false;
- ugui::ModKeys mod;
- ugui::MouseButtons btn;
- while (!quit) {
- clock.mark();
- fps_clock.mark();
- sleep_clock.mark();
-
- do {
- switch (e.type) {
- case EVENT_QUIT:
- quit = true;
- case EVENT_KEY_UP: nextcase;
- case EVENT_KEY_DOWN:
- mod.rctrl = e.key.key == K_RCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.rctrl;
- mod.lctrl = e.key.key == K_LCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.lctrl;
- mod.bkspc = e.key.key == K_BACKSPACE ? !!(e.type == EVENT_KEY_DOWN) : mod.bkspc;
-
- // pressing ctrl+key or alt+key does not generate a character as such no
- // TEXT_INPUT event is generated. When those keys are pressed we have to
- // do manual text input, bummer
- if (e.type == EVENT_KEY_DOWN && (mod.lctrl || mod.rctrl)) {
- if (ascii::is_alnum_m((uint)e.key.key)) {
- ui.input_char((char)e.key.key);
- }
- }
-
- if (e.type == EVENT_KEY_DOWN && e.key.key == K_RETURN) ui.input_char('\n');
-
- case EVENT_TEXT_INPUT:
- ui.input_text_utf8(e.text.text.str_view());
- case EVENT_WINDOW_RESIZED:
- ui.input_window_size((short)e.window.data1, (short)e.window.data2)!!;
- case EVENT_WINDOW_FOCUS_GAINED:
- ui.input_changefocus(true);
- case EVENT_WINDOW_FOCUS_LOST:
- ui.input_changefocus(false);
- case EVENT_MOUSE_MOTION:
- ui.input_mouse_abs((short)e.motion.x, (short)e.motion.y);
- case EVENT_MOUSE_WHEEL:
- ui.input_mouse_wheel((short)e.wheel.integer_x, (short)e.wheel.integer_y);
- case EVENT_MOUSE_BUTTON_DOWN: nextcase;
- case EVENT_MOUSE_BUTTON_UP:
- sdl::MouseButtonFlags mb = sdl::get_mouse_state(null, null);
- btn = {
- .btn_left = !!(mb & BUTTON_LMASK),
- .btn_right = !!(mb & BUTTON_RMASK),
- .btn_middle = !!(mb & BUTTON_MMASK),
- .btn_4 = !!(mb & BUTTON_X1MASK),
- .btn_5 = !!(mb & BUTTON_X2MASK),
- };
- case EVENT_POLL_SENTINEL: break;
- default:
- io::eprintfn("unhandled event: %s", e.type);
- }
- } while(sdl::poll_event(&e));
- ui.input_mod_keys(mod);
- ui.input_mouse_button(btn);
-
- /* End Input Handling */
-
- /* Start UI Handling */
- ui.frame_begin()!!;
-
- if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true;
-
- ui.div_begin({.w=-100})!!;
- {
- ui.layout_set_column()!!;
- if (ui.button({0,0,30,30}, toggle)!!.mouse_press) {
- io::printn("press button0");
- toggle = !toggle;
- }
- //ui.layout_next_column()!!;
- if (ui.button({0,0,30,30})!!.mouse_press) {
- io::printn("press button1");
- }
- //ui.layout_next_column()!!;
- if (ui.button({0,0,30,30})!!.mouse_release) {
- io::printn("release button2");
- }
-
- ui.layout_set_row()!!;
- ui.layout_next_row()!!;
- static float rf, gf, bf, af;
- ui.slider_ver({0,0,30,100}, &rf)!!;
- ui.slider_ver({0,0,30,100}, &gf)!!;
- ui.slider_ver({0,0,30,100}, &bf)!!;
- ui.slider_ver({0,0,30,100}, &af)!!;
-
- ui.layout_next_column()!!;
- ui.text_unbounded("Ciao Mamma\nAbilità ⚡\n'\udb80\udd2c'")!!;
-
- ui.layout_next_column()!!;
- ui.button_label("Continua!")!!;
-
- ui.layout_next_row()!!;
- static bool check;
- ui.checkbox("", {}, &check, "tick")!!;
- ui.checkbox("", {}, &check)!!;
- ui.toggle("", {}, &toggle)!!;
- };
- ui.sprite("tux")!!;
-
- static char[128] text_box = "ciao mamma";
- static usz text_len = "ciao mamma".len;
- ui.text_box({0,0,200,200}, text_box[..], &text_len)!!;
-
- ui.div_end()!!;
-
- ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!;
- {
- ui.layout_set_column()!!;
- static float slider2 = 0.5;
- if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) {
- io::printfn("other slider: %f", slider2);
- }
- ui.button({0,0,50,50})!!;
- ui.button({0,0,50,50})!!;
- ui.button({0,0,50,50})!!;
- ui.button({0,0,50,50})!!;
- if (toggle) {
- ui.button({0,0,50,50})!!;
- ui.button({0,0,50,50})!!;
- ui.button({0,0,50,50})!!;
- ui.button({0,0,50,50})!!;
- }
- ui.layout_next_column()!!;
- ui.layout_set_row()!!;
- static float f1, f2;
- ui.slider_hor({0,0,100,30}, &f1)!!;
- ui.slider_hor({0,0,100,30}, &f2)!!;
- };
- ui.div_end()!!;
-
- // Timings counter
- TimeStats dts = draw_times.get_stats();
- TimeStats uts = ui_times.get_stats();
-
- ui.layout_set_floating()!!;
- // FIXME: I cannot anchor shit to the bottom of the screen
- ui.div_begin({0, ui.height-150, -300, 150})!!;
- {
- ui.layout_set_column()!!;
- ui.text_unbounded(string::tformat("frame %d, fps = %.2f", frame, fps))!!;
- ui.text_unbounded(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!;
- ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[..]))!!;
- };
- ui.div_end()!!;
-
- ui.frame_end()!!;
- /* End UI Handling */
- ui_times.push(clock.mark());
- //ui_times.print_stats();
-
-
- /* Start UI Drawing */
- ren.begin_render(true);
-
- ren.render_ugui(&ui.cmd_queue);
-
- ren.end_render();
-
- draw_times.push(clock.mark());
- //draw_times.print_stats();
- /* End Drawing */
-
- // wait for the next event, timeout after 100ms
-
- sdl::wait_event_timeout(&e, (int)(100.0-sleep_clock.mark().to_ms()-0.5));
-
- fps = 1.0 / fps_clock.mark().to_sec();
- frame++;
- }
-
- return 0;
-}
diff --git a/src/mtree.c3 b/src/mtree.c3
new file mode 100644
index 0000000..c0042bf
--- /dev/null
+++ b/src/mtree.c3
@@ -0,0 +1,367 @@
+module mtree;
+
+/* ================================================================================================
+ * 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::alloc;
+import std::io;
+import std::bits;
+import std::collections::list;
+
+
+alias Bitmap = ulong;
+const BITS = Bitmap::size*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, (sz)size);
+ tree.used = alloc::new_array(tree.allocator, Bitmap, (sz)size/BITS);
+ tree.elem_vec = alloc::new_array(tree.allocator, Type, (sz)size);
+ tree.refs_vec = alloc::new_array(tree.allocator, RefNode, (sz)size);
+
+ foreach (&r: tree.refs_vec) {
+ r.next = -1;
+ }
+}
+
+
+<* @param [&inout] tree *>
+fn void MTree.free(&tree)
+{
+ tree.elements = 0;
+ tree.queue.free();
+ (void)alloc::free(tree.allocator, tree.used);
+ (void)alloc::free(tree.allocator, tree.elem_vec);
+ (void)alloc::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 int? 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
+ );
+ }
+ }
+}
diff --git a/src/renderer.c3 b/src/renderer.c3
deleted file mode 100644
index eae3b8b..0000000
--- a/src/renderer.c3
+++ /dev/null
@@ -1,962 +0,0 @@
-module idlist{Type};
-
-// extends the List type to search elements that have a type.id property
-// TODO: check that type has an id
-
-import std::collections::list;
-
-alias IdList = List{Type};
-
-macro Type* IdList.get_from_name(&self, String name)
-{
- return self.get_from_id(name.hash());
-}
-
-macro Type* IdList.get_from_id(&self, id)
-{
- foreach(&s: self) {
- if (s.id == id) {
- return s;
- }
- }
- return null;
-}
-
-module sdlrenderer::ren;
-
-// 2D renderer for ugui, based on SDL3 using the new GPU API
-
-import std::io;
-import std::core::mem;
-import sdl3::sdl;
-import idlist;
-import ugui;
-
-struct Shader {
- sdl::GPUShader* frag;
- sdl::GPUShader* vert;
- ugui::Id id;
-}
-
-struct Pipeline {
- sdl::GPUGraphicsPipeline* pipeline;
- ugui::Id id;
-}
-
-struct Texture {
- sdl::GPUTexture* texture;
- sdl::GPUSampler* sampler;
- ushort width, height;
- ugui::Id id;
-}
-
-// The GPU buffers that contain quad info, the size is determined by MAX_QUAD_BATCH
-const int MAX_QUAD_BATCH = 2048;
-struct QuadBuffer {
- sdl::GPUBuffer* vert_buf; // on-gpu vertex buffer
- sdl::GPUBuffer* idx_buf; // on-gpu index buffer
- sdl::GPUBuffer* attr_buf; // on-gpu quad attribute buffer
-
- sdl::GPUTransferBuffer* attr_ts;
-
- QuadAttributes[] attr_ts_mapped;
-
- // how many quads are currently stored
- int count;
-
- bool initialized;
-}
-
-alias ShaderList = IdList{Shader};
-alias PipelineList = IdList{Pipeline};
-alias TextureList = IdList{Texture};
-
-struct Renderer {
- sdl::Window* win;
- sdl::GPUDevice* gpu;
- sdl::GPURenderPass* render_pass;
- sdl::GPUTexture* swapchain_texture;
- sdl::GPUCommandBuffer* render_cmdbuf;
-
- QuadBuffer quad_buffer;
- ShaderList shaders;
- PipelineList pipelines;
- TextureList textures;
-
- Id sprite_atlas_id;
- Id font_atlas_id;
-
- int scissor_x, scissor_y, scissor_w, scissor_h;
-}
-
-// How each vertex is represented in the gpu
-struct Vertex {
- short x, y;
-}
-
-// Attributes of each quad instance
-struct QuadAttributes {
- struct pos {
- short x, y, w, h;
- }
- struct uv {
- short u, v;
- }
- uint color;
-}
-
-// A single quad
-struct Quad {
- struct vertices {
- Vertex v1,v2,v3,v4;
- }
- struct indices {
- short i1,i2,i3,i4,i5,i6;
- }
-}
-
-struct ViewsizeUniform @align(16) {
- int w, h;
-}
-
-const int DEBUG = 1;
-const bool CYCLE = true;
-
-fn void Renderer.init(&self, ZString title, uint width, uint height, bool vsync)
-{
- // set wayland hint automagically
-$if DEBUG == 0:
- bool has_wayland = false;
- for (int i = 0; i < sdl::get_num_video_drivers(); i++) {
- ZString driver = sdl::get_video_driver(i);
- if (driver.str_view() == "wayland") {
- has_wayland = true;
- break;
- }
- }
-
- if (has_wayland) {
- sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "wayland");
- }
-$else
- // in debug mode set the video driver to X11 because renderdoc
- // doesn't support debugging in wayland yet.
- sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "x11");
- sdl::set_hint(sdl::HINT_RENDER_GPU_DEBUG, "1");
-$endif
-
- // init subsystems
- if (!sdl::init(INIT_VIDEO)) {
- unreachable("sdl error: %s", sdl::get_error());
- }
-
- // create the window
- self.win = sdl::create_window(title, width, height, WINDOW_RESIZABLE|WINDOW_VULKAN);
- if (self.win == null) {
- unreachable("sdl error: %s", sdl::get_error());
- }
-
- // get the gpu device handle
- self.gpu = sdl::create_gpu_device(GPU_SHADERFORMAT_SPIRV, true, "vulkan");
- if (self.gpu == null) {
- unreachable("failed to create gpu device: %s", sdl::get_error());
- }
-
- if (!sdl::claim_window_for_gpu_device(self.gpu, self.win)) {
- unreachable("failed to claim window for use with gpu: %s", sdl::get_error());
- }
-
- // set swapchain parameters, like vsync
- GPUPresentMode present_mode = vsync ? GPU_PRESENTMODE_VSYNC : GPU_PRESENTMODE_IMMEDIATE;
- sdl::set_gpu_swapchain_parameters(self.gpu, self.win, GPU_SWAPCHAINCOMPOSITION_SDR, present_mode);
-
- //
- // initialize the quad buffer
- // ==========================
- QuadBuffer* qb = &self.quad_buffer;
-
- // since instanced rendering is used, on the gpu there is only one mesh, a single quad.
-
- // create the vertex and index buffer on the gpu
- qb.vert_buf = sdl::create_gpu_buffer(self.gpu,
- &&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = Quad.vertices.sizeof}
- );
- if (qb.vert_buf == null) {
- unreachable("failed to initialize quad buffer (vertex): %s", sdl::get_error());
- }
-
- qb.idx_buf = sdl::create_gpu_buffer(self.gpu,
- &&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_INDEX, .size = Quad.indices.sizeof}
- );
- if (qb.idx_buf == null) {
- unreachable("failed to initialize quad buffer (index): %s", sdl::get_error());
- }
-
- qb.attr_buf = sdl::create_gpu_buffer(self.gpu,
- &&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = QuadAttributes.sizeof * MAX_QUAD_BATCH}
- );
- if (qb.attr_buf == null) {
- unreachable("failed to initialize quad buffer (index): %s", sdl::get_error());
- }
-
-
- // upload the quad mesh
- GPUTransferBuffer *ts = sdl::create_gpu_transfer_buffer(self.gpu,
- &&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = Quad.sizeof}
- );
- if (ts == null) {
- unreachable("failed to create gpu transfer buffer: %s", sdl::get_error());
- }
- Quad* quad = (Quad*)sdl::map_gpu_transfer_buffer(self.gpu, ts, false);
-
- /* v1 v4
- * +-------------+
- * | _/|
- * | _/ |
- * | 1 _/ |
- * | _/ |
- * | _/ |
- * | _/ 2 |
- * |/ |
- * +-------------+
- * v2 v3
- */
- quad.vertices.v1 = {.x = 0, .y = 0};
- quad.vertices.v2 = {.x = 0, .y = 1};
- quad.vertices.v3 = {.x = 1, .y = 1};
- quad.vertices.v4 = {.x = 1, .y = 0};
- // triangle 1 indices
- quad.indices.i1 = 0; // v1
- quad.indices.i2 = 1; // v2
- quad.indices.i3 = 3; // v4
- // triangle 2 indices
- quad.indices.i4 = 1; // v2
- quad.indices.i5 = 2; // v3
- quad.indices.i6 = 3; // v4
-
- sdl::unmap_gpu_transfer_buffer(self.gpu, ts);
-
- GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu);
- if (cmd == null) {
- unreachable("failed to upload quad at acquiring command buffer: %s", sdl::get_error());
- }
- GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
-
- // upload vertices
- sdl::upload_to_gpu_buffer(cpy,
- &&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.vertices.offsetof},
- &&(GPUBufferRegion){.buffer = qb.vert_buf, .offset = 0, .size = Quad.vertices.sizeof},
- false
- );
- // upload indices
- sdl::upload_to_gpu_buffer(cpy,
- &&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.indices.offsetof},
- &&(GPUBufferRegion){.buffer = qb.idx_buf, .offset = 0, .size = Quad.indices.sizeof},
- false
- );
-
- sdl::end_gpu_copy_pass(cpy);
- if (!sdl::submit_gpu_command_buffer(cmd)) {
- unreachable("failed to upload quads at submit command buffer: %s", sdl::get_error());
- }
- sdl::release_gpu_transfer_buffer(self.gpu, ts);
-
-
- // create and map the quad attributes transfer buffer
- qb.attr_ts = sdl::create_gpu_transfer_buffer(self.gpu,
- &&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = QuadAttributes.sizeof * MAX_QUAD_BATCH}
- );
- if (qb.attr_ts == null) {
- unreachable("failed to create gpu transfer buffer: %s", sdl::get_error());
- }
- qb.attr_ts_mapped = ((QuadAttributes*)sdl::map_gpu_transfer_buffer(self.gpu, qb.attr_ts, false))[:MAX_QUAD_BATCH];
- if (qb.attr_ts_mapped.ptr == null) {
- unreachable("failed to map vertex or index buffers: %s", sdl::get_error());
- }
-
-
- qb.initialized = true;
-}
-
-fn void Renderer.free(&self)
-{
- foreach (&s: self.shaders) {
- sdl::release_gpu_shader(self.gpu, s.frag);
- sdl::release_gpu_shader(self.gpu, s.vert);
- }
- self.shaders.free();
-
- foreach (&p: self.pipelines) {
- sdl::release_gpu_graphics_pipeline(self.gpu, p.pipeline);
- }
- self.pipelines.free();
-
- foreach (&t: self.textures) {
- sdl::release_gpu_texture(self.gpu, t.texture);
- sdl::release_gpu_sampler(self.gpu, t.sampler);
- }
- self.textures.free();
-
- QuadBuffer* qb = &self.quad_buffer;
- sdl::unmap_gpu_transfer_buffer(self.gpu, qb.attr_ts);
- sdl::release_gpu_transfer_buffer(self.gpu, qb.attr_ts);
- sdl::release_gpu_buffer(self.gpu, qb.vert_buf);
- sdl::release_gpu_buffer(self.gpu, qb.idx_buf);
- sdl::release_gpu_buffer(self.gpu, qb.attr_buf);
-
- sdl::release_window_from_gpu_device(self.gpu, self.win);
- sdl::destroy_gpu_device(self.gpu);
- sdl::destroy_window(self.win);
- sdl::quit();
-}
-
-fn void Renderer.resize_window(&self, uint width, uint height)
-{
- sdl::set_window_size(self.win, width, height);
-}
-
-fn void Renderer.get_window_size(&self, int* width, int* height)
-{
- sdl::get_window_size_in_pixels(self.win, width, height);
-}
-
-
-// Both the vertex shader and fragment shader have an implicit uniform buffer at binding 0 that
-// contains the viewport size. It is populated automatically at every begin_render() call
-fn void Renderer.load_spirv_shader_from_mem(&self, String name, char[] vert_code, char[] frag_code, uint textures, uint uniforms)
-{
- Shader s;
- s.id = name.hash();
-
- if (vert_code.len == 0 || frag_code.len == 0) {
- unreachable("vertex shader and fragment shader cannot be empty");
- }
-
- if (vert_code.len > 0) {
- // FIXME: these should be passed by parameter and/or automatically determined by parsing
- // the shader code
- GPUShaderCreateInfo shader_info = {
- .code = vert_code.ptr,
- .code_size = vert_code.len,
- .entrypoint = "main",
- .format = GPU_SHADERFORMAT_SPIRV,
- .stage = GPU_SHADERSTAGE_VERTEX,
- .num_samplers = 0,
- .num_uniform_buffers = 1+uniforms,
- .num_storage_buffers = 0,
- .num_storage_textures = 0
- };
-
- s.vert = sdl::create_gpu_shader(self.gpu, &shader_info);
- if (s.vert == null) {
- unreachable("failed to create gpu vertex shader: %s", sdl::get_error());
- }
- }
-
- if (frag_code.len > 0) {
- // FIXME: these should be passed by parameter and/or automatically determined by parsing
- // the shader code
- GPUShaderCreateInfo shader_info = {
- .code = frag_code.ptr,
- .code_size = frag_code.len,
- .entrypoint = "main",
- .format = GPU_SHADERFORMAT_SPIRV,
- .stage = GPU_SHADERSTAGE_FRAGMENT,
- .num_samplers = textures,
- .num_uniform_buffers = 1,
- .num_storage_buffers = 0,
- .num_storage_textures = 0
- };
-
- s.frag = sdl::create_gpu_shader(self.gpu, &shader_info);
- if (s.frag == null) {
- unreachable("failed to create gpu fragment shader: %s", sdl::get_error());
- }
- }
-
- // push the shader into the list
- self.shaders.push(s);
-}
-
-fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_path, String frag_path, uint textures, uint uniforms)
-{
- if (vert_path == "" || frag_path == "") {
- unreachable("need both a vertex shader and fragment shader path");
- }
-
- char[] vert_code;
- char[] frag_code;
-
- // create vertex shader
- usz size = file::get_size(vert_path)!!;
- vert_code = mem::new_array(char, size + size%4);
- file::load_buffer(vert_path, vert_code)!!;
- defer mem::free(vert_code);
-
- // create fragment shader
- size = file::get_size(frag_path)!!;
- frag_code = mem::new_array(char, size + size%4);
- file::load_buffer(frag_path, frag_code)!!;
- defer mem::free(frag_code);
-
- self.load_spirv_shader_from_mem(name, vert_code, frag_code, textures, uniforms);
-}
-
-// this describes what we want to draw, since for drawing different things we have to change
-// the GPUPrimitiveType and GPURasterizerState for the pipeline.
-enum PipelineType : (GPUPrimitiveType primitive_type, GPURasterizerState raster_state) {
- RECT = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
- SPRITE = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
- LINE = {GPU_PRIMITIVETYPE_LINELIST, {.fill_mode = GPU_FILLMODE_LINE, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}},
-}
-
-// create a graphics pipeline to draw to the window using a set of vertex/fragment shaders
-// the pipeline is pushed into the renderer's pipeline list and it will have the same id as
-// the shader set.
-fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type)
-{
- Shader *s = self.shaders.get_from_name(shader_name);
- if (s == null) {
- unreachable("error in creating pipeline: no shader named %s", shader_name);
- }
-
- GPUGraphicsPipelineCreateInfo ci = {
- .vertex_shader = s.vert,
- .fragment_shader = s.frag,
- // This structure specifies how the vertex buffer looks in memory, what it contains
- // and what is passed where to the gpu. Each vertex has three attributes, position,
- // color and uv coordinates. Since this is a 2D pixel-based renderer the position
- // is represented by two floats, the color as 32 bit rgba and the uv also as intgers.
- .vertex_input_state = {
- // the description of each vertex buffer, for now I use only one buffer
- .vertex_buffer_descriptions = (GPUVertexBufferDescription[]){
- { // first slot, per-vertex attributes
- .slot = 0,
- .pitch = Vertex.sizeof,
- .input_rate = GPU_VERTEXINPUTRATE_VERTEX,
- },
- { // second slot, per-instance attributes
- .slot = 1,
- .pitch = QuadAttributes.sizeof,
- .input_rate = GPU_VERTEXINPUTRATE_INSTANCE,
- }
- },
- .num_vertex_buffers = 2,
- // the description of each vertex
- .vertex_attributes = (GPUVertexAttribute[]){
- { // at location zero there is the position of the vertex
- .location = 0,
- .buffer_slot = 0, // buffer slot zero so per-vertex
- .format = GPU_VERTEXELEMENTFORMAT_SHORT2, // x,y
- .offset = 0,
- },
- { // at location one there is the per-quad position
- .location = 1,
- .buffer_slot = 1, // buffer slot one so per-instance
- .format = GPU_VERTEXELEMENTFORMAT_SHORT4, // x,y,w,h
- .offset = QuadAttributes.pos.offsetof,
- },
- { // at location two there are the per-quad uv coordinates
- .location = 2,
- .buffer_slot = 1,
- .format = GPU_VERTEXELEMENTFORMAT_SHORT2,
- .offset = QuadAttributes.uv.offsetof,
- },
- { // at location three there is the quad color
- .location = 3,
- .buffer_slot = 1,
- .format = GPU_VERTEXELEMENTFORMAT_UBYTE4,
- .offset = QuadAttributes.color.offsetof,
- }
- },
- .num_vertex_attributes = 4,
- },
- // the pipeline's primitive type and rasterizer state differs based on what needs to
- // be drawn
- .primitive_type = type.primitive_type,
- .rasterizer_state = type.raster_state,
- .multisample_state = {}, // no multisampling, all zeroes
- .depth_stencil_state = {}, // no stencil test, all zeroes
- .target_info = { // the target (texture) description
- .color_target_descriptions = (GPUColorTargetDescription[]){{
- // rendering happens to the window, so get it's format
- .format = sdl::get_gpu_swapchain_texture_format(self.gpu, self.win),
- .blend_state = {
- // alpha blending on everything
- // https://en.wikipedia.org/wiki/Alpha_compositing
- .src_color_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA,
- .dst_color_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
- .color_blend_op = GPU_BLENDOP_ADD,
- .src_alpha_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA,
- .dst_alpha_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
- .alpha_blend_op = GPU_BLENDOP_ADD,
- .enable_blend = true,
- // color write mask is not enabled so all rgba channels are written to
- },
- }},
- .num_color_targets = 1,
- .depth_stencil_format = {}, // FIXME: no stencil, no depth buffering
- .has_depth_stencil_target = false,
- },
- };
-
- // create the pipeline and add it to the pipeline list
- Pipeline p = {
- .id = s.id,
- .pipeline = sdl::create_gpu_graphics_pipeline(self.gpu, &ci),
- };
-
- if (p.pipeline == null) {
- unreachable("failed to create pipeline (shaders: %s, type: %s): %s", shader_name, type.nameof, sdl::get_error());
- }
-
- self.pipelines.push(p);
-}
-
-// NOTE: with TEXTUREUSAGE_SAMPLER the texture format cannot be intger _UINT so it has to be nermalized
-enum TextureType : (GPUTextureFormat format) {
- FULL_COLOR = GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
- JUST_ALPHA = GPU_TEXTUREFORMAT_R8_UNORM
-}
-
-macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixels, uint width, uint height)
-{
- $switch $typeof(name_or_id):
- $case uint: return self.new_texture_by_id(id, type, pixels, width, height);
- $case String: return self.new_texture_by_id(name_or_id.hash(), type, pixels, width, height);
- $default: unreachable("texture must have a name (String) or an id (uint)");
- $endswitch
-}
-
-macro void Renderer.update_texture(&self, name_or_id, char[] pixels, uint width, uint height, uint x = 0, uint y = 0)
-{
- $switch $typeof(name_or_id):
- $case uint: return self.update_texture_by_id(name_or_id, pixels, width, height, x, y);
- $case String: return self.update_texture_by_id(name_or_id.hash(), pixels, width, height, x, y);
- $default: unreachable("texture must have a name (String) or an id (uint)");
- $endswitch
-}
-
-
-// create a new gpu texture from a pixel buffer, the format has to be specified
-// the new texture s given an id and pushed into a texture list
-fn void Renderer.new_texture_by_id(&self, Id id, TextureType type, char[] pixels, uint width, uint height)
-{
- // the texture description
- GPUTextureCreateInfo tci = {
- .type = GPU_TEXTURETYPE_2D,
- .format = type.format,
- // all textures are used with samplers, which means read-only textures that contain data to be sampled
- .usage = GPU_TEXTUREUSAGE_SAMPLER,
- .width = width,
- .height = height,
- .layer_count_or_depth = 1,
- .num_levels = 1, // no mip maps so just one level
- // .sample_count not used since the texture is not a render target
- };
-
- GPUTexture* texture = sdl::create_gpu_texture(self.gpu, &tci);
- if (texture == null) {
- unreachable("failed to create texture (id: %s, type: %s): %s", id, type.nameof, sdl::get_error());
- }
-
- // the sampler description, how the texture should be sampled
- GPUSamplerCreateInfo sci = {
- .min_filter = GPU_FILTER_LINEAR, // linear interpolation for textures
- .mag_filter = GPU_FILTER_LINEAR,
- .mipmap_mode = GPU_SAMPLERMIPMAPMODE_NEAREST,
- .address_mode_u = GPU_SAMPLERADDRESSMODE_REPEAT, // tiling textures
- .address_mode_v = GPU_SAMPLERADDRESSMODE_REPEAT,
- .address_mode_w = GPU_SAMPLERADDRESSMODE_REPEAT,
- // everything else is not used and not needed
- };
-
- GPUSampler* sampler = sdl::create_gpu_sampler(self.gpu, &sci);
- if (sampler == null) {
- unreachable("failed to create sampler (texture id: %s, type: %s): %s", id, type.nameof, sdl::get_error());
- }
-
- Texture t = {
- .id = id,
- .texture = texture,
- .sampler = sampler,
- };
- self.textures.push(t);
-
- // upload the texture data
- self.update_texture_by_id(id, pixels, width, height, 0, 0);
-}
-
-fn void Renderer.update_texture_by_id(&self, Id id, char[] pixels, uint width, uint height, uint x, uint y)
-{
- Texture* t = self.textures.get_from_id(id);
- if (t == null || t.texture == null) {
- unreachable("failed updating texture: no texture with id %s", id);
- }
- GPUTexture* texture = t.texture;
-
- // FIXME: do a better job at validating the copy
- if (x > t.width || y > t.height) {
- unreachable("failed updating texture: attempting to copy outside of the texture region");
- }
-
- // upload image data
- GPUCommandBuffer* cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu);
- if (cmdbuf == null) {
- unreachable("failed to upload texture data at acquiring command buffer: %s", sdl::get_error());
- }
- GPUCopyPass* copypass = sdl::begin_gpu_copy_pass(cmdbuf);
- if (copypass == null) {
- unreachable("failed to upload texture data at beginning copy pass: %s", sdl::get_error());
- }
-
- GPUTransferBuffer* buf = sdl::create_gpu_transfer_buffer(self.gpu,
- &&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = pixels.len}
- );
- if (buf == null) {
- unreachable("failed to upload texture data at creating the transfer buffer: %s", sdl::get_error());
- }
-
- char* gpu_mem = (char*)sdl::map_gpu_transfer_buffer(self.gpu, buf, CYCLE);
- if (gpu_mem == null) {
- unreachable("failed to upload texture data at mapping the transfer buffer: %s", sdl::get_error());
- }
- // copy the data to the driver's memory
- gpu_mem[:pixels.len] = pixels[..];
- sdl::unmap_gpu_transfer_buffer(self.gpu, buf);
-
- // upload the data to gpu memory
- sdl::upload_to_gpu_texture(copypass,
- &&(GPUTextureTransferInfo){.transfer_buffer = buf, .offset = 0},
- &&(GPUTextureRegion){.texture = texture, .x = x, .y = y, .w = width, .h = height, .d = 1},
- false
- );
-
- sdl::end_gpu_copy_pass(copypass);
- if (!sdl::submit_gpu_command_buffer(cmdbuf)) {
- unreachable("failed to upload texture data at command buffer submission: %s", sdl::get_error());
- }
- sdl::release_gpu_transfer_buffer(self.gpu, buf);
-}
-
-
-fn bool Renderer.push_sprite(&self, short x, short y, short w, short h, short u, short v, uint color = 0xffffffff)
-{
- QuadAttributes qa = {
- .pos = {.x = x, .y = y, .w = w, .h = h},
- .uv = {.u = u, .v = v},
- .color = color
- };
-
- return self.map_quad(qa);
-}
-
-fn bool Renderer.push_quad(&self, short x, short y, short w, short h, uint color, ushort radius = 0)
-{
- QuadAttributes qa = {
- .pos = {.x = x, .y = y, .w = w, .h = h},
- .uv = {.u = radius, .v = radius},
- .color = color
- };
-
- return self.map_quad(qa);
-}
-
-// this does not upload a quad, but it simply copies the quad data to the correct transfer buffers.
-// Data transfer to the GPU only happens in draw_quads() to save time
-fn bool Renderer.map_quad(&self, QuadAttributes qa)
-{
- if (self.quad_buffer.count >= MAX_QUAD_BATCH) {
- return false;
- }
- QuadBuffer* qb = &self.quad_buffer;
-
- // upload the quad data to the gpu
- if (qb.initialized == false) {
- unreachable("quad buffer not initialized");
- }
-
- qb.attr_ts_mapped[qb.count] = qa;
-
- qb.count++;
-
- return true;
-}
-
-fn void Renderer.upload_quads(&self)
-{
- QuadBuffer* qb = &self.quad_buffer;
-
- GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu);
- if (cmd == null) {
- unreachable("failed to upload quad at acquiring command buffer: %s", sdl::get_error());
- }
- GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd);
-
- // upload quad attributes
- sdl::upload_to_gpu_buffer(cpy,
- &&(GPUTransferBufferLocation){.transfer_buffer = qb.attr_ts, .offset = 0},
- &&(GPUBufferRegion){.buffer = qb.attr_buf, .offset = 0, .size = QuadAttributes.sizeof * qb.count},
- false
- );
-
- sdl::end_gpu_copy_pass(cpy);
- if (!sdl::submit_gpu_command_buffer(cmd)) {
- unreachable("failed to upload quads at submit command buffer: %s", sdl::get_error());
- }
-}
-
-// draw all quads in the quad buffer, since uniforms are per-drawcall it makes no sense
-// to draw them one a the time
-fn void Renderer.draw_quads(&self, uint off, uint count)
-{
- QuadBuffer* qb = &self.quad_buffer;
-
- // too many quads to draw
- if (off >= qb.count || count > qb.count - off) {
- unreachable("too many quads, have %d, requested %d, offset %d", qb.count, count, off);
- }
-
- sdl::bind_gpu_vertex_buffers(self.render_pass, 0,
- (GPUBufferBinding[]){
- {.buffer = qb.vert_buf, .offset = 0},
- {.buffer = qb.attr_buf, .offset = 0},
- }, 2);
- sdl::bind_gpu_index_buffer(self.render_pass, &&(GPUBufferBinding){.buffer = qb.idx_buf, .offset = 0}, GPU_INDEXELEMENTSIZE_16BIT);
-
- sdl::draw_gpu_indexed_primitives(self.render_pass, 6, count, 0, 0, off);
-}
-
-fn void Renderer.reset_quads(&self)
-{
- self.quad_buffer.count = 0;
-}
-
-
-fn void Renderer.begin_render(&self, bool clear_screen)
-{
- self.render_cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu);
- sdl::wait_and_acquire_gpu_swapchain_texture(self.render_cmdbuf, self.win, &self.swapchain_texture, null, null);
-
- // push the window size as a uniform
- // TODO: maybe make this configurable and/or add more things
- ViewsizeUniform v;
- self.get_window_size(&v.w, &v.h);
- sdl::push_gpu_vertex_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof);
- sdl::push_gpu_fragment_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof);
-
- if (clear_screen) {
- GPURenderPass* pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
- &&(GPUColorTargetInfo){
- .texture = self.swapchain_texture,
- .mip_level = 0,
- .layer_or_depth_plane = 0,
- .clear_color = {.r = 1.0, .g = 0.0, .b = 1.0, .a = 1.0},
- .load_op = GPU_LOADOP_CLEAR, // clear the screen at the start of the render pass
- .store_op = GPU_STOREOP_STORE,
- .resolve_texture = null,
- .resolve_mip_level = 0,
- .resolve_layer = 0,
- .cycle = false,
- .cycle_resolve_texture = false
- },
- 1,
- null // huh
- );
- if (pass == null) {
- unreachable("render pass creation went wrong: %s", sdl::get_error());
- }
-
- sdl::end_gpu_render_pass(pass);
- }
-}
-
-fn void Renderer.end_render(&self)
-{
- sdl::submit_gpu_command_buffer(self.render_cmdbuf);
- self.reset_quads();
-}
-
-fn void Renderer.start_render_pass(&self, String pipeline_name)
-{
- self.render_pass = sdl::begin_gpu_render_pass(self.render_cmdbuf,
- &&(GPUColorTargetInfo){
- .texture = self.swapchain_texture,
- .mip_level = 0,
- .layer_or_depth_plane = 0,
- .clear_color = {.r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0},
- .load_op = GPU_LOADOP_DONT_CARE,
- .store_op = GPU_STOREOP_STORE,
- .resolve_texture = null,
- .resolve_mip_level = 0,
- .resolve_layer = 0,
- .cycle = false,
- .cycle_resolve_texture = false
- },
- 1,
- null // huh
- );
-
- if (self.render_pass == null) {
- unreachable("render pass creation went wrong: %s", sdl::get_error());
- }
-
- sdl::GPUGraphicsPipeline* p;
- p = self.pipelines.get_from_name(pipeline_name).pipeline;
- if (p == null) {
- unreachable("no pipeline");
- }
-
- sdl::bind_gpu_graphics_pipeline(self.render_pass, p);
-}
-
-fn void Renderer.end_render_pass(&self)
-{
- sdl::end_gpu_render_pass(self.render_pass);
-}
-
-fn void Renderer.bind_texture(&self, String texture_name)
-{
- ren::Texture* tx = self.textures.get_from_name(texture_name);
- sdl::bind_gpu_fragment_samplers(self.render_pass, 0,
- (GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1
- );
-}
-
-fn void Renderer.set_scissor(&self, int x, int y, int w, int h)
-{
- // in vulkan scissor size must be positive, clamp to zero
- w = max(w, 0);
- h = max(h, 0);
- sdl::set_gpu_scissor(self.render_pass, &&(sdl::Rect){x,y,w,h});
-}
-
-fn void Renderer.reset_scissor(&self)
-{
- int w, h;
- sdl::get_window_size(self.win, &w, &h);
- self.set_scissor(0, 0, w, h);
-}
-
-/// === NOTES ===
-/* 1. The uniform data is per-render pass. So you can do:
- * - push uniform
- * - draw 1
- * - draw 2
- * But not:
- * - push uniform
- * - draw
- * - push new uniform
- * - draw
- * And not even:
- * - draw
- * - push uniform
- * - draw
- *
- * 2. The GPU buffers are read per-command-buffer and not per
- * render pass. So I cannot override an element in the buffer
- * before submitting the command buffer.
- */
- /// === END NOTES ===
-
-
-fn void Renderer.render_ugui(&self, CmdQueue* queue)
-{
- // upload pass
- foreach (&c : queue) {
- if (c.type == CMD_RECT) {
- CmdRect r = c.rect;
- self.push_quad(r.rect.x, r.rect.y, r.rect.w, r.rect.h, r.color.to_uint(), r.radius);
- } else if (c.type == CMD_SPRITE) {
- CmdSprite s = c.sprite;
- self.push_sprite(s.rect.x, s.rect.y, s.texture_rect.w, s.texture_rect.h, s.texture_rect.x, s.texture_rect.y, s.hue.to_uint());
- }
- }
- self.upload_quads();
-
- Cmd* last_command;
- uint off;
- uint count;
- for (Cmd* cmd; (cmd = queue.dequeue() ?? null) != null;) {
- if (last_command == null || last_command.type != cmd.type) {
- self.end_command(last_command, off, count);
- self.begin_command(cmd);
- off += count;
- count = 0;
- }
-
- switch (cmd.type) {
- case CMD_RECT: nextcase;
- case CMD_SPRITE:
- count++;
- case CMD_UPDATE_ATLAS:
- // TODO: verify the correct type
- CmdUpdateAtlas u = cmd.update_atlas;
- char[] pixels = u.raw_buffer[..u.width*u.height*u.bpp];
- self.update_texture(u.id, pixels, u.width, u.height);
- case CMD_SCISSOR:
- ugui::Rect s = cmd.scissor.rect;
- if (s.x == 0 && s.y == 0 && s.w == 0 && s.h == 0) {
- self.get_window_size((int*)&s.w, (int*)&s.h);
- }
- self.scissor_x = s.x;
- self.scissor_y = s.y;
- self.scissor_w = s.w;
- self.scissor_h = s.h;
- default: unreachable("unknown command: %s", cmd.type);
- }
-
- last_command = cmd;
- }
- self.end_command(last_command, off, count);
-}
-
-fn void Renderer.begin_command(&self, Cmd* cmd)
-{
- if (cmd == null) return;
-
- switch (cmd.type) {
- case CMD_RECT:
- self.start_render_pass("UGUI_PIPELINE_RECT");
- self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
- case CMD_SPRITE:
- // TODO: support multiple sprite and font atlases
- CmdSprite s = cmd.sprite;
- String pipeline;
- String texture;
- if (s.texture_id == self.sprite_atlas_id) {
- switch (s.type) {
- case SPRITE_NORMAL: pipeline = "UGUI_PIPELINE_SPRITE";
- case SPRITE_SDF: pipeline = "UGUI_PIPELINE_SPRITE_SDF";
- case SPRITE_MSDF: pipeline = "UGUI_PIPELINE_SPRITE_MSDF";
- case SPRITE_ANIMATED: unreachable("animated sprtes are unsupported for now");
- default: unreachable("unknown sprite type %s", s.type);
- }
- texture = "icons";
- } else if (s.texture_id == self.font_atlas_id) {
- pipeline = "UGUI_PIPELINE_FONT";
- texture = "font1";
- }
- self.start_render_pass(pipeline);
- self.bind_texture(texture);
- self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h);
- case CMD_UPDATE_ATLAS: break;
- case CMD_SCISSOR: break;
- default: unreachable("unknown command: %s", cmd.type);
- }
-}
-
-fn void Renderer.end_command(&self, Cmd* cmd, uint off, uint count)
-{
- if (cmd == null) return;
-
- switch (cmd.type) {
- case CMD_RECT: nextcase;
- case CMD_SPRITE:
- self.draw_quads(off, count);
- self.end_render_pass();
- case CMD_UPDATE_ATLAS: break;
- case CMD_SCISSOR: break;
- default: unreachable("unknown command: %s", cmd.type);
- }
-}
diff --git a/lib/ugui.c3l/src/ugui_shapes.c3 b/src/shapes.c3
similarity index 56%
rename from lib/ugui.c3l/src/ugui_shapes.c3
rename to src/shapes.c3
index 9e778b7..96d9710 100644
--- a/lib/ugui.c3l/src/ugui_shapes.c3
+++ b/src/shapes.c3
@@ -1,5 +1,5 @@
module ugui;
-
+import std::math;
// ---------------------------------------------------------------------------------- //
// RECTANGLE //
@@ -10,6 +10,9 @@ 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)
{
@@ -33,11 +36,26 @@ 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)
+macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
{
return {
.x = r1.x + r2.x,
@@ -48,7 +66,7 @@ macro Rect Rect.add(Rect r1, Rect r2)
}
// returns the element-wise subtraction of r1 and r2
-macro Rect Rect.sub(Rect r1, Rect r2)
+macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
{
return {
.x = r1.x - r2.x,
@@ -59,7 +77,7 @@ macro Rect Rect.sub(Rect r1, Rect r2)
}
// returns the element-wise multiplication of r1 and r2
-macro Rect Rect.mul(Rect r1, Rect r2)
+macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*)
{
return {
.x = r1.x * r2.x,
@@ -106,7 +124,7 @@ macro Rect Rect.min(Rect a, Rect b)
}
// Offset a rect by a point
-macro Rect Rect.off(Rect r, Point p)
+macro Rect Rect.off(Rect r, Point p) @operator_s(+)
{
return {
.x = r.x + p.x,
@@ -136,6 +154,35 @@ macro Point Rect.bottom_right(Rect r)
};
}
+macro Rect Rect.center_to(Rect a, Rect b)
+{
+ return {
+ .x = b.x + (b.w - a.w)/2,
+ .y = b.y + (b.h - a.h)/2,
+ .w = a.w,
+ .h = a.h,
+ };
+}
+
+macro Rect Rect.pad(Rect a, Rect b)
+{
+ return {
+ .x = a.x + b.x,
+ .y = a.y + b.y,
+ .w = a.w - b.x - b.w,
+ .h = a.h - b.y - b.h,
+ };
+}
+
+macro Rect Rect.expand(Rect a, Rect b)
+{
+ return {
+ .x = a.x - b.x,
+ .y = a.y - b.y,
+ .w = a.w + b.x + b.w,
+ .h = a.h + b.y + b.h,
+ };
+}
// ---------------------------------------------------------------------------------- //
// POINT //
@@ -151,39 +198,16 @@ 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 Point Point.add(Point a, Point b)
-{
- return {
- .x = a.x + b.x,
- .y = a.y + b.y,
- };
-}
+macro bool Point.outside(Point p, Rect r) => !p.in_rect(r);
-macro Point Point.sub(Point a, Point b)
-{
- return {
- .x = a.x - b.x,
- .y = a.y - b.y,
- };
-}
+macro Point Point.add(Point a, Point b) @operator_s(+) => {.x = a.x+b.x, .y = a.y+b.y};
+macro Point Point.sub(Point a, Point b) @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.neg(Point p) => {-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 Point Point.max(Point a, Point b)
-{
- return {
- .x = max(a.x, b.x),
- .y = max(a.y, b.y),
- };
-}
-
-macro Point Point.min(Point a, Point b)
-{
- return {
- .x = min(a.x, b.x),
- .y = min(a.y, b.y),
- };
-}
+macro bool Point.equals(Point a, Point b) @operator_s(==) => a.x == b.x && a.y == b.y;
// ---------------------------------------------------------------------------------- //
// COLOR //
@@ -214,8 +238,29 @@ macro Color uint.@to_rgba($u)
};
}
-macro uint Color.to_uint(c)
-{
- uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
- return u;
+macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
+
+// ---------------------------------------------------------------------------------- //
+// SIZE //
+// ---------------------------------------------------------------------------------- //
+
+struct Size {
+ short min, max;
}
+
+macro Size @grow() => {.min = 0, .max = 0};
+macro Size @exact(short s) => {.min = s, .max = s};
+macro Size @fit(short min = 0, short max = short::max) => {.min = min, .max = max};
+
+macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0);
+macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0);
+macro bool Size.@is_fit(s) => (s.min != s.max);
+
+macro Size Size.add(a, Size b) @operator_s(+) => {.min = a.min.sat_add(b.min), .max = a.max.sat_add(b.max)};
+macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min.sat_sub(b.min), .max = a.max.sat_sub(b.max)};
+
+macro Size Size.combine(a, Size b) => {.min = max(a.min, b.min), .max = min(a.max, b.max)};
+macro Size Size.comb_max(a, Size b) => {.min = max(a.min, b.min), .max = max(a.max, b.max)};
+macro Size Size.comb_min(a, Size b) => {.min = min(a.min, b.min), .max = min(a.max, b.max)};
+
+macro short Size.greater(a) => a.min > a.max ? a.min : a.max;
diff --git a/lib/ugui.c3l/src/ugui_sprite.c3 b/src/sprite.c3
similarity index 58%
rename from lib/ugui.c3l/src/ugui_sprite.c3
rename to src/sprite.c3
index 724262d..2b01e65 100644
--- a/lib/ugui.c3l/src/ugui_sprite.c3
+++ b/src/sprite.c3
@@ -30,21 +30,17 @@ struct SpriteAtlas {
bool should_update;
}
-struct ElemSprite {
- Id id;
-}
-
// 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?;
+ return INVALID_TYPE~;
}
this.id = name.hash();
this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!;
- this.sprites.init(allocator::heap(), capacity: SRITES_PER_ATLAS);
+ this.sprites.init(mem, capacity: SRITES_PER_ATLAS);
this.should_update = false;
}
@@ -64,8 +60,8 @@ fn Sprite*? SpriteAtlas.insert(&this, String name, SpriteType type, char[] pixel
Point uv = this.atlas.place(pixels, w, h, stride)!;
s.w = w;
s.h = h;
- s.u = uv.x;
- s.v = uv.y;
+ s.u = (ushort)uv.x;
+ s.v = (ushort)uv.y;
this.sprites.set(s.id, s);
this.should_update = true;
return this.sprites.get_ref(s.id);
@@ -82,8 +78,8 @@ 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};
+macro Rect Sprite.rect(s) => {0, 0, (short)s.w, (short)s.h};
+macro Rect Sprite.uv(s) => {(short)s.u, (short)s.v, (short)s.w, (short)s.h};
fn void? Ctx.sprite_atlas_create(&ctx, String name, AtlasType type, ushort w, ushort h)
{
@@ -104,50 +100,8 @@ fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType t
{
QOIDesc desc;
- char[] pixels = qoi::read(allocator::heap(), path, &desc, QOIChannels.RGBA)!;
+ char[] pixels = qoi::read(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)!;
}
-
-
-macro Ctx.sprite(&ctx, String name, Point off = {0,0}, ...)
- => ctx.sprite_id(@compute_id($vasplat), name, off);
-fn void? Ctx.sprite_id(&ctx, Id id, String name, Point off)
-{
- id = ctx.gen_id(id)!;
-
- Elem *parent = ctx.get_parent()!;
- Elem *elem = ctx.get_elem(id, ETYPE_SPRITE)!;
-
- Style* style = ctx.styles.get_style(@str_hash("sprite"));
- Sprite* sprite = ctx.sprite_atlas.get(name)!;
-
- Rect uv = { sprite.u, sprite.v, sprite.w, sprite.h };
- Rect bounds = { 0, 0, sprite.w, sprite.h };
-
- elem.bounds = ctx.position_element(parent, bounds.off(off), style);
- elem.sprite.id = ctx.get_sprite_atlas_id(name);
-
- // if the bounds are null the element is outside the div view,
- // no interaction should occur so just return
- if (elem.bounds.is_null()) return;
-
- Id tex_id = ctx.sprite_atlas.id;
-
- return ctx.push_sprite(elem.bounds, uv, tex_id, parent.div.z_index)!;
-}
-
-fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds, bool center = false)
-{
- Elem *parent = ctx.get_parent()!;
- Sprite* sprite = ctx.sprite_atlas.get(name)!;
- Id tex_id = ctx.sprite_atlas.id;
-
- if (center) {
- Point off = {.x = (bounds.w - sprite.w) / 2, .y = (bounds.h - sprite.h) / 2};
- bounds = bounds.off(off);
- }
-
- return ctx.push_sprite(bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!;
-}
diff --git a/src/string.c3 b/src/string.c3
new file mode 100644
index 0000000..01b2840
--- /dev/null
+++ b/src/string.c3
@@ -0,0 +1,582 @@
+module ugui;
+
+import std::collections::list;
+import std::core::string;
+
+struct LineInfo @local {
+ sz start, end;
+ short width, height;
+ short first_off; // first character offset
+}
+
+macro sz LineInfo.len(li) => li.end-li.start;
+
+alias LineStack @local = list::List{LineInfo};
+
+fn short Rect.y_off(Rect bounds, short height, Anchor anchor)
+{
+ 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)
+{
+ 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;
+ sz 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)
+{
+ sz line_start;
+ LineInfo li;
+ Point o = self.o;
+ StringIterator ti = self.text.iterator();
+
+ sz prev_off;
+ for (Codepoint cp; ti.has_next();) {
+ cp = ti.next()!;
+ sz 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..];
+ }
+
+ sz 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 sz 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;
+
+ 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)!;
+ }
+}
+
+
+// ---------------------------------------------------------------------------------- //
+// CURSOR AND MOUSE //
+// ---------------------------------------------------------------------------------- //
+
+
+fn Rect? Ctx.get_cursor_position(&ctx, String text, Rect bounds, Anchor anchor, sz 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 sz? 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)!;
+
+ sz prev_offset = 0;
+ Point prev_o = gi.o;
+
+ while (gi.has_next()) {
+ Point o_before = gi.o;
+ sz 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, sz start, sz 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) {
+ sz 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
+ sz sel_line = -1; // selection line
+ while (gi.has_next()) {
+ Rect b = gi.next()!;
+ sz off = gi.current_offset()-1;
+ sz 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 = (usz)space_width * TAB_SIZE;
+
+ sz 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 += (short)gp.w + (short)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;
+}
diff --git a/lib/ugui.c3l/src/ugui_style.c3 b/src/style.c3
similarity index 75%
rename from lib/ugui.c3l/src/ugui_style.c3
rename to src/style.c3
index 9e9727e..5957403 100644
--- a/lib/ugui.c3l/src/ugui_style.c3
+++ b/src/style.c3
@@ -24,14 +24,14 @@ const Style DEFAULT_STYLE = {
.margin = {2, 2, 2, 2},
.border = {2, 2, 2, 2},
.padding = {1, 1, 1, 1},
- .radius = 12,
+ .radius = 0,
.size = 16,
-
- .bg = 0x282828ffu.@to_rgba(),
- .fg = 0xfbf1c7ffu.@to_rgba(),
- .primary = 0xcc241dffu.@to_rgba(),
+
+ .bg = 0x282828ffu.@to_rgba(),
+ .fg = 0xfbf1c7ffu.@to_rgba(),
+ .primary = 0xcc241dffu.@to_rgba(),
.secondary = 0x458588ffu.@to_rgba(),
- .accent = 0xfabd2fffu.@to_rgba(),
+ .accent = 0xfabd2fffu.@to_rgba(),
};
// style is stored in a hashmap, each style has an Id that can be generated by a string or whatever
@@ -60,7 +60,7 @@ 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
@@ -75,11 +75,11 @@ fn int Ctx.import_style_from_string(&ctx, String text) => ctx.styles.import_styl
fn int Ctx.import_style_from_file(&ctx, String path)
{
char[] text;
- usz size = file::get_size(path)!!;
+ sz 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;
}
@@ -90,7 +90,7 @@ fn int Ctx.import_style_from_file(&ctx, String path)
*