diff --git a/.ecode/project_build.json b/.ecode/project_build.json
new file mode 100644
index 0000000..593ac60
--- /dev/null
+++ b/.ecode/project_build.json
@@ -0,0 +1,43 @@
+{
+ "Debug": {
+ "build": [
+ {
+ "args": "-C resources/shaders",
+ "command": "make",
+ "working_dir": ""
+ },
+ {
+ "args": "build -g",
+ "command": "c3c",
+ "working_dir": ""
+ }
+ ],
+ "build_types": [],
+ "clean": [
+ {
+ "args": "clean",
+ "command": "c3c",
+ "working_dir": ""
+ }
+ ],
+ "config": {
+ "clear_sys_env": false
+ },
+ "os": [
+ "linux"
+ ],
+ "output_parser": {
+ "config": {
+ "relative_file_paths": true
+ }
+ },
+ "run": [
+ {
+ "args": "",
+ "command": "build/ugui",
+ "name": "Custom Executable",
+ "working_dir": ""
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index e69de29..74cac9a 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -0,0 +1,16 @@
+[submodule "lib/grapheme.c3l/thirdparty/libgrapheme"]
+ path = lib/grapheme.c3l/thirdparty/libgrapheme
+ url = git://git.suckless.org/libgrapheme
+ ignore = dirty
+[submodule "lib/schrift.c3l/thirdparty/libschrift"]
+ path = lib/schrift.c3l/thirdparty/libschrift
+ url = https://github.com/tomolt/libschrift
+ ignore = dirty
+[submodule "lib/sdl3.c3l"]
+ path = lib/sdl3.c3l
+ url = https://git.alemauri.eu/alema/sdl3.c3l
+ ignore = dirty
+[submodule "lib/vendor"]
+ path = lib/vendor
+ url = https://github.com/c3lang/vendor
+ ignore = dirty
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..b533650
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,184 @@
+## High level overview
+
+Under the hood every element has an id, this id allows the library to store state
+between frames.
+Elements are also cached such that when the ui tree is rebuilt at the beginning of
+every frame the element data structure doesn't have to be rebuilt.
+
+Elements are arranged in a tree, nodes are container elements that can contain other
+elements, leafs are elements that cannot contain other elements.
+
+Every element has a size and a position, containers also have to keep track of their
+layout information and some other state.
+
+Elements can push commands into the draw stack, which is a structure that contains
+all the draw commands that the user application has to perform do display the ui
+correctly, such commands include drawing lines, rectangles, sprites, text, etc.
+
+
+```text
+ +-----------+
+ | ug_init() |
+ +-----+-----+
+ |
+ |
+ |
+ +---------v----------+
+ |ug_input_keyboard() |
+ |ug_input_mouse() <----+
+ |ug_input_clipboard()| |
+ | ... | |
+ +---------+----------+ |
+ | |
+ | |
+ +-------v--------+ |
+ |ug_frame_begin()| |
+ +-------+--------+ |
+ | |
+ | |
+ +---------v----------+ |
+ |ug_window_start() | |
+ +---->ug_container_start()| |
+ | |ug_div_start() | |
+ | | ... | |
+ | +---------+----------+ |
+ | | |
+ | | |
+multiple +--------v---------+ |
+ times |ug_layout_row() | |
+ | |ug_layout_column()| |
+ | |ug_layout_float() | |
+ | | ... | |
+ | +--------+---------+ |
+ | | |
+ | | |
+ | +------v------+ |
+ | |ug_button() | |
+ | |ug_text_box()| |
+ | |ug_slider() | |
+ | | ... | |
+ | +------+------+ |
+ | | |
+ +--------------+ |
+ | |
+ +--------v---------+ |
+ |ug_window_end() | |
+ |ug_container_end()| |
+ |ug_div_end() | |
+ | ... | |
+ +--------+---------+ |
+ | |
+ | |
+ | |
+ +------v-------+ |
+ |ug_frame_end()| |
+ +------+-------+ |
+ | |
+ | |
+ | |
+ +------v-------+ |
+ |user draws the| |
+ | ui +-------+
+ +------+-------+
+ |
+ |quit
+ |
+ +------v-------+
+ | ug_destroy() |
+ +--------------+
+```
+
+### Layouting
+
+Layouting happens in a dynamic grid, when a new element is inserted in a non-floating
+manner it reserves a space in the grid, new elements are placed following this grid.
+
+Every div has two points of origin, one for the row layout and one for the column
+layout, named origin_r and origin_c respectively
+
+origin_r is used when the row layout is used and it is used to position the child
+elements one next to the other, as such it always points to the top-right edge
+of the last row element
+
+```text
+Layout: row
+#: lost space
+ Parent div
+x---------------------------------+
+|[origin_c] |
+|[origin_r] |
+| |
+| |
+| |
+| |
+| |
+| |
+| |
++---------------------------------+
+
+ Parent div
++-----x---------------------------+
+| |[origin_r] |
+| E1 | |
+| | |
+x-----+---------------------------+
+|[origin_c] |
+| | |
+| | |
+| | |
+| | |
+| | |
++-----+---------------------------+
+
+ Parent div
++-----+----------+-----x----------+
+| | E2 | |[origin_r]|
+| E1 +----------+ | |
+| |##########| E3 | |
++-----+##########| | |
+|################| | |
++----------------x-----+----------+
+| [origin_c] |
+| | |
+| | |
+| | |
++----------------+----------------+
+```
+
+TODO: handle when the content overflows the div
+- Use a different concept, like a view or relative space, for example the child
+div could have position `[0,0]` but in reality it is relative to the origin of the
+parent div
+- each div could have a view and a total area of the content, when drawing everything
+is clipped to the view and scrollbars are shown
+- individual elements accept dimensions and the x/y coordinates could be interpreted
+as offset if the layout is row/column or absolute coordinates if the leayout is floating
+
+A div can be marked resizeable or fixed, and static or dynamic. The difference being
+that resizeable adds a resize handle to the div and dynamic lets the content overflow
+causing scrollbars to be drawn
+
+### Notes
+
+How elements determine if they have focus or not
+
+```C
+// in begin_{container} code
+calculate focus
+set has_focus property
+
+
+
+// in the element code
+if(PARENT_HAS_FOCUS()) {
+ update stuff
+} else {
+ fast path to return
+}
+```
+
+How to get ids:
+ 1. use a name for each element
+ 2. supply an id for each element
+ 3. use a macro and the line position as id and then hash it
+ 4. use a macro, get the code line and hash it
diff --git a/LAYOUT b/LAYOUT
deleted file mode 100644
index 700b43d..0000000
--- a/LAYOUT
+++ /dev/null
@@ -1,243 +0,0 @@
- Div Children Alignment
-+------------------------------------------------+
-|TOP-LEFT TOP TOP-RIGHT|
-| |
-| |
-| |
-| |
-| |
-| |
-|LEFT CENTER RIGHT|
-| |
-| |
-| |
-| |
-| |
-| |
-|BOTTOM-LEFT BOTTOM BOTTOM-RIGHT|
-+------------------------------------------------+
-
-ALIGNMENT CHART:
-+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
-| TOP-LEFT, ROW: | TOP-LEFT, COLUMN: | BOTTOM, ROW: | BOTTOM, COLUMN: | TOP-RIGHT, ROW: | TOP-RIGHT, COLUMN: |
-| | | | | | |
-| +------------------------- - | +----------- - | | +----+ | | - -----------+ |
-| |+-------++-----++-----+ | |+-------+ | | | E1 | | | +-------+| |
-| || E1 || E2 || | | || E1 | | | | | | - -----------------------+ | | E1 || |
-| || |+-----+| E3 | | || | | | +----+ | +-------++-----++-----+| | | || |
-| |+-------+ | | | |+-------+ | +-------+ +---+ | +------+ | | E1 || E2 || || | +-------+| |
-| | +-----+ | |+----+ | | E1 |+------+|E3 | | | E2 | | | |+-----+| E3 || | +----+| |
-| ' | || E2 | | | || E2 || | | | | | +-------+ | || | | E2 || |
-| ' | |+----+ | +-------++------++---+ | +------+ | +-----+| | +----+| |
-| | |+---------+ | - ------------------------ - | +--+ | ' | +---------+| |
-| | || E3 | | | |E3| | ' | | E3 || |
-| | |+---------+ | | +--+ | | +---------+| |
-| | ' | | - ----------- - | | ' |
-| | ' | | | | ' |
-| | | | | | |
-+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
-| LEFT, ROW: | LEFT, COLUMN: | BOTTOM-RIGHT, ROW: | BOTTOM-RIGHT, COLUMN: | TOP, ROW: | TOP, COLUMN: |
-| | | | | | |
-| | ' | | ' | | - -------------- - |
-| ' | |+-------+ | | +-------+| | | +----------+ |
-| | +----+ | || E1 | | ' | | E1 || | - ----------------------- - | | E1 | |
-| |+------+ | | | || | | +-----+| | | || | +------++----++-----+ | | | |
-| || |+-----+| | | |+-------+ | +-------+ | || | +-------+| | | E1 || E2 || E3 | | +----------+ |
-| || E1 || E2 || E3 | | |+----+ | | E1 |+-----+| E3 || | +----+| | | |+----+| | | +--------+ |
-| || |+-----+| | | || E2 | | | || E2 || || | | E2 || | +------+ | | | | E2 | |
-| |+------+ | | | |+----+ | +-------++-----++-----+| | +----+| | +-----+ | +--------+ |
-| | +----+ | |+---------+ | - -----------------------+ | +---------+| | | +------+ |
-| ' | || E3 | | | | E3 || | | | E3 | |
-| ' | |+---------+ | | +---------+| | | | | |
-| | ' | | - -----------+ | | +------+ |
-| | ' | | | | |
-+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
-| BOTTOM-LEFT, ROW: | BOTTOM-LEFT, COLUMN: | RIGHT, ROW: | RIGHT, COLUMN: | CENTER, ROW: | CENTER, COLUMN: |
-| | | | | | |
-| | ' | | ' | | | |
-| | |+-------+ | | +-------+| | | | +-----------+ |
-| | || E1 | | ' | | E1 || | | | | E1 | |
-| ' | || | | +----+| | | || | | +----+ | | | | |
-| | +-----+ | |+-------+ | +------+ | || | +-------+| | +------+ | | | | +-----------+ |
-| |+-------+ | | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ |
-| || E1 |+-----+| E3 | | || E2 | | | E1 || E2 || E3 || | | E2 || | ---|--E1--||-E2-||-E3-|--- | ----|---E2----|---- |
-| || || E2 || | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ |
-| |+-------++-----++-----+ | |+---------+ | +------+ | || | +---------+| | +------+ | | | | +-------+ |
-| +------------------------- - | || E3 | | +----+| | | E3 || | | +----+ | | E3 | |
-| | |+---------+ | ' | +---------+| | | | | | | |
-| | +----------- - | ' | ' | | | +-------+ |
-| | | | ' | | | |
-| | | | | | |
-+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+
-
-div (
- align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER
- size_x/y: EXACT(x) | GROW() | FIT(min, max)
- scroll_x/y: true | false
- resize_x/y: true | false
- layout: ROW | COLUMN
-)
-
-align: alignment of the children elements
-size: how the div should be sized
-scroll: enables scrollbars
-layout: the layout direction of the children
-
- COLUMN ROW
- +--------------------+ +----------------------------------------------------+
- | +----------------+ | |+----------------+ |
- | | | | || |+------------+ |
- | | | | || || |+------------------+|
- | | E1 | | || E1 || E2 || E3 ||
- | | | | || || |+------------------+|
- | | | | || |+------------+ |
- | +----------------+ | |+----------------+ |
- | +------------+ | +----------------------------------------------------+
- | | | |
- | | E2 | |
- | | | | (both have center alignment)
- | +------------+ |
- |+------------------+|
- || ||
- || E3 ||
- || ||
- || ||
- |+------------------+|
- +--------------------+
-
-Element {
- id: uint
- sizing: { min_w, min_h max_w, max_h }
- bounds: { x, y, w, h }
-}
-
-id: unique identifier of the element
-sizing: the size that the element wants
-bounds: the absoulte bounds that the element got assigned
-
-Rendering
-=========
-
-Rendering happens when the element is called (immediately for leaf widgets like buttons and at the end
-for root widgets like divs). The drawing is done on the bounds assigned to the widget, these bounds
-have a one-frame delay on the current layout.
-
-The layout is calculated by each div at the end of their block and at frame end all the sizes and positions
-are assigned at frame end by iterating the element tree.
-
-ElemDiv {
- align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER
- size_x/y: { min, max }
- scroll_x/y: true | false
- layout: ROW | COLUMN
- children_size_x/y: { min, max }
-}
-
-size:
- - min != max -> FIT sizing, fit to the content but respect the min and max size
- - min == max == 0 -> GROW sizing, grow to the max amount of space possible
- - min == max != 0 -> EXACT sizing
-children_size: the size of the combined children sizes
-
-
-
-root(size_x: screen width, size_y: screen height, layout: ROW) {
-
- div1(layout: COLUMN, size_x: FIT, size_y GROW, resize_x: true) {
- E1()
- E2()
- E3()
- E4()
- } <-(end div 1)
-
- div2(size_x: GROW, size_y: GROW) {
- ...
- } <-(end div 2)
-
- div3(layout: COLUMN, size_x: FIT, size_y: GROW) {
- E5()
- E6()
- E7()
- } <-(end div 3)
-
-} <-(end root)
-(frame end)
-
-
-+-Root-Div------------------------------------------------+
-|+-Div-1----------++-Div-2--------------------++-Div-3---+|
-||+--------------+|| ||+-------+||
-||| E1 ||| ||| E5 |||
-||| ||| ||| |||
-||+--------------+|| ||+-------+|| [Root Div]
-||+--------------+|| ||+-------+|| |
-||| E2 ||| ||| E6 ||| +----------+----+-------+
-||| ||| ||| ||| v v v
-||+--------------+|| ||+-------+|| [Div 1] [Div 2] [Div 3]
-||+------+ || ||+-------+|| | |
-||| | || ||| E7 ||| +----+----+----+ |
-||| E3 | || ||| ||| v v v v |
-||| | || ||+-------+|| [E1] [E2] [E3] [E4] +----+----+
-||+------+ || || || v v v
-||+------+ || || || [E5] [E6] [E7]
-||| | || || ||
-||| E4 | || || ||
-||| | || || ||
-||+------+ || || ||
-|| || || ||
-|+----------------++--------------------------++---------+|
-+---------------------------------------------------------+
-
-the call order is as follows
-
-E1() -> updates the children size of div1
-E2() -> " "
-E3() -> " "
-E4() -> " "
-end div1() -> updates the children size of root
-end div2() -> updates the children size of root
-E5() -> updates the children size of div3
-E6() -> " "
-E7() -> " "
-end root() -> does nothing
-
-at frame end:
- * Root: the root has a size constraint of fit so the bounds get assigned the whole window
- * Div 1: the width has a size of fit, so it gets set to the children bounds, the height is set
- to the root height since it has a height of GROW
- - E1 to E4 get laid out
- * Div 2: it has a width of GROW which is **along** the layout axis, so it gets added to grow list
- the height gets set to the root height
- * Div 3: the width is FIT, so it gets set to the content width, the height gets se to the root
- height.
- - E5 to E7 get laid out
- * Div 2: is given a width (if there were other contending grow divs along the layout axis they
- would also get sized).
- - Now that div 2 has a size all it's children can be given a size
-
-Styling
-=======
-
-The element bounds include the whole CSS box model:
-
-+---------------------------------------------+
-| MARGIN |
-| +-----------------------------------+ |
-| |xxxxxxxxxxx BORDER xxxxxxxxxxxx| |
-| |x+-------------------------------+x| |
-| |x| PADDING |x| |
-| |x| +-----------------------+ |x| |
-| |x| | | |x| |
-| |x| | CONTENT | |x| |
-| |x| | | |x| |
-| |x| +-----------------------+ |x| |
-| |x| |x| |
-| |x+-------------------------------+x| |
-| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| |
-| +-----------------------------------+ |
-| |
-+---------------------------------------------+
-
-Styling happens via a .css file, the sizing strictly refers to the content, so if the the user
-requests an exact size of 100px*100px the content box will have those dimensions, but the element
-bounds will be larger.
diff --git a/LICENSE b/LICENSE
index 0a04128..e69de29 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,165 +0,0 @@
- 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
new file mode 100644
index 0000000..4f151b9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,3 @@
+test_renderer: test_renderer.c3 src/renderer.c3 resources/shaders/source/*
+ scripts/compile_shaders.sh
+ c3c compile -g -O0 test_renderer.c3 src/renderer.c3 --libdir lib --lib sdl3 --lib ugui
diff --git a/README.md b/README.md
index 151118a..e69de29 100644
--- a/README.md
+++ b/README.md
@@ -1 +0,0 @@
-Welcome to the ugui library.
diff --git a/TODO b/TODO
index f2fa0fa..96b1ba4 100644
--- a/TODO
+++ b/TODO
@@ -4,22 +4,21 @@
[x] Implement div.view and scrollbars
[x] Port font system from C to C3 (rewrite1)
[ ] Update ARCHITECTURE.md
-[x] Write a README.md
-[x] Use an arena allocator for cache
+[ ] Write a README.md
+[ ] 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
-[x] Maybe cache codepoint converted strings
+[ ] 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
@@ -29,52 +28,31 @@
[ ] .png
[ ] .jpg
[ ] gif support?
-[x] layout_set_max_rows() and layout_set_max_columns()
+[ ] layout_set_max_rows() and layout_set_max_columns()
[x] Maybe SDF sprites??
[x] Stylesheets and stylesheet import
[x] use SDF to draw anti-aliased rounded rectangles https://zed.dev/blog/videogame
[ ] Subdivide modules into ugui::ug for exported functions and ugui::core for
internal use functions (used to create widgets)
[x] The render loop RAPES the gpu, valve pls fix
-[x] The way the element structures are implemented wastes a lot of memory since
+[ ] The way the element structures are implemented wastes a lot of memory since
each struct Elem, struct Cmd, etc. is as big as the largest element. It would
be better to use a different allcation strategy.
[ ] Add a way to handle time events like double clicks
-[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the
- content, the background color is applied starting from the border. Right now push_rect() offsets
- the background rect by both border and padding
-[x] Investigate why the debug pointer (cyan rectangle) disappears...
-[ ] Selectable divs
-[ ] Selectable text
-[ ] Copy buffer
+[ ] 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
## Layout
[x] Flexbox
-[x] Center elements to the row/column
-[x] Text wrapping / reflow
-[x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct
-[x] Correct whitespace handling in text (\t \r etc)
-[x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
+[ ] For some reason padding is not correct, look at the sliders, they have 2px per side when the
+ theme specifies 4px per side
+[ ] Center elements to the row/column
+[ ] Text wrapping / reflow
+[ ] 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
@@ -82,9 +60,7 @@
[x] Mouse scroll wheel
[ ] Touch input
[x] Do not set input event to true if the movement was zero (like no mouse movement)
-[x] Use input event flags, for example to consume the input event
-[x] Fix bug in text box: when spamming keys you can get multiple characters in the text input field
- of the context, this causes a bug where only the first char is actually used
+[ ] Use input event flags, for example to consume the input event
## Commands
@@ -92,10 +68,9 @@
- border width
- border radius
[x] add a command to update an atlas
-[x] New window command, useful for popups
-[x] Text command returns the text bounds, this way we can avoid the pattern
+[ ] New window command, useful for popups
+[ ] 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
@@ -118,6 +93,11 @@
[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/linux-x64/.gitkeep b/docs/.gitkeep
similarity index 100%
rename from linux-x64/.gitkeep
rename to docs/.gitkeep
diff --git a/lib/.gitkeep b/lib/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/lib/grapheme.c3l/Makefile b/lib/grapheme.c3l/Makefile
new file mode 100644
index 0000000..09193f1
--- /dev/null
+++ b/lib/grapheme.c3l/Makefile
@@ -0,0 +1,4 @@
+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
new file mode 100644
index 0000000..6b1c7fc
--- /dev/null
+++ b/lib/grapheme.c3l/libgrapheme.c3i
@@ -0,0 +1,46 @@
+module grapheme;
+
+const uint GRAPHEME_INVALID_CODEPOINT = 0xFFFD;
+
+enum BidirectionalDirection {
+ GRAPHEME_BIDIRECTIONAL_DIRECTION_NEUTRAL,
+ GRAPHEME_BIDIRECTIONAL_DIRECTION_LTR,
+ GRAPHEME_BIDIRECTIONAL_DIRECTION_RTL,
+}
+
+fn isz bidirectional_get_line_embedding_levels(uint *, isz, ichar *, isz) @extern("grapheme_bidirectional_get_line_embedding_levels");
+
+fn isz bidirectional_preprocess_paragraph(uint *, isz, BidirectionalDirection, uint *, isz, BidirectionalDirection *) @extern("grapheme_bidirectional_preprocess_paragraph");
+
+fn isz bidirectional_reorder_line(uint *, uint *, isz, uint *, isz) @extern("grapheme_bidirectional_reorder_line");
+
+fn isz decode_utf8(char *, isz, uint *) @extern("grapheme_decode_utf8");
+fn isz encode_utf8(uint, char *, isz) @extern("grapheme_encode_utf8");
+
+fn bool is_character_break(uint, uint, ushort *) @extern("grapheme_is_character_break");
+
+fn bool is_lowercase(uint *, isz, isz *) @extern("grapheme_is_lowercase");
+fn bool is_titlecase(uint *, isz, isz *) @extern("grapheme_is_titlecase");
+fn bool is_uppercase(uint *, isz, isz *) @extern("grapheme_is_uppercase");
+
+fn bool is_lowercase_utf8(char *, isz, isz *) @extern("grapheme_is_lowercase_utf8");
+fn bool is_titlecase_utf8(char *, isz, isz *) @extern("grapheme_is_titlecase_utf8");
+fn bool is_uppercase_utf8(char *, isz, isz *) @extern("grapheme_is_uppercase_utf8");
+
+fn isz next_character_break(uint *, isz) @extern("grapheme_next_character_break");
+fn isz next_line_break(uint *, isz) @extern("grapheme_next_line_break");
+fn isz next_sentence_break(uint *, isz) @extern("grapheme_next_sentence_break");
+fn isz next_word_break(uint *, isz) @extern("grapheme_next_word_break");
+
+fn isz next_character_break_utf8(char *, isz) @extern("grapheme_next_character_break_utf8");
+fn isz next_line_break_utf8(char *, isz) @extern("grapheme_next_line_break_utf8");
+fn isz next_sentence_break_utf8(char *, isz) @extern("grapheme_next_sentence_break_utf8");
+fn isz next_word_break_utf8(char *, isz) @extern("grapheme_next_word_break_utf8");
+
+fn isz to_lowercase(uint *, isz, uint *, isz) @extern("grapheme_to_lowercase");
+fn isz to_titlecase(uint *, isz, uint *, isz) @extern("grapheme_to_titlecase");
+fn isz to_uppercase(uint *, isz, uint *, isz) @extern("grapheme_to_uppercase");
+
+fn isz to_lowercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_lowercase_utf8");
+fn isz to_titlecase_utf8(char *, isz, char *, isz) @extern("grapheme_to_titlecase_utf8");
+fn isz to_uppercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_uppercase_utf8");
diff --git a/lib/grapheme.c3l/manifest.json b/lib/grapheme.c3l/manifest.json
new file mode 100644
index 0000000..97f04d0
--- /dev/null
+++ b/lib/grapheme.c3l/manifest.json
@@ -0,0 +1,9 @@
+{
+ "provides": "grapheme",
+ "targets": {
+ "linux-x64": {
+ "dependencies": [],
+ "linked-libraries": ["grapheme", "c"]
+ }
+ }
+}
diff --git a/lib/grapheme.c3l/project.json b/lib/grapheme.c3l/project.json
new file mode 100644
index 0000000..4b2d212
--- /dev/null
+++ b/lib/grapheme.c3l/project.json
@@ -0,0 +1,14 @@
+{
+ "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
new file mode 160000
index 0000000..65b354f
--- /dev/null
+++ b/lib/grapheme.c3l/thirdparty/libgrapheme
@@ -0,0 +1 @@
+Subproject commit 65b354f0fcb1d925f4340dbb4415ea06e8af2bec
diff --git a/lib/schrift.c3l/Makefile b/lib/schrift.c3l/Makefile
new file mode 100644
index 0000000..1de46d5
--- /dev/null
+++ b/lib/schrift.c3l/Makefile
@@ -0,0 +1,4 @@
+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
new file mode 100644
index 0000000..699d296
--- /dev/null
+++ b/lib/schrift.c3l/libschrift.c3
@@ -0,0 +1,58 @@
+module schrift;
+
+alias SftFont = void*;
+alias SftUChar = uint;
+alias SftGlyph = uint;
+
+const int SFT_DOWNWARD_Y = 0x01;
+
+struct Sft
+{
+ SftFont font;
+ double xScale;
+ double yScale;
+ double xOffset;
+ double yOffset;
+ int flags;
+}
+
+struct SftLMetrics
+{
+ double ascender;
+ double descender;
+ double lineGap;
+}
+
+struct SftGMetrics
+{
+ double advanceWidth;
+ double leftSideBearing;
+ int yOffset;
+ int minWidth;
+ int minHeight;
+}
+
+struct SftKerning
+{
+ double xShift;
+ double yShift;
+}
+
+struct SftImage
+{
+ void *pixels;
+ int width;
+ int height;
+}
+
+extern fn ZString sft_version() @extern("sft_version");
+
+extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem");
+extern fn SftFont loadfile(ZString filename) @extern("sft_loadfile");
+extern fn void freefont(SftFont font) @extern("sft_freefont");
+
+extern fn int lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics");
+extern fn int lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup");
+extern fn int gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics");
+extern fn int kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning");
+extern fn int render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render");
diff --git a/lib/schrift.c3l/manifest.json b/lib/schrift.c3l/manifest.json
new file mode 100644
index 0000000..efb7f23
--- /dev/null
+++ b/lib/schrift.c3l/manifest.json
@@ -0,0 +1,9 @@
+{
+ "provides" : "schrift",
+ "targets" : {
+ "linux-x64" : {
+ "dependencies" : [],
+ "linked-libraries" : ["schrift", "c"]
+ }
+ }
+}
diff --git a/lib/schrift.c3l/project.json b/lib/schrift.c3l/project.json
new file mode 100644
index 0000000..92b049c
--- /dev/null
+++ b/lib/schrift.c3l/project.json
@@ -0,0 +1,14 @@
+{
+ "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
new file mode 160000
index 0000000..24737d2
--- /dev/null
+++ b/lib/schrift.c3l/thirdparty/libschrift
@@ -0,0 +1 @@
+Subproject commit 24737d2922b23df4a5692014f5ba03da0c296112
diff --git a/lib/sdl3.c3l b/lib/sdl3.c3l
new file mode 160000
index 0000000..076355e
--- /dev/null
+++ b/lib/sdl3.c3l
@@ -0,0 +1 @@
+Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d
diff --git a/lib/ugui.c3l/LICENSE b/lib/ugui.c3l/LICENSE
new file mode 100644
index 0000000..e69de29
diff --git a/lib/ugui.c3l/README.md b/lib/ugui.c3l/README.md
new file mode 100644
index 0000000..151118a
--- /dev/null
+++ b/lib/ugui.c3l/README.md
@@ -0,0 +1 @@
+Welcome to the ugui library.
diff --git a/lib/ugui.c3l/linux-x64/.gitkeep b/lib/ugui.c3l/linux-x64/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/manifest.json b/lib/ugui.c3l/manifest.json
similarity index 76%
rename from manifest.json
rename to lib/ugui.c3l/manifest.json
index e34f342..813ed39 100644
--- a/manifest.json
+++ b/lib/ugui.c3l/manifest.json
@@ -4,7 +4,7 @@
"targets" : {
"linux-x64" : {
"link-args" : [],
- "dependencies" : ["schrift"],
+ "dependencies" : ["schrift", "grapheme"],
"linked-libraries" : []
}
}
diff --git a/lib/ugui.c3l/scripts/.gitkeep b/lib/ugui.c3l/scripts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/cache.c3 b/lib/ugui.c3l/src/cache.c3
similarity index 74%
rename from src/cache.c3
rename to lib/ugui.c3l/src/cache.c3
index 53141a4..130ffd1 100644
--- a/src/cache.c3
+++ b/lib/ugui.c3l/src/cache.c3
@@ -1,4 +1,4 @@
-module cache;
+module cache{Key, Value, SIZE};
/* LRU Cache
* The cache uses a pool (array) to store all the elements, each element has
@@ -9,19 +9,22 @@ module cache;
* 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::alloc;
+import std::core::mem::allocator;
import std::collections::bitset;
import std::collections::map;
alias BitArr = bitset::BitSet{SIZE};
-alias IdTable = map::HashMap{Key, sz};
-alias IdTableEntry = map::Entry{Key, sz};
+alias IdTable = map::HashMap{Key, usz};
+alias IdTableEntry = map::Entry{Key, usz};
const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0);
struct Cache {
- Allocator allocator;
BitArr present, used;
IdTable table;
Value[] pool;
@@ -29,7 +32,7 @@ struct Cache {
}
// Every CACHE_CYCLES operations mark as not-present the unused elements
-macro Cache.cycle(&cache) {
+macro Cache.cycle(&cache) @private {
cache.cycle_count++;
if (cache.cycle_count > CACHE_NCYCLES) {
for (usz i = 0; i < cache.present.data.len; i++) {
@@ -40,20 +43,19 @@ macro Cache.cycle(&cache) {
}
}
-fn void? Cache.init(&cache, Allocator allocator)
+fn void? Cache.init(&cache)
{
- cache.allocator = allocator;
- cache.table.init(allocator, capacity: SIZE);
+ cache.table.init(allocator::heap(), 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 = alloc::new_array(allocator, Value, SIZE);
+ cache.pool = mem::new_array(Value, SIZE);
}
fn void Cache.free(&cache)
{
(void)cache.table.free();
- (void)alloc::free(cache.allocator, cache.pool);
+ (void)mem::free(cache.pool);
}
fn Value*? Cache.search(&cache, Key id)
@@ -64,18 +66,17 @@ 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]);
}
@@ -94,22 +95,20 @@ 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 sz Cache.get_free_spot(&cache)
+fn usz Cache.get_free_spot(&cache) @private
{
- const BITS = @bitsizeof(cache.present.data[0]);
+ const BITS = $typeof(cache.present.data[0]).sizeof*8;
foreach (idx, d: cache.present.data) {
- 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;
+ if (d.clz() != BITS) {
+ return idx*BITS + BITS-d.clz();
}
}
return 0;
}
-<* @require g != null *>
-fn Value*? Cache.insert_at(&cache, Value* g, Key id, sz index)
+fn Value*? Cache.insert_at(&cache, Value *g, Key id, usz index) @private
{
+ // TODO: verify index, g and id
Value* spot;
/* Set used and present */
@@ -124,20 +123,18 @@ fn Value*? Cache.insert_at(&cache, Value* g, Key id, sz index)
}
// Insert an element in the cache, returns the index
-<* @require g != null *>
fn Value*? Cache.insert_new(&cache, Value* g, Key id)
{
- sz index = cache.get_free_spot();
+ usz 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/lib/ugui.c3l/src/fifo.c3 b/lib/ugui.c3l/src/fifo.c3
new file mode 100644
index 0000000..d9fc52e
--- /dev/null
+++ b/lib/ugui.c3l/src/fifo.c3
@@ -0,0 +1,91 @@
+module fifo::faults;
+faultdef FULL, EMPTY;
+
+module fifo{Type};
+
+import std::core::mem;
+import std::sort;
+
+// TODO: specify the allocator
+
+struct Fifo {
+ Type[] arr;
+ usz out;
+ usz count;
+}
+
+fn void? Fifo.init(&fifo, usz size)
+{
+ fifo.arr = mem::new_array(Type, size);
+ fifo.out = 0;
+ fifo.count = 0;
+}
+
+fn void Fifo.free(&fifo)
+{
+ (void)mem::free(fifo.arr);
+}
+
+fn void? Fifo.enqueue(&fifo, Type *elem)
+{
+ if (fifo.count >= fifo.arr.len) {
+ return fifo::faults::FULL?;
+ }
+ usz in = (fifo.out + fifo.count) % fifo.arr.len;
+ fifo.arr[in] = *elem;
+ fifo.count++;
+}
+
+fn Type*? Fifo.dequeue(&fifo)
+{
+ if (fifo.count == 0) {
+ return fifo::faults::EMPTY?;
+ }
+ Type *ret = &fifo.arr[fifo.out];
+ fifo.count--;
+ fifo.out = (fifo.out + 1) % fifo.arr.len;
+ return ret;
+}
+
+macro Type Fifo.get(&fifo, usz i) @operator([])
+{
+ return fifo.arr[(fifo.out + i) % fifo.arr.len];
+}
+
+fn void Fifo.set(&fifo, usz i, Type val) @operator([]=)
+{
+ fifo.arr[(fifo.out + i) % fifo.arr.len] = val;
+}
+
+macro Type* Fifo.get_ref(&fifo, usz i) @operator(&[])
+{
+ return &fifo.arr[(fifo.out + i) % fifo.arr.len];
+}
+
+macro usz Fifo.len(&fifo) @operator(len)
+{
+ return fifo.count;
+}
+
+fn void? Fifo.sort(&fifo)
+{
+ Type[] arr = mem::new_array(Type, fifo.count);
+ defer mem::free(arr);
+
+ foreach(i, c: fifo) {
+ arr[i] = c;
+ }
+
+ // doesn't keep ordering
+ //sort::quicksort(arr);
+
+ // seems to keep the right order but we will never know...
+ // also since most things are already ordered the time is closer to O(n) than to O(n^2)
+ sort::insertionsort(arr);
+
+ fifo.count = 0;
+ fifo.out = 0;
+ foreach (&c: arr) {
+ fifo.enqueue(c)!;
+ }
+}
\ No newline at end of file
diff --git a/src/atlas.c3 b/lib/ugui.c3l/src/ugui_atlas.c3
similarity index 70%
rename from src/atlas.c3
rename to lib/ugui.c3l/src/ugui_atlas.c3
index 3bc2dbf..7f3a40b 100644
--- a/src/atlas.c3
+++ b/lib/ugui.c3l/src/ugui_atlas.c3
@@ -4,9 +4,9 @@ import std::io;
faultdef CANNOT_PLACE, INVALID_TYPE;
-enum AtlasType : (sz bpp, typeid underlying) {
- ATLAS_GRAYSCALE {1, char},
- ATLAS_R8G8B8A8 {4, uint},
+enum AtlasType {
+ ATLAS_GRAYSCALE,
+ ATLAS_R8G8B8A8,
}
// black and white atlas
@@ -21,6 +21,23 @@ 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)
@@ -35,10 +52,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
@@ -52,7 +69,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, (sz)atlas.width*atlas.height*type.bpp);
+ atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height*type.bpp());
}
fn void Atlas.free(&atlas)
@@ -86,26 +103,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 = (short)atlas.row.y + (short)atlas.row_h;
+ atlas.row.y = atlas.row.y + atlas.row_h;
atlas.row_h = 0;
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
p = atlas.row;
} else {
- return CANNOT_PLACE~;
+ return CANNOT_PLACE?;
}
}
- 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 ..];
+ usz bpp = atlas.type.bpp();
+ for (usz y = 0; y < h; y++) {
+ for (usz x = 0; x < w; x++) {
+ char[] buf = atlas.buffer[(usz)(p.y+y)*atlas.width*bpp + (p.x+x)*bpp ..];
+ char[] pix = pixels[(usz)y*stride*bpp + x*bpp ..];
buf[0..bpp-1] = pix[0..bpp-1];
}
}
- atlas.row.x += (short)w;
+ atlas.row.x += w;
if (h > atlas.row_h) {
atlas.row_h = h;
}
diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3
new file mode 100644
index 0000000..344cea5
--- /dev/null
+++ b/lib/ugui.c3l/src/ugui_button.c3
@@ -0,0 +1,187 @@
+module ugui;
+
+import std::io;
+
+// button element
+struct ElemButton {
+ int filler;
+}
+
+
+// draw a button, return the events on that button
+macro Ctx.button(&ctx, Rect size, bool state = false, ...)
+ => ctx.button_id(@compute_id($vasplat), size, state);
+fn ElemEvents? Ctx.button_id(&ctx, Id id, Rect size, bool active)
+{
+ id = ctx.gen_id(id)!;
+
+ Elem *parent = ctx.get_parent()!;
+ Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
+ Style* style_norm = ctx.styles.get_style(@str_hash("button"));
+
+ elem.bounds = ctx.position_element(parent, size, style_norm);
+
+ // if the bounds are null the element is outside the div view,
+ // no interaction should occur so just return
+ if (elem.bounds.is_null()) { return {}; }
+
+ Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
+
+ elem.events = ctx.get_elem_events(elem);
+ bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
+
+ // Draw the button
+ ctx.push_rect(elem.bounds, parent.div.z_index, is_active ? style_active : style_norm)!;
+
+ return elem.events;
+}
+
+macro Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool active = false, ...)
+ => ctx.button_label_id(@compute_id($vasplat), label, size, active);
+fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool active)
+{
+ id = ctx.gen_id(id)!;
+
+ Elem *parent = ctx.get_parent()!;
+ Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
+
+ short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
+ Rect text_size = ctx.get_text_bounds(label)!;
+ Rect btn_size = text_size.add({0,0,10,10});
+ Style* style_norm = ctx.styles.get_style(@str_hash("button"));
+
+ // 2. Layout
+ elem.bounds = ctx.position_element(parent, btn_size, style_norm);
+ if (elem.bounds.is_null()) { return {}; }
+
+ Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
+
+ elem.events = ctx.get_elem_events(elem);
+
+ bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
+ Style* style = is_active ? style_active : style_norm;
+
+ // Draw the button
+ text_size.x = elem.bounds.x;
+ text_size.y = elem.bounds.y;
+ Point off = ctx.center_text(text_size, elem.bounds);
+ text_size.x += off.x;
+ text_size.y += off.y;
+ ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
+ ctx.push_string(text_size, label, parent.div.z_index, style.fg)!;
+
+ return elem.events;
+}
+
+macro Ctx.button_icon(&ctx, String icon, String on_icon = "", bool active = false, ...)
+ => ctx.button_icon_id(@compute_id($vasplat), icon, on_icon, active);
+fn ElemEvents? Ctx.button_icon_id(&ctx, Id id, String icon, String on_icon, bool active)
+{
+ id = ctx.gen_id(id)!;
+
+ Elem *parent = ctx.get_parent()!;
+ Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
+
+ Sprite* def_sprite = ctx.sprite_atlas.get(icon)!;
+ Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){};
+ Rect max_size = def_sprite.rect().max(on_sprite.rect());
+
+ Style* style_norm = ctx.styles.get_style(@str_hash("button"));
+
+ elem.bounds = ctx.position_element(parent, max_size, style_norm);
+
+ // if the bounds are null the element is outside the div view,
+ // no interaction should occur so just return
+ if (elem.bounds.is_null()) { return {}; }
+
+ Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
+
+ elem.events = ctx.get_elem_events(elem);
+
+ bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover;
+ Style* style = is_active ? style_active : style_norm;
+
+ Id tex_id = ctx.sprite_atlas.id;
+ if (active && on_icon != "") {
+ ctx.push_sprite(elem.bounds, on_sprite.uv(), tex_id, parent.div.z_index, type: on_sprite.type)!;
+ } else {
+ ctx.push_sprite(elem.bounds, def_sprite.uv(), tex_id, parent.div.z_index, type: def_sprite.type)!;
+ }
+
+ // Draw the button
+ ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
+
+ return elem.events;
+}
+
+// FIXME: this should be inside the style
+macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...)
+ => ctx.checkbox_id(@compute_id($vasplat), desc, off, active, tick_sprite);
+fn void? Ctx.checkbox_id(&ctx, Id id, String description, Point off, bool* active, String tick_sprite)
+{
+ id = ctx.gen_id(id)!;
+
+ Elem *parent = ctx.get_parent()!;
+ Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
+ Style* style = ctx.styles.get_style(@str_hash("checkbox"));
+
+ Rect size = {off.x, off.y, style.size, style.size};
+ elem.bounds = ctx.position_element(parent, size, style);
+
+ // if the bounds are null the element is outside the div view,
+ // no interaction should occur so just return
+ if (elem.bounds.is_null()) return;
+
+ elem.events = ctx.get_elem_events(elem);
+ if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
+
+ if (tick_sprite != {}) {
+ ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
+ if (*active) {
+ ctx.draw_sprite_raw(tick_sprite, elem.bounds, center: true)!;
+ }
+ } else {
+ ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
+ if (*active) {
+ ushort x = style.size / 4;
+ Rect check = elem.bounds.add({x, x, -x*2, -x*2});
+ Style s = *style;
+ s.bg = s.primary;
+ s.margin = s.padding = {};
+ s.border = 0;
+ ctx.push_rect(check, parent.div.z_index, &s)!;
+ }
+ }
+}
+
+// FIXME: this should be inside the style
+macro Ctx.toggle(&ctx, String desc, Point off, bool* active)
+ => ctx.toggle_id(@compute_id($vasplat), desc, off, active);
+fn void? Ctx.toggle_id(&ctx, Id id, String description, Point off, bool* active)
+{
+ id = ctx.gen_id(id)!;
+
+ Elem *parent = ctx.get_parent()!;
+ Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
+ Style* style = ctx.styles.get_style(@str_hash("toggle"));
+
+ Rect size = {off.x, off.y, style.size*2, style.size};
+ elem.bounds = ctx.position_element(parent, size, style);
+
+ // if the bounds are null the element is outside the div view,
+ // no interaction should occur so just return
+ if (elem.bounds.is_null()) return;
+
+ elem.events = ctx.get_elem_events(elem);
+ if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active);
+
+ // Draw the button
+ // FIXME: THIS IS SHIT
+ ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
+ Rect t = elem.bounds.add({*active ? (style.size+3) : +3, +3, -style.size-6, -6});
+ Style s = *style;
+ s.bg = s.primary;
+ s.margin = s.padding = {};
+ s.border = 0;
+ ctx.push_rect(t, parent.div.z_index, &s)!;
+}
diff --git a/lib/ugui.c3l/src/ugui_cmd.c3 b/lib/ugui.c3l/src/ugui_cmd.c3
new file mode 100644
index 0000000..0247e7a
--- /dev/null
+++ b/lib/ugui.c3l/src/ugui_cmd.c3
@@ -0,0 +1,209 @@
+module ugui;
+
+import std::ascii;
+import std::io;
+
+// command type
+enum CmdType {
+ CMD_RECT,
+ CMD_UPDATE_ATLAS,
+ CMD_SPRITE,
+ CMD_SCISSOR,
+}
+
+// command to draw a rect
+struct CmdRect {
+ Rect rect;
+ ushort thickness;
+ ushort radius;
+ Color color;
+}
+
+struct CmdUpdateAtlas {
+ Id id;
+ char* raw_buffer;
+ short width, height, bpp;
+}
+
+struct CmdSprite {
+ Id texture_id;
+ SpriteType type;
+ Rect rect;
+ Rect texture_rect;
+ Color hue;
+}
+
+// if rect is zero Rect{0} then reset the scissor
+struct CmdScissor {
+ Rect rect;
+}
+
+// command structure
+struct Cmd (Printable) {
+ CmdType type;
+ int z_index;
+ union {
+ CmdRect rect;
+ CmdUpdateAtlas update_atlas;
+ CmdSprite sprite;
+ CmdScissor scissor;
+ }
+}
+
+fn int Cmd.compare_to(Cmd a, Cmd b)
+{
+ if (a.z_index == b.z_index) return 0;
+ return a.z_index > b.z_index ? 1 : -1;
+}
+
+// implement the Printable interface
+fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic
+{
+ return f.printf("Cmd{ type: %s, z_index: %d }", cmd.type, cmd.z_index);
+}
+
+macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max})
+{
+ bool no_area = rect.w <= 0 || rect.h <= 0;
+ return no_area || !rect.collides(clip);
+}
+
+// FIXME: this whole thing could be done at compile time, maybe
+macro Ctx.push_cmd(&ctx, Cmd *cmd, int z_index)
+{
+ cmd.z_index = z_index;
+ Rect rect;
+ switch (cmd.type) {
+ case CMD_RECT: rect = cmd.rect.rect;
+ case CMD_SPRITE: rect = cmd.sprite.rect;
+ default: return ctx.cmd_queue.enqueue(cmd);
+ }
+ if (cull_rect(rect, ctx.div_scissor)) return;
+ return ctx.cmd_queue.enqueue(cmd);
+}
+
+fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index)
+{
+ Cmd sc = {
+ .type = CMD_SCISSOR,
+ .scissor.rect = rect.intersection(ctx.div_scissor),
+ };
+ ctx.push_cmd(&sc, z_index)!;
+}
+
+fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style)
+{
+ Rect padding = style.padding;
+ ushort border = style.border;
+ ushort radius = style.radius;
+ Color bg = style.bg;
+ Color border_color = style.secondary;
+
+ if (border != 0) {
+ Cmd cmd = {
+ .type = CMD_RECT,
+ .rect.rect = rect,
+ .rect.color = border_color,
+ .rect.radius = radius+border,
+ .rect.thickness = border,
+ };
+ ctx.push_cmd(&cmd, z_index)!;
+ }
+
+ Cmd cmd = {
+ .type = CMD_RECT,
+ .rect.rect = {
+ .x = rect.x + border + padding.x,
+ .y = rect.y + border + padding.y,
+ .h = rect.h - (border*2) - (padding.y+padding.h),
+ .w = rect.w - (border*2) - (padding.x+padding.w),
+ },
+ .rect.color = bg,
+ .rect.radius = radius,
+ .rect.thickness = max(rect.w, rect.h)/2+1,
+ };
+ if (cull_rect(cmd.rect.rect, ctx.div_scissor)) return;
+ ctx.push_cmd(&cmd, z_index)!;
+}
+
+fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL)
+{
+ Cmd cmd = {
+ .type = CMD_SPRITE,
+ .sprite.type = type,
+ .sprite.rect = bounds,
+ .sprite.texture_rect = texture,
+ .sprite.texture_id = texture_id,
+ .sprite.hue = hue,
+ };
+ ctx.push_cmd(&cmd, z_index)!;
+}
+
+fn void? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue)
+{
+ if (text.len == 0) {
+ return;
+ }
+
+ ctx.push_scissor(bounds, z_index)!;
+
+ short baseline = (short)ctx.font.ascender;
+ short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
+ short line_gap = (short)ctx.font.linegap;
+ Id texture_id = ctx.font.id; // or ctx.font.atlas.id
+ Point orig = {
+ .x = bounds.x,
+ .y = bounds.y,
+ };
+
+ short line_len;
+ Codepoint cp;
+ usz off, x;
+ while (off < text.len && (cp = str_to_codepoint(text[off..], &x)) != 0) {
+ off += x;
+ Glyph* gp;
+ if (!ascii::is_cntrl((char)cp)) {
+ gp = ctx.font.get_glyph(cp)!;
+ Rect gb = {
+ .x = orig.x + line_len + gp.ox,
+ .y = orig.y + gp.oy + baseline,
+ .w = gp.w,
+ .h = gp.h,
+ };
+ Rect gt = {
+ .x = gp.u,
+ .y = gp.v,
+ .w = gp.w,
+ .h = gp.h,
+ };
+ // push the sprite only if it collides with the bounds
+ if (!cull_rect(gb, bounds)) ctx.push_sprite(gb, gt, texture_id, z_index, hue)!;
+ line_len += gp.adv;
+ } else if (cp == '\n'){
+ orig.y += line_height + line_gap;
+ line_len = 0;
+ } else {
+ continue;
+ }
+ }
+
+ // FIXME: we never get here if an error was thrown before
+ ctx.push_scissor({}, z_index)!;
+}
+
+fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
+{
+ Cmd up = {
+ .type = CMD_UPDATE_ATLAS,
+ .update_atlas = {
+ .id = atlas.id,
+ .raw_buffer = atlas.buffer,
+ .width = atlas.width,
+ .height = atlas.height,
+ .bpp = (ushort)atlas.type.bpp(),
+ },
+ };
+ // update the atlases before everything else
+ ctx.push_cmd(&up, -1)!;
+}
+
diff --git a/lib/ugui.c3l/src/ugui_core.c3 b/lib/ugui.c3l/src/ugui_core.c3
new file mode 100644
index 0000000..d25385b
--- /dev/null
+++ b/lib/ugui.c3l/src/ugui_core.c3
@@ -0,0 +1,330 @@
+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
new file mode 100644
index 0000000..8b9e292
--- /dev/null
+++ b/lib/ugui.c3l/src/ugui_div.c3
@@ -0,0 +1,153 @@
+module ugui;
+
+import std::io;
+import std::math;
+
+// div element
+struct ElemDiv {
+ Layout layout;
+ struct scroll_x {
+ bool enabled;
+ bool on;
+ float value;
+ }
+ struct scroll_y {
+ bool enabled;
+ bool on;
+ float value;
+ }
+ ushort scroll_size;
+ int z_index;
+ Rect children_bounds; // current frame children bounds
+ Rect pcb; // previous frame children bounds
+ Point origin_r, origin_c;
+}
+
+// begin a widget container, or div, the size determines the offset (x,y) width and height.
+// if the width or height are zero the width or height are set to the maximum available.
+// if the width or height are negative the width or height will be calculated based on the children size
+// sort similar to a flexbox, and the minimum size is set by the negative of the width or height
+// FIXME: there is a bug if the size.w or size.h == -0
+macro Ctx.div_begin(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...)
+ => ctx.div_begin_id(@compute_id($vasplat), size, scroll_x, scroll_y);
+fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y)
+{
+ id = ctx.gen_id(id)!;
+
+ Elem* parent = ctx.get_parent()!;
+ Elem* elem = ctx.get_elem(id, ETYPE_DIV)!;
+ ctx.active_div = elem.tree_idx;
+
+ Style* style = ctx.styles.get_style(@str_hash("default"));
+ Style* slider_style = ctx.styles.get_style(@str_hash("slider"));
+
+ elem.div.scroll_x.enabled = scroll_x;
+ elem.div.scroll_y.enabled = scroll_y;
+ elem.div.scroll_size = slider_style.size ? slider_style.size : (style.size ? style.size : DEFAULT_STYLE.size);
+ elem.div.z_index = parent.div.z_index + 1;
+
+ // 2. layout the element
+ Rect wanted_size = {
+ .x = size.x,
+ .y = size.y,
+ .w = size.w < 0 ? max(elem.div.pcb.w, (short)-size.w) : size.w,
+ .h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h,
+ };
+ elem.bounds = ctx.position_element(parent, wanted_size, style);
+ elem.div.children_bounds = {};
+
+ // update the ctx scissor
+ ctx.div_scissor = elem.bounds;
+ ctx.push_scissor(elem.bounds, elem.div.z_index)!;
+
+ // 4. Fill the div fields
+ elem.div.origin_c = {
+ .x = elem.bounds.x,
+ .y = elem.bounds.y
+ };
+ elem.div.origin_r = elem.div.origin_c;
+ elem.div.layout = parent.div.layout;
+
+ // Add the background to the draw stack
+ bool do_border = parent.div.layout == LAYOUT_FLOATING;
+ ctx.push_rect(elem.bounds, elem.div.z_index, style)!;
+
+ elem.events = ctx.get_elem_events(elem);
+
+ // TODO: check active
+ // TODO: check resizeable
+}
+
+fn void? Ctx.div_end(&ctx)
+{
+ // swap the children bounds
+ Elem* parent = ctx.get_parent()!;
+ Elem* elem = ctx.get_active_div()!;
+ elem.div.pcb = elem.div.children_bounds;
+
+ // FIXME: this causes all elements inside the div to loose focus since the mouse press happens
+ // both inside the element and inside the div bounds
+ //elem.events = ctx.get_elem_events(elem);
+
+ Rect cb = elem.div.pcb;
+ // children bounds bottom-right corner
+ Point cbc = {
+ .x = cb.x + cb.w,
+ .y = cb.y + cb.h,
+ };
+ // div bounds bottom-right corner
+ Point bc = {
+ .x = elem.bounds.x + elem.bounds.w,
+ .y = elem.bounds.y + elem.bounds.h,
+ };
+
+ // set the scrollbar flag, is used in layout
+ // horizontal overflow
+ elem.div.scroll_x.on = cbc.x > bc.x && elem.div.scroll_x.enabled;
+ // vertical overflow
+ elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled;
+
+ Id hsid_raw = @str_hash("div_scrollbar_horizontal");
+ Id vsid_raw = @str_hash("div_scrollbar_vertical");
+ Id hsid_real = ctx.gen_id(@str_hash("div_scrollbar_horizontal"))!;
+ Id vsid_real = ctx.gen_id(@str_hash("div_scrollbar_vertical"))!;
+ short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid_real || ctx.is_hovered(ctx.find_elem(vsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
+ short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid_real || ctx.is_hovered(ctx.find_elem(hsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0;
+
+ if (elem.div.scroll_y.on) {
+ if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
+ elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f;
+ elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f);
+ }
+ Rect vslider = {
+ .x = elem.bounds.x + elem.bounds.w - wdim,
+ .y = elem.bounds.y,
+ .w = wdim,
+ .h = elem.bounds.h - hdim,
+ };
+ Layout prev_l = elem.div.layout;
+ elem.div.layout = LAYOUT_ABSOLUTE;
+ ctx.slider_ver_id(vsid_raw, vslider, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!;
+ elem.div.layout = prev_l;
+ }
+
+ if (elem.div.scroll_x.on) {
+ if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) {
+ elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f;
+ elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f);
+ }
+ Rect hslider = {
+ .x = elem.bounds.x,
+ .y = elem.bounds.y + elem.bounds.h - hdim,
+ .w = elem.bounds.w - wdim,
+ .h = hdim,
+ };
+ Layout prev_l = elem.div.layout;
+ elem.div.layout = LAYOUT_ABSOLUTE;
+ ctx.slider_hor_id(hsid_raw, hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!;
+ elem.div.layout = prev_l;
+ }
+
+ // the active_div returns to the parent of the current one
+ ctx.active_div = ctx.tree.parentof(ctx.active_div)!;
+}
diff --git a/src/font.c3 b/lib/ugui.c3l/src/ugui_font.c3
similarity index 58%
rename from src/font.c3
rename to lib/ugui.c3l/src/ugui_font.c3
index 0bc233e..020ed75 100644
--- a/src/font.c3
+++ b/lib/ugui.c3l/src/ugui_font.c3
@@ -1,26 +1,19 @@
-module ugui::font;
+module ugui;
import schrift;
+import grapheme;
import std::collections::map;
import std::core::mem;
import std::core::mem::allocator;
import std::io;
-import std::core::ascii;
+import std::ascii;
-// ---------------------------------------------------------------------------------- //
-// CODEPOINT //
-// ---------------------------------------------------------------------------------- //
-
// unicode code point, different type for a different hash
alias Codepoint = uint;
//macro uint Codepoint.hash(self) => ((uint)self).hash();
-// ---------------------------------------------------------------------------------- //
-// FONT ATLAS //
-// ---------------------------------------------------------------------------------- //
-
/* width and height of a glyph contain the kering advance
* (u,v)
* +-------------*---+ -
@@ -65,18 +58,9 @@ struct Font {
bool should_update; // should send update_atlas command, resets at frame_end()
}
-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)
+fn void? Font.load(&font, String name, ZString path, uint height, float scale)
{
- font.table.init(allocator, capacity: FONT_CACHED);
+ font.table.init(allocator::heap(), capacity: FONT_CACHED);
font.id = name.hash();
font.size = height*scale;
@@ -90,7 +74,7 @@ fn void? Font.load(&font, Allocator allocator, String name, ZString path, uint h
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;
@@ -100,19 +84,17 @@ fn void? Font.load(&font, Allocator allocator, String name, ZString path, uint h
font.linegap = (float)lmetrics.lineGap;
//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);
+ // TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
+ // like the letter 'A'
ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED));
font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!;
// preallocate the ASCII range
for (char c = ' '; c < '~'; c++) {
- // FIXME: without @inline, this crashes with O1 or greater
- font.get_glyph((Codepoint)c) @inline!;
+ font.get_glyph((Codepoint)c)!;
}
}
-<*
-@param [&inout] font
-*>
fn Glyph*? Font.get_glyph(&font, Codepoint code)
{
Glyph*? gp;
@@ -120,13 +102,12 @@ 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;
@@ -134,21 +115,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 * (usz)img.height);
+ char[] pixels = mem::new_array(char, (usz)img.width * img.height);
img.pixels = pixels;
if (schrift::render(&font.sft, gid, img) < 0) {
- return RENDER_ERROR~;
+ return RENDER_ERROR?;
}
glyph.code = code;
@@ -161,8 +142,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 = (ushort)uv.x;
- glyph.v = (ushort)uv.y;
+ glyph.u = uv.x;
+ glyph.v = uv.y;
mem::free(pixels);
@@ -172,9 +153,6 @@ 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();
@@ -182,44 +160,107 @@ fn void Font.free(&font)
schrift::freefont(font.sft.font);
}
-
-// ---------------------------------------------------------------------------------- //
-// FONT LOAD AND QUERY //
-// ---------------------------------------------------------------------------------- //
-
-module ugui;
-
-<*
-@param [&inout] ctx
-@param [in] name
-@param [&in] path
-@require height > 0, scale > 0: "height and scale must be positive non-zero"
-*>
-fn void? Ctx.load_font(&ctx, Allocator allocator, String name, ZString path, uint height, float scale = 1.0)
+fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0)
{
- return ctx.font.load(allocator, name, path, height, scale);
+ return ctx.font.load(name, path, height, scale);
}
<*
-@param [&in] ctx
-@param [in] label
-@require ctx.font.id != 0 : "font not loaded"
+@require off != null
*>
-// TODO: check if the font is present in the context
-fn Id Ctx.get_font_id(&ctx, String label) => (Id)ctx.font.id;
+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();
+}
-<*
-@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/lib/ugui.c3l/src/ugui_input.c3 b/lib/ugui.c3l/src/ugui_input.c3
new file mode 100644
index 0000000..ac70110
--- /dev/null
+++ b/lib/ugui.c3l/src/ugui_input.c3
@@ -0,0 +1,204 @@
+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
new file mode 100644
index 0000000..84eaf8b
--- /dev/null
+++ b/lib/ugui.c3l/src/ugui_layout.c3
@@ -0,0 +1,196 @@
+module ugui;
+
+enum Layout {
+ LAYOUT_ROW,
+ LAYOUT_COLUMN,
+ LAYOUT_FLOATING,
+ LAYOUT_ABSOLUTE,
+}
+
+fn void? Ctx.layout_set_row(&ctx)
+{
+ Id parent_id = ctx.tree.get(ctx.active_div)!;
+ Elem *parent = ctx.cache.search(parent_id)!;
+
+ if (parent.type != ETYPE_DIV) {
+ // what?
+ return UNEXPECTED_ELEMENT?;
+ }
+
+ parent.div.layout = LAYOUT_ROW;
+}
+
+fn void? Ctx.layout_set_column(&ctx)
+{
+ Id parent_id = ctx.tree.get(ctx.active_div)!;
+ Elem *parent = ctx.cache.search(parent_id)!;
+
+ if (parent.type != ETYPE_DIV) {
+ // what?
+ return UNEXPECTED_ELEMENT?;
+ }
+
+ parent.div.layout = LAYOUT_COLUMN;
+}
+
+fn void? Ctx.layout_set_floating(&ctx)
+{
+ Id parent_id = ctx.tree.get(ctx.active_div)!;
+ Elem *parent = ctx.cache.search(parent_id)!;
+
+ if (parent.type != ETYPE_DIV) {
+ // what?
+ return UNEXPECTED_ELEMENT?;
+ }
+
+ parent.div.layout = LAYOUT_FLOATING;
+}
+
+fn void? Ctx.layout_next_row(&ctx)
+{
+ Id parent_id = ctx.tree.get(ctx.active_div)!;
+ Elem *parent = ctx.cache.search(parent_id)!;
+
+ if (parent.type != ETYPE_DIV) {
+ return UNEXPECTED_ELEMENT?;
+ }
+
+ parent.div.origin_r = {
+ .x = parent.bounds.x,
+ .y = parent.div.children_bounds.bottom_right().y,
+ };
+ parent.div.origin_c = parent.div.origin_r;
+}
+
+fn void? Ctx.layout_next_column(&ctx)
+{
+ Id parent_id = ctx.tree.get(ctx.active_div)!;
+ Elem *parent = ctx.cache.search(parent_id)!;
+
+ if (parent.type != ETYPE_DIV) {
+ return UNEXPECTED_ELEMENT?;
+ }
+
+ parent.div.origin_c = {
+ .x = parent.div.children_bounds.bottom_right().x,
+ .y = parent.bounds.y,
+ };
+ parent.div.origin_r = parent.div.origin_c;
+}
+
+
+macro Rect Elem.get_view(&elem)
+{
+ Rect off;
+ if (elem.div.scroll_x.enabled && elem.div.scroll_x.on) {
+ off.x = (short)((float)(elem.div.pcb.w - elem.bounds.w) * elem.div.scroll_x.value);
+ off.w = -elem.div.scroll_size;
+ }
+ if (elem.div.scroll_y.enabled && elem.div.scroll_y.on) {
+ off.y = (short)((float)(elem.div.pcb.h - elem.bounds.h) * elem.div.scroll_y.value);
+ off.h = -elem.div.scroll_size;
+ }
+ return elem.bounds.add(off);
+}
+
+macro Point Elem.get_view_off(&elem)
+{
+ return elem.get_view().sub(elem.bounds).position();
+}
+
+// position the rectangle inside the parent according to the layout
+// parent: parent div
+// rect: the requested size
+// style: apply style
+<*
+@require ctx != null
+@require parent.type == ETYPE_DIV
+*>
+fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, Style* style)
+{
+ ElemDiv* div = &parent.div;
+
+ Rect parent_bounds, parent_view;
+ Rect child_placement, child_occupied;
+
+ // 1. Select the right origin
+ Point origin;
+ switch (div.layout) {
+ case LAYOUT_ROW:
+ origin = div.origin_r;
+ case LAYOUT_COLUMN:
+ origin = div.origin_c;
+ case LAYOUT_FLOATING: // none, relative to zero zero
+ case LAYOUT_ABSOLUTE: // absolute position, this is a no-op, return the rect
+ return rect;
+ default: // error
+ return {};
+ }
+
+ // 2. Compute the parent's view
+ parent_bounds = parent.bounds;
+ parent_view = parent.get_view();
+
+ // 3. Compute the placement and occupied area
+
+ // grow rect (wanted size) when widht or height are less than zero
+ bool adapt_x = rect.w <= 0;
+ bool adapt_y = rect.h <= 0;
+ if (adapt_x) rect.w = parent_bounds.w - parent_bounds.x - origin.x;
+ if (adapt_y) rect.h = parent_bounds.h - parent_bounds.y - origin.y;
+
+ // offset placement and area
+ child_placement = child_placement.off(origin.add(rect.position()));
+ child_occupied = child_occupied.off(origin.add(rect.position()));
+
+ Rect margin = style.margin;
+ Rect padding = style.padding;
+ ushort border = style.border;
+
+ // padding, grows both the placement and occupied area
+ child_placement = child_placement.grow(padding.position().add(padding.size()));
+ child_occupied = child_occupied.grow(padding.position().add(padding.size()));
+ // border, grows both the placement and occupied area
+ child_placement = child_placement.grow({border*2, border*2});
+ child_occupied = child_occupied.grow({border*2, border*2});
+ // margin, offsets the placement and grows the occupied area
+ child_placement = child_placement.off(margin.position());
+ child_occupied = child_occupied.grow(margin.position().add(margin.size()));
+
+ // oh yeah also adjust the rect if i was to grow
+ if (adapt_x) rect.w -= padding.x+padding.w + border*2 + margin.x+margin.w;
+ if (adapt_y) rect.h -= padding.y+padding.h + border*2 + margin.y+margin.h;
+
+ // set the size
+ child_placement = child_placement.grow(rect.size());
+ child_occupied = child_occupied.grow(rect.size());
+
+ // 4. Update the parent's origin
+ div.origin_r = {
+ .x = child_occupied.bottom_right().x,
+ .y = origin.y,
+ };
+ div.origin_c = {
+ .x = origin.x,
+ .y = child_occupied.bottom_right().y,
+ };
+
+ // 5. Update the parent's children bounds
+ if (!child_occupied.bottom_right().in_rect(div.children_bounds)) {
+ // right overflow
+ if (child_occupied.bottom_right().x > div.children_bounds.bottom_right().x) {
+ div.children_bounds.w += child_occupied.bottom_right().x - div.children_bounds.bottom_right().x;
+ }
+ // bottom overflow
+ if (child_occupied.bottom_right().y > div.children_bounds.bottom_right().y) {
+ div.children_bounds.h += child_occupied.bottom_right().y - div.children_bounds.bottom_right().y;
+ }
+ }
+
+ // 99. return the placement
+ if (child_placement.collides(parent_view)) {
+ return child_placement.off(parent.get_view_off().neg());
+ } else {
+ return {};
+ }
+}
diff --git a/src/shapes.c3 b/lib/ugui.c3l/src/ugui_shapes.c3
similarity index 56%
rename from src/shapes.c3
rename to lib/ugui.c3l/src/ugui_shapes.c3
index 96d9710..9e778b7 100644
--- a/src/shapes.c3
+++ b/lib/ugui.c3l/src/ugui_shapes.c3
@@ -1,5 +1,5 @@
module ugui;
-import std::math;
+
// ---------------------------------------------------------------------------------- //
// RECTANGLE //
@@ -10,9 +10,6 @@ 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)
{
@@ -36,26 +33,11 @@ macro bool Rect.collides(Rect a, Rect b)
return !(a.x > b.x+b.w || a.x+a.w < b.x || a.y > b.y+b.h || a.y+a.h < b.y);
}
-// return a rect that contains both rects, a bounding box of both
-macro Rect containing_rect(Rect a, Rect b)
-{
- short min_x = (short)min(a.x, b.x);
- short min_y = (short)min(a.y, b.y);
- short max_x = (short)max(a.x + a.w, b.x + b.w);
- short max_y = (short)max(a.y + a.h, b.y + b.h);
- return {
- .x = min_x,
- .y = min_y,
- .w = (short)(max_x - min_x),
- .h = (short)(max_y - min_y)
- };
-}
-
// check for empty rect
macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0;
// returns the element-wise addition of r1 and r2
-macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
+macro Rect Rect.add(Rect r1, Rect r2)
{
return {
.x = r1.x + r2.x,
@@ -66,7 +48,7 @@ macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+)
}
// returns the element-wise subtraction of r1 and r2
-macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
+macro Rect Rect.sub(Rect r1, Rect r2)
{
return {
.x = r1.x - r2.x,
@@ -77,7 +59,7 @@ macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-)
}
// returns the element-wise multiplication of r1 and r2
-macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*)
+macro Rect Rect.mul(Rect r1, Rect r2)
{
return {
.x = r1.x * r2.x,
@@ -124,7 +106,7 @@ macro Rect Rect.min(Rect a, Rect b)
}
// Offset a rect by a point
-macro Rect Rect.off(Rect r, Point p) @operator_s(+)
+macro Rect Rect.off(Rect r, Point p)
{
return {
.x = r.x + p.x,
@@ -154,35 +136,6 @@ macro Point Rect.bottom_right(Rect r)
};
}
-macro Rect Rect.center_to(Rect a, Rect b)
-{
- return {
- .x = b.x + (b.w - a.w)/2,
- .y = b.y + (b.h - a.h)/2,
- .w = a.w,
- .h = a.h,
- };
-}
-
-macro Rect Rect.pad(Rect a, Rect b)
-{
- return {
- .x = a.x + b.x,
- .y = a.y + b.y,
- .w = a.w - b.x - b.w,
- .h = a.h - b.y - b.h,
- };
-}
-
-macro Rect Rect.expand(Rect a, Rect b)
-{
- return {
- .x = a.x - b.x,
- .y = a.y - b.y,
- .w = a.w + b.x + b.w,
- .h = a.h + b.y + b.h,
- };
-}
// ---------------------------------------------------------------------------------- //
// POINT //
@@ -198,16 +151,39 @@ macro bool Point.in_rect(Point p, Rect r)
return (p.x >= r.x && p.x <= r.x + r.w) && (p.y >= r.y && p.y <= r.y + r.h);
}
-macro bool Point.outside(Point p, Rect r) => !p.in_rect(r);
+macro Point Point.add(Point a, Point b)
+{
+ return {
+ .x = a.x + b.x,
+ .y = a.y + b.y,
+ };
+}
-macro Point Point.add(Point a, Point b) @operator_s(+) => {.x = a.x+b.x, .y = a.y+b.y};
-macro Point Point.sub(Point a, Point b) @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.sub(Point a, Point b)
+{
+ return {
+ .x = a.x - b.x,
+ .y = a.y - b.y,
+ };
+}
-macro Point Point.max(Point a, Point b) => {.x = max(a.x, b.x), .y = max(a.y, b.y)};
-macro Point Point.min(Point a, Point b) => {.x = min(a.x, b.x), .y = min(a.y, b.y)};
+macro Point Point.neg(Point p) => {-p.x, -p.y};
-macro bool Point.equals(Point a, Point b) @operator_s(==) => a.x == b.x && a.y == b.y;
+macro Point Point.max(Point a, Point b)
+{
+ return {
+ .x = max(a.x, b.x),
+ .y = max(a.y, b.y),
+ };
+}
+
+macro Point Point.min(Point a, Point b)
+{
+ return {
+ .x = min(a.x, b.x),
+ .y = min(a.y, b.y),
+ };
+}
// ---------------------------------------------------------------------------------- //
// COLOR //
@@ -238,29 +214,8 @@ macro Color uint.@to_rgba($u)
};
}
-macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
-
-// ---------------------------------------------------------------------------------- //
-// SIZE //
-// ---------------------------------------------------------------------------------- //
-
-struct Size {
- short min, max;
+macro uint Color.to_uint(c)
+{
+ uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24);
+ return u;
}
-
-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_slider.c3 b/lib/ugui.c3l/src/ugui_slider.c3
new file mode 100644
index 0000000..a631dee
--- /dev/null
+++ b/lib/ugui.c3l/src/ugui_slider.c3
@@ -0,0 +1,132 @@
+module ugui;
+
+import std::io;
+import std::math;
+
+// slider element
+struct ElemSlider {
+ Rect handle;
+}
+
+
+/* handle
+ * +----+-----+---------------------+
+ * | |#####| |
+ * +----+-----+---------------------+
+ */
+macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
+ => ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent);
+<*
+@require value != null
+*>
+fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
+{
+ id = ctx.gen_id(id)!;
+
+ Elem* parent = ctx.get_parent()!;
+ Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!;
+ Style* style = ctx.styles.get_style(@str_hash("slider"));
+
+ // 2. Layout
+ elem.bounds = ctx.position_element(parent, size, style);
+
+ // handle width
+ short hw = (short)(elem.bounds.w * hpercent);
+ Rect handle = {
+ .x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value),
+ .y = elem.bounds.y,
+ .w = hw,
+ .h = elem.bounds.h,
+ };
+ elem.slider.handle = handle;
+
+ Point m = ctx.input.mouse.pos;
+ elem.events = ctx.get_elem_events(elem);
+
+ if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
+ *value = calc_value(elem.bounds.x, m.x, elem.bounds.w, hw);
+ elem.slider.handle.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value);
+ elem.events.update = true;
+ }
+
+ // Draw the slider background and handle
+ Style s = *style;
+ Rect padding = s.padding;
+ s.padding = {};
+ ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
+ s.bg = s.primary;
+ s.padding = padding;
+ s.border = {};
+ ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
+
+ return elem.events;
+}
+
+
+/*
+ * +--+
+ * | |
+ * | |
+ * +--+
+ * |##| handle
+ * |##|
+ * +--+
+ * | |
+ * | |
+ * +--+
+ */
+macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...)
+ => ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent);
+fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25)
+{
+ id = ctx.gen_id(id)!;
+
+ Elem *parent = ctx.get_parent()!;
+ Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!;
+ Style* style = ctx.styles.get_style(@str_hash("slider"));
+
+ // 1. Fill the element fields
+ if (elem.flags.is_new) {
+ elem.type = ETYPE_SLIDER;
+ } else if (elem.type != ETYPE_SLIDER) {
+ return WRONG_ELEMENT_TYPE?;
+ }
+
+ // 2. Layout
+ elem.bounds = ctx.position_element(parent, size, style);
+
+ // handle height
+ short hh = (short)(elem.bounds.h * hpercent);
+ Rect handle = {
+ .x = elem.bounds.x,
+ .y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value),
+ .w = elem.bounds.w,
+ .h = hh,
+ };
+ elem.slider.handle = handle;
+
+ Point m = ctx.input.mouse.pos;
+ elem.events = ctx.get_elem_events(elem);
+
+ if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) {
+ *value = calc_value(elem.bounds.y, m.y, elem.bounds.h, hh);
+ elem.slider.handle.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value);
+ elem.events.update = true;
+ }
+
+ // Draw the slider background and handle
+ Style s = *style;
+ Rect padding = s.padding;
+ s.padding = {};
+ ctx.push_rect(elem.bounds, parent.div.z_index, &s)!;
+ s.bg = s.primary;
+ s.padding = padding;
+ s.border = {};
+ ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!;
+
+ return elem.events;
+}
+
+macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value);
+macro float calc_value(short off, short mouse, short dim, short slider)
+ => math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f);
diff --git a/src/sprite.c3 b/lib/ugui.c3l/src/ugui_sprite.c3
similarity index 58%
rename from src/sprite.c3
rename to lib/ugui.c3l/src/ugui_sprite.c3
index 2b01e65..724262d 100644
--- a/src/sprite.c3
+++ b/lib/ugui.c3l/src/ugui_sprite.c3
@@ -30,17 +30,21 @@ 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(mem, capacity: SRITES_PER_ATLAS);
+ this.sprites.init(allocator::heap(), capacity: SRITES_PER_ATLAS);
this.should_update = false;
}
@@ -60,8 +64,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 = (ushort)uv.x;
- s.v = (ushort)uv.y;
+ s.u = uv.x;
+ s.v = uv.y;
this.sprites.set(s.id, s);
this.should_update = true;
return this.sprites.get_ref(s.id);
@@ -78,8 +82,8 @@ fn Sprite*? SpriteAtlas.get_by_id(&this, Id id)
return this.sprites.get_ref(id);
}
-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};
+macro Rect Sprite.rect(s) => {0,0,s.w,s.h};
+macro Rect Sprite.uv(s) => {s.u,s.v,s.w,s.h};
fn void? Ctx.sprite_atlas_create(&ctx, String name, AtlasType type, ushort w, ushort h)
{
@@ -100,8 +104,50 @@ fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType t
{
QOIDesc desc;
- char[] pixels = qoi::read(mem, path, &desc, QOIChannels.RGBA)!;
+ char[] pixels = qoi::read(allocator::heap(), 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/style.c3 b/lib/ugui.c3l/src/ugui_style.c3
similarity index 71%
rename from src/style.c3
rename to lib/ugui.c3l/src/ugui_style.c3
index 5957403..aa5d20b 100644
--- a/src/style.c3
+++ b/lib/ugui.c3l/src/ugui_style.c3
@@ -7,8 +7,10 @@ import std::io;
// global style, similar to the css box model
struct Style { // css box model
Rect padding;
- Rect border;
Rect margin;
+ ushort border;
+ ushort radius;
+ ushort size;
Color bg; // background color
Color fg; // foreground color
@@ -16,22 +18,20 @@ struct Style { // css box model
Color secondary; // secondary color
Color accent; // accent color
- ushort radius;
- short size;
}
const Style DEFAULT_STYLE = {
.margin = {2, 2, 2, 2},
- .border = {2, 2, 2, 2},
- .padding = {1, 1, 1, 1},
- .radius = 0,
+ .padding = {},
+ .border = 2,
+ .radius = 12,
.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;
- sz size = file::get_size(path)!!;
+ usz size = file::get_size(path)!!;
text = mem::new_array(char, size);
file::load_buffer(path, text)!!;
defer mem::free(text);
-
+
int added = ctx.import_style_from_string((String)text);
return added;
}
@@ -89,8 +89,8 @@ fn int Ctx.import_style_from_file(&ctx, String path)
* Style can be serialized and deserialized with a subset of CSS
*