diff --git a/.ecode/project_build.json b/.ecode/project_build.json deleted file mode 100644 index 593ac60..0000000 --- a/.ecode/project_build.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "Debug": { - "build": [ - { - "args": "-C resources/shaders", - "command": "make", - "working_dir": "" - }, - { - "args": "build -g", - "command": "c3c", - "working_dir": "" - } - ], - "build_types": [], - "clean": [ - { - "args": "clean", - "command": "c3c", - "working_dir": "" - } - ], - "config": { - "clear_sys_env": false - }, - "os": [ - "linux" - ], - "output_parser": { - "config": { - "relative_file_paths": true - } - }, - "run": [ - { - "args": "", - "command": "build/ugui", - "name": "Custom Executable", - "working_dir": "" - } - ] - } -} \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 74cac9a..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,16 +0,0 @@ -[submodule "lib/grapheme.c3l/thirdparty/libgrapheme"] - path = lib/grapheme.c3l/thirdparty/libgrapheme - url = git://git.suckless.org/libgrapheme - ignore = dirty -[submodule "lib/schrift.c3l/thirdparty/libschrift"] - path = lib/schrift.c3l/thirdparty/libschrift - url = https://github.com/tomolt/libschrift - ignore = dirty -[submodule "lib/sdl3.c3l"] - path = lib/sdl3.c3l - url = https://git.alemauri.eu/alema/sdl3.c3l - ignore = dirty -[submodule "lib/vendor"] - path = lib/vendor - url = https://github.com/c3lang/vendor - ignore = dirty diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index b533650..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,184 +0,0 @@ -## High level overview - -Under the hood every element has an id, this id allows the library to store state -between frames. -Elements are also cached such that when the ui tree is rebuilt at the beginning of -every frame the element data structure doesn't have to be rebuilt. - -Elements are arranged in a tree, nodes are container elements that can contain other -elements, leafs are elements that cannot contain other elements. - -Every element has a size and a position, containers also have to keep track of their -layout information and some other state. - -Elements can push commands into the draw stack, which is a structure that contains -all the draw commands that the user application has to perform do display the ui -correctly, such commands include drawing lines, rectangles, sprites, text, etc. - - -```text - +-----------+ - | ug_init() | - +-----+-----+ - | - | - | - +---------v----------+ - |ug_input_keyboard() | - |ug_input_mouse() <----+ - |ug_input_clipboard()| | - | ... | | - +---------+----------+ | - | | - | | - +-------v--------+ | - |ug_frame_begin()| | - +-------+--------+ | - | | - | | - +---------v----------+ | - |ug_window_start() | | - +---->ug_container_start()| | - | |ug_div_start() | | - | | ... | | - | +---------+----------+ | - | | | - | | | -multiple +--------v---------+ | - times |ug_layout_row() | | - | |ug_layout_column()| | - | |ug_layout_float() | | - | | ... | | - | +--------+---------+ | - | | | - | | | - | +------v------+ | - | |ug_button() | | - | |ug_text_box()| | - | |ug_slider() | | - | | ... | | - | +------+------+ | - | | | - +--------------+ | - | | - +--------v---------+ | - |ug_window_end() | | - |ug_container_end()| | - |ug_div_end() | | - | ... | | - +--------+---------+ | - | | - | | - | | - +------v-------+ | - |ug_frame_end()| | - +------+-------+ | - | | - | | - | | - +------v-------+ | - |user draws the| | - | ui +-------+ - +------+-------+ - | - |quit - | - +------v-------+ - | ug_destroy() | - +--------------+ -``` - -### Layouting - -Layouting happens in a dynamic grid, when a new element is inserted in a non-floating -manner it reserves a space in the grid, new elements are placed following this grid. - -Every div has two points of origin, one for the row layout and one for the column -layout, named origin_r and origin_c respectively - -origin_r is used when the row layout is used and it is used to position the child -elements one next to the other, as such it always points to the top-right edge -of the last row element - -```text -Layout: row -#: lost space - Parent div -x---------------------------------+ -|[origin_c] | -|[origin_r] | -| | -| | -| | -| | -| | -| | -| | -+---------------------------------+ - - Parent div -+-----x---------------------------+ -| |[origin_r] | -| E1 | | -| | | -x-----+---------------------------+ -|[origin_c] | -| | | -| | | -| | | -| | | -| | | -+-----+---------------------------+ - - Parent div -+-----+----------+-----x----------+ -| | E2 | |[origin_r]| -| E1 +----------+ | | -| |##########| E3 | | -+-----+##########| | | -|################| | | -+----------------x-----+----------+ -| [origin_c] | -| | | -| | | -| | | -+----------------+----------------+ -``` - -TODO: handle when the content overflows the div -- Use a different concept, like a view or relative space, for example the child -div could have position `[0,0]` but in reality it is relative to the origin of the -parent div -- each div could have a view and a total area of the content, when drawing everything -is clipped to the view and scrollbars are shown -- individual elements accept dimensions and the x/y coordinates could be interpreted -as offset if the layout is row/column or absolute coordinates if the leayout is floating - -A div can be marked resizeable or fixed, and static or dynamic. The difference being -that resizeable adds a resize handle to the div and dynamic lets the content overflow -causing scrollbars to be drawn - -### Notes - -How elements determine if they have focus or not - -```C -// in begin_{container} code -calculate focus -set has_focus property - - - -// in the element code -if(PARENT_HAS_FOCUS()) { - update stuff -} else { - fast path to return -} -``` - -How to get ids: - 1. use a name for each element - 2. supply an id for each element - 3. use a macro and the line position as id and then hash it - 4. use a macro, get the code line and hash it diff --git a/LAYOUT b/LAYOUT new file mode 100644 index 0000000..700b43d --- /dev/null +++ b/LAYOUT @@ -0,0 +1,243 @@ + Div Children Alignment ++------------------------------------------------+ +|TOP-LEFT TOP TOP-RIGHT| +| | +| | +| | +| | +| | +| | +|LEFT CENTER RIGHT| +| | +| | +| | +| | +| | +| | +|BOTTOM-LEFT BOTTOM BOTTOM-RIGHT| ++------------------------------------------------+ + +ALIGNMENT CHART: ++------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ +| TOP-LEFT, ROW: | TOP-LEFT, COLUMN: | BOTTOM, ROW: | BOTTOM, COLUMN: | TOP-RIGHT, ROW: | TOP-RIGHT, COLUMN: | +| | | | | | | +| +------------------------- - | +----------- - | | +----+ | | - -----------+ | +| |+-------++-----++-----+ | |+-------+ | | | E1 | | | +-------+| | +| || E1 || E2 || | | || E1 | | | | | | - -----------------------+ | | E1 || | +| || |+-----+| E3 | | || | | | +----+ | +-------++-----++-----+| | | || | +| |+-------+ | | | |+-------+ | +-------+ +---+ | +------+ | | E1 || E2 || || | +-------+| | +| | +-----+ | |+----+ | | E1 |+------+|E3 | | | E2 | | | |+-----+| E3 || | +----+| | +| ' | || E2 | | | || E2 || | | | | | +-------+ | || | | E2 || | +| ' | |+----+ | +-------++------++---+ | +------+ | +-----+| | +----+| | +| | |+---------+ | - ------------------------ - | +--+ | ' | +---------+| | +| | || E3 | | | |E3| | ' | | E3 || | +| | |+---------+ | | +--+ | | +---------+| | +| | ' | | - ----------- - | | ' | +| | ' | | | | ' | +| | | | | | | ++------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ +| LEFT, ROW: | LEFT, COLUMN: | BOTTOM-RIGHT, ROW: | BOTTOM-RIGHT, COLUMN: | TOP, ROW: | TOP, COLUMN: | +| | | | | | | +| | ' | | ' | | - -------------- - | +| ' | |+-------+ | | +-------+| | | +----------+ | +| | +----+ | || E1 | | ' | | E1 || | - ----------------------- - | | E1 | | +| |+------+ | | | || | | +-----+| | | || | +------++----++-----+ | | | | +| || |+-----+| | | |+-------+ | +-------+ | || | +-------+| | | E1 || E2 || E3 | | +----------+ | +| || E1 || E2 || E3 | | |+----+ | | E1 |+-----+| E3 || | +----+| | | |+----+| | | +--------+ | +| || |+-----+| | | || E2 | | | || E2 || || | | E2 || | +------+ | | | | E2 | | +| |+------+ | | | |+----+ | +-------++-----++-----+| | +----+| | +-----+ | +--------+ | +| | +----+ | |+---------+ | - -----------------------+ | +---------+| | | +------+ | +| ' | || E3 | | | | E3 || | | | E3 | | +| ' | |+---------+ | | +---------+| | | | | | +| | ' | | - -----------+ | | +------+ | +| | ' | | | | | ++------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ +| BOTTOM-LEFT, ROW: | BOTTOM-LEFT, COLUMN: | RIGHT, ROW: | RIGHT, COLUMN: | CENTER, ROW: | CENTER, COLUMN: | +| | | | | | | +| | ' | | ' | | | | +| | |+-------+ | | +-------+| | | | +-----------+ | +| | || E1 | | ' | | E1 || | | | | E1 | | +| ' | || | | +----+| | | || | | +----+ | | | | | +| | +-----+ | |+-------+ | +------+ | || | +-------+| | +------+ | | | | +-----------+ | +| |+-------+ | | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ | +| || E1 |+-----+| E3 | | || E2 | | | E1 || E2 || E3 || | | E2 || | ---|--E1--||-E2-||-E3-|--- | ----|---E2----|---- | +| || || E2 || | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ | +| |+-------++-----++-----+ | |+---------+ | +------+ | || | +---------+| | +------+ | | | | +-------+ | +| +------------------------- - | || E3 | | +----+| | | E3 || | | +----+ | | E3 | | +| | |+---------+ | ' | +---------+| | | | | | | | +| | +----------- - | ' | ' | | | +-------+ | +| | | | ' | | | | +| | | | | | | ++------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ + +div ( + align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER + size_x/y: EXACT(x) | GROW() | FIT(min, max) + scroll_x/y: true | false + resize_x/y: true | false + layout: ROW | COLUMN +) + +align: alignment of the children elements +size: how the div should be sized +scroll: enables scrollbars +layout: the layout direction of the children + + COLUMN ROW + +--------------------+ +----------------------------------------------------+ + | +----------------+ | |+----------------+ | + | | | | || |+------------+ | + | | | | || || |+------------------+| + | | E1 | | || E1 || E2 || E3 || + | | | | || || |+------------------+| + | | | | || |+------------+ | + | +----------------+ | |+----------------+ | + | +------------+ | +----------------------------------------------------+ + | | | | + | | E2 | | + | | | | (both have center alignment) + | +------------+ | + |+------------------+| + || || + || E3 || + || || + || || + |+------------------+| + +--------------------+ + +Element { + id: uint + sizing: { min_w, min_h max_w, max_h } + bounds: { x, y, w, h } +} + +id: unique identifier of the element +sizing: the size that the element wants +bounds: the absoulte bounds that the element got assigned + +Rendering +========= + +Rendering happens when the element is called (immediately for leaf widgets like buttons and at the end +for root widgets like divs). The drawing is done on the bounds assigned to the widget, these bounds +have a one-frame delay on the current layout. + +The layout is calculated by each div at the end of their block and at frame end all the sizes and positions +are assigned at frame end by iterating the element tree. + +ElemDiv { + align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER + size_x/y: { min, max } + scroll_x/y: true | false + layout: ROW | COLUMN + children_size_x/y: { min, max } +} + +size: + - min != max -> FIT sizing, fit to the content but respect the min and max size + - min == max == 0 -> GROW sizing, grow to the max amount of space possible + - min == max != 0 -> EXACT sizing +children_size: the size of the combined children sizes + + + +root(size_x: screen width, size_y: screen height, layout: ROW) { + + div1(layout: COLUMN, size_x: FIT, size_y GROW, resize_x: true) { + E1() + E2() + E3() + E4() + } <-(end div 1) + + div2(size_x: GROW, size_y: GROW) { + ... + } <-(end div 2) + + div3(layout: COLUMN, size_x: FIT, size_y: GROW) { + E5() + E6() + E7() + } <-(end div 3) + +} <-(end root) +(frame end) + + ++-Root-Div------------------------------------------------+ +|+-Div-1----------++-Div-2--------------------++-Div-3---+| +||+--------------+|| ||+-------+|| +||| E1 ||| ||| E5 ||| +||| ||| ||| ||| +||+--------------+|| ||+-------+|| [Root Div] +||+--------------+|| ||+-------+|| | +||| E2 ||| ||| E6 ||| +----------+----+-------+ +||| ||| ||| ||| v v v +||+--------------+|| ||+-------+|| [Div 1] [Div 2] [Div 3] +||+------+ || ||+-------+|| | | +||| | || ||| E7 ||| +----+----+----+ | +||| E3 | || ||| ||| v v v v | +||| | || ||+-------+|| [E1] [E2] [E3] [E4] +----+----+ +||+------+ || || || v v v +||+------+ || || || [E5] [E6] [E7] +||| | || || || +||| E4 | || || || +||| | || || || +||+------+ || || || +|| || || || +|+----------------++--------------------------++---------+| ++---------------------------------------------------------+ + +the call order is as follows + +E1() -> updates the children size of div1 +E2() -> " " +E3() -> " " +E4() -> " " +end div1() -> updates the children size of root +end div2() -> updates the children size of root +E5() -> updates the children size of div3 +E6() -> " " +E7() -> " " +end root() -> does nothing + +at frame end: + * Root: the root has a size constraint of fit so the bounds get assigned the whole window + * Div 1: the width has a size of fit, so it gets set to the children bounds, the height is set + to the root height since it has a height of GROW + - E1 to E4 get laid out + * Div 2: it has a width of GROW which is **along** the layout axis, so it gets added to grow list + the height gets set to the root height + * Div 3: the width is FIT, so it gets set to the content width, the height gets se to the root + height. + - E5 to E7 get laid out + * Div 2: is given a width (if there were other contending grow divs along the layout axis they + would also get sized). + - Now that div 2 has a size all it's children can be given a size + +Styling +======= + +The element bounds include the whole CSS box model: + ++---------------------------------------------+ +| MARGIN | +| +-----------------------------------+ | +| |xxxxxxxxxxx BORDER xxxxxxxxxxxx| | +| |x+-------------------------------+x| | +| |x| PADDING |x| | +| |x| +-----------------------+ |x| | +| |x| | | |x| | +| |x| | CONTENT | |x| | +| |x| | | |x| | +| |x| +-----------------------+ |x| | +| |x| |x| | +| |x+-------------------------------+x| | +| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| | +| +-----------------------------------+ | +| | ++---------------------------------------------+ + +Styling happens via a .css file, the sizing strictly refers to the content, so if the the user +requests an exact size of 100px*100px the content box will have those dimensions, but the element +bounds will be larger. diff --git a/LICENSE b/LICENSE index e69de29..0a04128 100644 --- a/LICENSE +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/Makefile b/Makefile deleted file mode 100644 index 4f151b9..0000000 --- a/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -test_renderer: test_renderer.c3 src/renderer.c3 resources/shaders/source/* - scripts/compile_shaders.sh - c3c compile -g -O0 test_renderer.c3 src/renderer.c3 --libdir lib --lib sdl3 --lib ugui diff --git a/README.md b/README.md index e69de29..151118a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1 @@ +Welcome to the ugui library. diff --git a/TODO b/TODO index 96b1ba4..f2fa0fa 100644 --- a/TODO +++ b/TODO @@ -4,21 +4,22 @@ [x] Implement div.view and scrollbars [x] Port font system from C to C3 (rewrite1) [ ] Update ARCHITECTURE.md -[ ] Write a README.md -[ ] Use an arena allocator for cache +[x] Write a README.md +[x] Use an arena allocator for cache [ ] Do not redraw if there was no update (no layout and no draw) [ ] Do command buffer damage tracking based on a context grid (see rxi writeup) -[x] Better handling of the active and focused widgets, try -to maintain focus until mouse release (fix scroll bars) +[x] Better handling of the active and focused widgets, try to maintain focus until mouse release (fix scroll bars) [x] Clip element bounds to parent div, specifically text [ ] Resizeable divs [x] Implement a z index and sort command buffer based on that [ ] Ctx.set_z_index() [x] Sort command buffer on insertion -[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot of boiler plate and reuse it -[x] The id combination in gen_id() uses an intger division, which is costly, use another combination function that is non-linear and doesn't use division +[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot + of boiler plate and reuse it +[x] The id combination in gen_id() uses an intger division, which is costly, use another combination + function that is non-linear and doesn't use division [ ] Animations, somehow -[ ] Maybe cache codepoint converted strings +[x] Maybe cache codepoint converted strings [x] Fix scroll wheel when div is scrolled [ ] Be consistent with the initialization methods some are foo.new() and some are foo.init() [ ] Implement image loading (.bmp, .ff, .qoi and .png), in the future even lossy images like .jpg @@ -28,31 +29,52 @@ to maintain focus until mouse release (fix scroll bars) [ ] .png [ ] .jpg [ ] gif support? -[ ] layout_set_max_rows() and layout_set_max_columns() +[x] layout_set_max_rows() and layout_set_max_columns() [x] Maybe SDF sprites?? [x] Stylesheets and stylesheet import [x] use SDF to draw anti-aliased rounded rectangles https://zed.dev/blog/videogame [ ] Subdivide modules into ugui::ug for exported functions and ugui::core for internal use functions (used to create widgets) [x] The render loop RAPES the gpu, valve pls fix -[ ] The way the element structures are implemented wastes a lot of memory since +[x] The way the element structures are implemented wastes a lot of memory since each struct Elem, struct Cmd, etc. is as big as the largest element. It would be better to use a different allcation strategy. [ ] Add a way to handle time events like double clicks -[ ] Border and padding do not go well together if the library issues two rect commands, the visible - border is effectively the border size plus the padding since there is a gap between the border - rect and the internal rect. A better solution is to leave it up to the renderer to draw the rect - correctly +[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the + content, the background color is applied starting from the border. Right now push_rect() offsets + the background rect by both border and padding +[x] Investigate why the debug pointer (cyan rectangle) disappears... +[ ] Selectable divs +[ ] Selectable text +[ ] Copy buffer ## Layout [x] Flexbox -[ ] 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) +[x] Center elements to the row/column +[x] Text wrapping / reflow +[x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct +[x] Correct whitespace handling in text (\t \r etc) +[x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay) instead of the curren multi-frame approach. +[x] Implement column/row sizing (min, max) +[x] Implement a way to size the element as the current row/column size + * +-------------+ + * | | + * +-------------+ + * +--+ + * | | + * +--+ + * <-------------> + * column size + See the calculator example for why it is useful +[ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing + of elements +[x] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly) +[x] Use containing_rect() in position_element() to skip some computing and semplify the function +[x] Rename position_element() to layout_element() +[x] Make functions to mark rows/columns as full, to fix the calculator demo +[ ] Grids ## Input @@ -60,7 +82,9 @@ to maintain focus until mouse release (fix scroll bars) [x] Mouse scroll wheel [ ] Touch input [x] Do not set input event to true if the movement was zero (like no mouse movement) -[ ] Use input event flags, for example to consume the input event +[x] Use input event flags, for example to consume the input event +[x] Fix bug in text box: when spamming keys you can get multiple characters in the text input field + of the context, this causes a bug where only the first char is actually used ## Commands @@ -68,9 +92,10 @@ to maintain focus until mouse release (fix scroll bars) - border width - border radius [x] add a command to update an atlas -[ ] New window command, useful for popups -[ ] Text command returns the text bounds, this way we can avoid the pattern +[x] New window command, useful for popups +[x] Text command returns the text bounds, this way we can avoid the pattern draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ... +[ ] Rounded rectangle with different radius for each corner ## Atlas @@ -93,11 +118,6 @@ to maintain focus until mouse release (fix scroll bars) [x] Checkbox [ ] Selectable text box -## Main / exaple - -[ ] Create maps from ids to textures and images instead of hardcoding them - - ## API [ ] Introduce a Layout structure that specifies the positioning of elements inside diff --git a/lib/.gitkeep b/lib/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/grapheme.c3l/Makefile b/lib/grapheme.c3l/Makefile deleted file mode 100644 index 09193f1..0000000 --- a/lib/grapheme.c3l/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -all: - make -C thirdparty/libgrapheme - mkdir -p linux-x64 - cp thirdparty/libgrapheme/libgrapheme.a linux-x64/libgrapheme.a diff --git a/lib/grapheme.c3l/libgrapheme.c3i b/lib/grapheme.c3l/libgrapheme.c3i deleted file mode 100644 index 6b1c7fc..0000000 --- a/lib/grapheme.c3l/libgrapheme.c3i +++ /dev/null @@ -1,46 +0,0 @@ -module grapheme; - -const uint GRAPHEME_INVALID_CODEPOINT = 0xFFFD; - -enum BidirectionalDirection { - GRAPHEME_BIDIRECTIONAL_DIRECTION_NEUTRAL, - GRAPHEME_BIDIRECTIONAL_DIRECTION_LTR, - GRAPHEME_BIDIRECTIONAL_DIRECTION_RTL, -} - -fn isz bidirectional_get_line_embedding_levels(uint *, isz, ichar *, isz) @extern("grapheme_bidirectional_get_line_embedding_levels"); - -fn isz bidirectional_preprocess_paragraph(uint *, isz, BidirectionalDirection, uint *, isz, BidirectionalDirection *) @extern("grapheme_bidirectional_preprocess_paragraph"); - -fn isz bidirectional_reorder_line(uint *, uint *, isz, uint *, isz) @extern("grapheme_bidirectional_reorder_line"); - -fn isz decode_utf8(char *, isz, uint *) @extern("grapheme_decode_utf8"); -fn isz encode_utf8(uint, char *, isz) @extern("grapheme_encode_utf8"); - -fn bool is_character_break(uint, uint, ushort *) @extern("grapheme_is_character_break"); - -fn bool is_lowercase(uint *, isz, isz *) @extern("grapheme_is_lowercase"); -fn bool is_titlecase(uint *, isz, isz *) @extern("grapheme_is_titlecase"); -fn bool is_uppercase(uint *, isz, isz *) @extern("grapheme_is_uppercase"); - -fn bool is_lowercase_utf8(char *, isz, isz *) @extern("grapheme_is_lowercase_utf8"); -fn bool is_titlecase_utf8(char *, isz, isz *) @extern("grapheme_is_titlecase_utf8"); -fn bool is_uppercase_utf8(char *, isz, isz *) @extern("grapheme_is_uppercase_utf8"); - -fn isz next_character_break(uint *, isz) @extern("grapheme_next_character_break"); -fn isz next_line_break(uint *, isz) @extern("grapheme_next_line_break"); -fn isz next_sentence_break(uint *, isz) @extern("grapheme_next_sentence_break"); -fn isz next_word_break(uint *, isz) @extern("grapheme_next_word_break"); - -fn isz next_character_break_utf8(char *, isz) @extern("grapheme_next_character_break_utf8"); -fn isz next_line_break_utf8(char *, isz) @extern("grapheme_next_line_break_utf8"); -fn isz next_sentence_break_utf8(char *, isz) @extern("grapheme_next_sentence_break_utf8"); -fn isz next_word_break_utf8(char *, isz) @extern("grapheme_next_word_break_utf8"); - -fn isz to_lowercase(uint *, isz, uint *, isz) @extern("grapheme_to_lowercase"); -fn isz to_titlecase(uint *, isz, uint *, isz) @extern("grapheme_to_titlecase"); -fn isz to_uppercase(uint *, isz, uint *, isz) @extern("grapheme_to_uppercase"); - -fn isz to_lowercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_lowercase_utf8"); -fn isz to_titlecase_utf8(char *, isz, char *, isz) @extern("grapheme_to_titlecase_utf8"); -fn isz to_uppercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_uppercase_utf8"); diff --git a/lib/grapheme.c3l/manifest.json b/lib/grapheme.c3l/manifest.json deleted file mode 100644 index 97f04d0..0000000 --- a/lib/grapheme.c3l/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "provides": "grapheme", - "targets": { - "linux-x64": { - "dependencies": [], - "linked-libraries": ["grapheme", "c"] - } - } -} diff --git a/lib/grapheme.c3l/project.json b/lib/grapheme.c3l/project.json deleted file mode 100644 index 4b2d212..0000000 --- a/lib/grapheme.c3l/project.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "langrev": "1", - "warnings": ["no-unused"], - "dependency-search-paths": [".."], - "dependencies": ["grapheme"], - "authors": ["Alessandro Mauri ", "Laslo Hunhold "], - "version": "0.1.0", - "sources": [], - "output": "build", - "target": "linux-x64", - "features": [], - "cpu": "generic", - "opt": "O0" -} diff --git a/lib/grapheme.c3l/thirdparty/libgrapheme b/lib/grapheme.c3l/thirdparty/libgrapheme deleted file mode 160000 index 65b354f..0000000 --- a/lib/grapheme.c3l/thirdparty/libgrapheme +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 65b354f0fcb1d925f4340dbb4415ea06e8af2bec diff --git a/lib/schrift.c3l/Makefile b/lib/schrift.c3l/Makefile deleted file mode 100644 index 1de46d5..0000000 --- a/lib/schrift.c3l/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -all: - make -C thirdparty/libschrift - mkdir -p linux-x64 - cp thirdparty/libschrift/libschrift.a linux-x64/libschrift.a diff --git a/lib/schrift.c3l/libschrift.c3 b/lib/schrift.c3l/libschrift.c3 deleted file mode 100644 index 699d296..0000000 --- a/lib/schrift.c3l/libschrift.c3 +++ /dev/null @@ -1,58 +0,0 @@ -module schrift; - -alias SftFont = void*; -alias SftUChar = uint; -alias SftGlyph = uint; - -const int SFT_DOWNWARD_Y = 0x01; - -struct Sft -{ - SftFont font; - double xScale; - double yScale; - double xOffset; - double yOffset; - int flags; -} - -struct SftLMetrics -{ - double ascender; - double descender; - double lineGap; -} - -struct SftGMetrics -{ - double advanceWidth; - double leftSideBearing; - int yOffset; - int minWidth; - int minHeight; -} - -struct SftKerning -{ - double xShift; - double yShift; -} - -struct SftImage -{ - void *pixels; - int width; - int height; -} - -extern fn ZString sft_version() @extern("sft_version"); - -extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem"); -extern fn SftFont loadfile(ZString filename) @extern("sft_loadfile"); -extern fn void freefont(SftFont font) @extern("sft_freefont"); - -extern fn int lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics"); -extern fn int lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup"); -extern fn int gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics"); -extern fn int kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning"); -extern fn int render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render"); diff --git a/lib/schrift.c3l/manifest.json b/lib/schrift.c3l/manifest.json deleted file mode 100644 index efb7f23..0000000 --- a/lib/schrift.c3l/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "provides" : "schrift", - "targets" : { - "linux-x64" : { - "dependencies" : [], - "linked-libraries" : ["schrift", "c"] - } - } -} diff --git a/lib/schrift.c3l/project.json b/lib/schrift.c3l/project.json deleted file mode 100644 index 92b049c..0000000 --- a/lib/schrift.c3l/project.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "langrev": "1", - "warnings": [ "no-unused" ], - "dependency-search-paths": [ ".." ], - "dependencies": [ "schrift" ], - "authors": [ "Alessandro Mauri ", "Thomas Oltmann " ], - "version": "0.1.0", - "sources": [ ], - "output": "build", - "target": "linux-x64", - "features": [], - "cpu": "generic", - "opt": "O0", -} diff --git a/lib/schrift.c3l/thirdparty/libschrift b/lib/schrift.c3l/thirdparty/libschrift deleted file mode 160000 index 24737d2..0000000 --- a/lib/schrift.c3l/thirdparty/libschrift +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 24737d2922b23df4a5692014f5ba03da0c296112 diff --git a/lib/sdl3.c3l b/lib/sdl3.c3l deleted file mode 160000 index 076355e..0000000 --- a/lib/sdl3.c3l +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d diff --git a/lib/ugui.c3l/LICENSE b/lib/ugui.c3l/LICENSE deleted file mode 100644 index e69de29..0000000 diff --git a/lib/ugui.c3l/README.md b/lib/ugui.c3l/README.md deleted file mode 100644 index 151118a..0000000 --- a/lib/ugui.c3l/README.md +++ /dev/null @@ -1 +0,0 @@ -Welcome to the ugui library. diff --git a/lib/ugui.c3l/linux-x64/.gitkeep b/lib/ugui.c3l/linux-x64/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/ugui.c3l/scripts/.gitkeep b/lib/ugui.c3l/scripts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/ugui.c3l/src/fifo.c3 b/lib/ugui.c3l/src/fifo.c3 deleted file mode 100644 index d9fc52e..0000000 --- a/lib/ugui.c3l/src/fifo.c3 +++ /dev/null @@ -1,91 +0,0 @@ -module fifo::faults; -faultdef FULL, EMPTY; - -module fifo{Type}; - -import std::core::mem; -import std::sort; - -// TODO: specify the allocator - -struct Fifo { - Type[] arr; - usz out; - usz count; -} - -fn void? Fifo.init(&fifo, usz size) -{ - fifo.arr = mem::new_array(Type, size); - fifo.out = 0; - fifo.count = 0; -} - -fn void Fifo.free(&fifo) -{ - (void)mem::free(fifo.arr); -} - -fn void? Fifo.enqueue(&fifo, Type *elem) -{ - if (fifo.count >= fifo.arr.len) { - return fifo::faults::FULL?; - } - usz in = (fifo.out + fifo.count) % fifo.arr.len; - fifo.arr[in] = *elem; - fifo.count++; -} - -fn Type*? Fifo.dequeue(&fifo) -{ - if (fifo.count == 0) { - return fifo::faults::EMPTY?; - } - Type *ret = &fifo.arr[fifo.out]; - fifo.count--; - fifo.out = (fifo.out + 1) % fifo.arr.len; - return ret; -} - -macro Type Fifo.get(&fifo, usz i) @operator([]) -{ - return fifo.arr[(fifo.out + i) % fifo.arr.len]; -} - -fn void Fifo.set(&fifo, usz i, Type val) @operator([]=) -{ - fifo.arr[(fifo.out + i) % fifo.arr.len] = val; -} - -macro Type* Fifo.get_ref(&fifo, usz i) @operator(&[]) -{ - return &fifo.arr[(fifo.out + i) % fifo.arr.len]; -} - -macro usz Fifo.len(&fifo) @operator(len) -{ - return fifo.count; -} - -fn void? Fifo.sort(&fifo) -{ - Type[] arr = mem::new_array(Type, fifo.count); - defer mem::free(arr); - - foreach(i, c: fifo) { - arr[i] = c; - } - - // doesn't keep ordering - //sort::quicksort(arr); - - // seems to keep the right order but we will never know... - // also since most things are already ordered the time is closer to O(n) than to O(n^2) - sort::insertionsort(arr); - - fifo.count = 0; - fifo.out = 0; - foreach (&c: arr) { - fifo.enqueue(c)!; - } -} \ No newline at end of file diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3 deleted file mode 100644 index 344cea5..0000000 --- a/lib/ugui.c3l/src/ugui_button.c3 +++ /dev/null @@ -1,187 +0,0 @@ -module ugui; - -import std::io; - -// button element -struct ElemButton { - int filler; -} - - -// draw a button, return the events on that button -macro Ctx.button(&ctx, Rect size, bool state = false, ...) - => ctx.button_id(@compute_id($vasplat), size, state); -fn ElemEvents? Ctx.button_id(&ctx, Id id, Rect size, bool active) -{ - id = ctx.gen_id(id)!; - - Elem *parent = ctx.get_parent()!; - Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; - Style* style_norm = ctx.styles.get_style(@str_hash("button")); - - elem.bounds = ctx.position_element(parent, size, style_norm); - - // if the bounds are null the element is outside the div view, - // no interaction should occur so just return - if (elem.bounds.is_null()) { return {}; } - - Style* style_active = ctx.styles.get_style(@str_hash("button-active")); - - elem.events = ctx.get_elem_events(elem); - bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover; - - // Draw the button - ctx.push_rect(elem.bounds, parent.div.z_index, is_active ? style_active : style_norm)!; - - return elem.events; -} - -macro Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool active = false, ...) - => ctx.button_label_id(@compute_id($vasplat), label, size, active); -fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool active) -{ - id = ctx.gen_id(id)!; - - Elem *parent = ctx.get_parent()!; - Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; - - short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; - Rect text_size = ctx.get_text_bounds(label)!; - Rect btn_size = text_size.add({0,0,10,10}); - Style* style_norm = ctx.styles.get_style(@str_hash("button")); - - // 2. Layout - elem.bounds = ctx.position_element(parent, btn_size, style_norm); - if (elem.bounds.is_null()) { return {}; } - - Style* style_active = ctx.styles.get_style(@str_hash("button-active")); - - elem.events = ctx.get_elem_events(elem); - - bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover; - Style* style = is_active ? style_active : style_norm; - - // Draw the button - text_size.x = elem.bounds.x; - text_size.y = elem.bounds.y; - Point off = ctx.center_text(text_size, elem.bounds); - text_size.x += off.x; - text_size.y += off.y; - ctx.push_rect(elem.bounds, parent.div.z_index, style)!; - ctx.push_string(text_size, label, parent.div.z_index, style.fg)!; - - return elem.events; -} - -macro Ctx.button_icon(&ctx, String icon, String on_icon = "", bool active = false, ...) - => ctx.button_icon_id(@compute_id($vasplat), icon, on_icon, active); -fn ElemEvents? Ctx.button_icon_id(&ctx, Id id, String icon, String on_icon, bool active) -{ - id = ctx.gen_id(id)!; - - Elem *parent = ctx.get_parent()!; - Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; - - Sprite* def_sprite = ctx.sprite_atlas.get(icon)!; - Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){}; - Rect max_size = def_sprite.rect().max(on_sprite.rect()); - - Style* style_norm = ctx.styles.get_style(@str_hash("button")); - - elem.bounds = ctx.position_element(parent, max_size, style_norm); - - // if the bounds are null the element is outside the div view, - // no interaction should occur so just return - if (elem.bounds.is_null()) { return {}; } - - Style* style_active = ctx.styles.get_style(@str_hash("button-active")); - - elem.events = ctx.get_elem_events(elem); - - bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover; - Style* style = is_active ? style_active : style_norm; - - Id tex_id = ctx.sprite_atlas.id; - if (active && on_icon != "") { - ctx.push_sprite(elem.bounds, on_sprite.uv(), tex_id, parent.div.z_index, type: on_sprite.type)!; - } else { - ctx.push_sprite(elem.bounds, def_sprite.uv(), tex_id, parent.div.z_index, type: def_sprite.type)!; - } - - // Draw the button - ctx.push_rect(elem.bounds, parent.div.z_index, style)!; - - return elem.events; -} - -// FIXME: this should be inside the style -macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...) - => ctx.checkbox_id(@compute_id($vasplat), desc, off, active, tick_sprite); -fn void? Ctx.checkbox_id(&ctx, Id id, String description, Point off, bool* active, String tick_sprite) -{ - id = ctx.gen_id(id)!; - - Elem *parent = ctx.get_parent()!; - Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; - Style* style = ctx.styles.get_style(@str_hash("checkbox")); - - Rect size = {off.x, off.y, style.size, style.size}; - elem.bounds = ctx.position_element(parent, size, style); - - // if the bounds are null the element is outside the div view, - // no interaction should occur so just return - if (elem.bounds.is_null()) return; - - elem.events = ctx.get_elem_events(elem); - if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active); - - if (tick_sprite != {}) { - ctx.push_rect(elem.bounds, parent.div.z_index, style)!; - if (*active) { - ctx.draw_sprite_raw(tick_sprite, elem.bounds, center: true)!; - } - } else { - ctx.push_rect(elem.bounds, parent.div.z_index, style)!; - if (*active) { - ushort x = style.size / 4; - Rect check = elem.bounds.add({x, x, -x*2, -x*2}); - Style s = *style; - s.bg = s.primary; - s.margin = s.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 deleted file mode 100644 index 0247e7a..0000000 --- a/lib/ugui.c3l/src/ugui_cmd.c3 +++ /dev/null @@ -1,209 +0,0 @@ -module ugui; - -import std::ascii; -import std::io; - -// command type -enum CmdType { - CMD_RECT, - CMD_UPDATE_ATLAS, - CMD_SPRITE, - CMD_SCISSOR, -} - -// command to draw a rect -struct CmdRect { - Rect rect; - ushort 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 deleted file mode 100644 index d25385b..0000000 --- a/lib/ugui.c3l/src/ugui_core.c3 +++ /dev/null @@ -1,330 +0,0 @@ -module ugui; - -import vtree; -import cache; -import fifo; - -import std::io; -import std::core::string; -import std::core::mem::allocator; - - -// element ids are just long ints -alias Id = uint; - -enum ElemType { - ETYPE_NONE, - ETYPE_DIV, - ETYPE_BUTTON, - ETYPE_SLIDER, - ETYPE_TEXT, - ETYPE_SPRITE, -} - -bitstruct ElemFlags : uint { - bool updated : 0; - bool is_new : 1; -} - -bitstruct ElemEvents : uint { - bool key_press : 0; - bool key_release : 1; - bool key_hold : 2; - bool mouse_hover : 3; - bool mouse_press : 4; - bool mouse_release : 5; - bool mouse_hold : 6; - bool update : 7; - bool text_input : 8; -} - -// element structure -struct Elem { - Id id; - isz tree_idx; - ElemFlags flags; - ElemEvents events; - Rect bounds; - ElemType type; - union { - ElemDiv div; - ElemButton button; - ElemSlider slider; - ElemText text; - ElemSprite sprite; - } -} - - -// relationships between elements are stored in a tree, it stores just the ids -alias IdTree = vtree::VTree{Id}; - -// elements themselves are kept in a cache -const uint MAX_ELEMENTS = 256; -alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS}; - -alias CmdQueue = fifo::Fifo{Cmd}; - -faultdef INVALID_SIZE, EVENT_UNSUPPORTED, UNEXPECTED_ELEMENT, WRONG_ELEMENT_TYPE, WRONG_ID; - -const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 }; - -const uint STACK_STEP = 10; -const uint MAX_ELEMS = 128; -const uint MAX_CMDS = 2048; -const uint ROOT_ID = 1; -const uint TEXT_MAX = 64; - -struct Ctx { - IdTree tree; - ElemCache cache; - CmdQueue cmd_queue; - StyleMap styles; - // total size in pixels of the context - ushort width, height; - Font font; - SpriteAtlas sprite_atlas; - - bool has_focus; - struct input { - InputEvents events; - struct mouse { - Point pos, delta; - // mouse_down: bitmap of mouse buttons that are held - // mouse_updated: bitmap of mouse buttons that have been updated - // mouse_released = mouse_updated & ~mouse_down - // mouse_pressed = mouse_updated & mouse_down - MouseButtons down; - MouseButtons updated; - // scroll wheel - Point scroll; - } - struct keyboard { - char[TEXT_MAX] text; - usz text_len; - ModKeys modkeys; - } - } - - Id hover_id; - Id focus_id; - - Rect div_scissor; // the current div bounds used for scissor test - isz active_div; // tree node indicating the current active div -} - -// return a pointer to the parent of the current active div -fn Elem*? Ctx.get_parent(&ctx) -{ - Id parent_id = ctx.tree.get(ctx.active_div)!; - return ctx.cache.search(parent_id); -} - -macro @bits(#a) => $typeof(#a).sizeof*8; -macro Id.rotate_left(id, uint $n) => (id << $n) | (id >> (@bits(id) - $n)); -const uint GOLDEN_RATIO = 0x9E3779B9; - -// generate an id combining the hashes of the parent id and the label -// with the Cantor pairing function -fn Id? Ctx.gen_id(&ctx, Id id2) -{ - Id id1 = ctx.tree.get(ctx.active_div)!; - // Mix the two IDs non-linearly - Id mixed = id1 ^ id2.rotate_left(13); - mixed ^= id1.rotate_left(7); - mixed += GOLDEN_RATIO; - return mixed; -} - -// compute the id from arguments and the line of the call -macro Id @compute_id(...) -{ - Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE); - $for var $i = 0; $i < $vacount; $i++: - id ^= (Id)$vaconst[$i].hash(); - $endfor - return id; -} - -// get or push an element from the cache, return a pointer to it -// resets all flags except is_new which is set accordingly -fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type) -{ - Elem empty_elem; - bool is_new; - Elem* elem; - elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!; - elem.flags = (ElemFlags)0; - elem.flags.is_new = is_new; - elem.id = id; - if (is_new == false && elem.type != type) { - return WRONG_ELEMENT_TYPE?; - } else { - elem.type = type; - } - elem.tree_idx = ctx.tree.add(id, ctx.active_div)!; - return elem; -} - -// find an element, does not allocate a new one in cache -// THIS HAS TO BE A MACRO SINCE IT RETURNS A POINTER TO A TEMPORARY VALUE -macro Elem* Ctx.find_elem(&ctx, Id id) -{ - Elem*? elem; - elem = ctx.cache.search(id); - if (catch elem) { - return &&(Elem){}; - } - return elem; -} - -fn Elem*? Ctx.get_active_div(&ctx) -{ - Id id = ctx.tree.get(ctx.active_div)!; - return ctx.cache.search(id); -} - -fn void? Ctx.init(&ctx) -{ - ctx.tree.init(MAX_ELEMENTS)!; - defer catch { (void)ctx.tree.free(); } - - ctx.cache.init()!; - defer catch { (void)ctx.cache.free(); } - - ctx.cmd_queue.init(MAX_CMDS)!; - defer catch { (void)ctx.cmd_queue.free(); } - - ctx.styles.init(allocator::heap()); - ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default")); - defer catch { ctx.styles.free(); } - - ctx.active_div = 0; -} - -fn void Ctx.free(&ctx) -{ - (void)ctx.tree.free(); - (void)ctx.cache.free(); - (void)ctx.cmd_queue.free(); - (void)ctx.font.free(); - (void)ctx.sprite_atlas.free(); - (void)ctx.styles.free(); -} - -fn void? Ctx.frame_begin(&ctx) -{ - // 1. Reset the active div - // 2. Get the root element from the cache and update it - ctx.active_div = 0; - Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!; - ctx.active_div = elem.tree_idx; - // The root should have the updated flag only if the size of the window - // was changed between frasmes, this propagates an element size recalculation - // down the element tree - elem.flags.updated = ctx.input.events.resize; - // if the window has focus then the root element also has focus, no other - // computation needed, child elements need to check the mouse positon and - // other stuff - //elem.flags.has_focus = ctx.has_focus; - - elem.bounds = {0, 0, ctx.width, ctx.height}; - elem.div.layout = LAYOUT_ROW; - elem.div.z_index = 0; - elem.div.children_bounds = elem.bounds; - elem.div.scroll_x.enabled = false; - elem.div.scroll_y.enabled = false; - elem.div.pcb = {}; - elem.div.origin_c = {}; - elem.div.origin_r = {}; - - ctx.div_scissor = {0, 0, ctx.width, ctx.height}; - - // The root element does not push anything to the stack - // TODO: add a background color taken from a theme or config -} - -const int DEBUG = 1; - -fn void? Ctx.frame_end(&ctx) -{ - // FIXME: this is not guaranteed to be root. the user might forget to close a div or some other element - Elem* root = ctx.get_active_div()!; - root.div.layout = LAYOUT_ROW; - - // 1. clear the tree - ctx.tree.nuke(); - - // 2. clear input fields - ctx.input.events = (InputEvents)0; - ctx.input.keyboard.text_len = 0; - - // send atlas updates - if (ctx.font.should_update) { - ctx.push_update_atlas(&ctx.font.atlas)!; - ctx.font.should_update = false; - } - if (ctx.sprite_atlas.should_update) { - ctx.push_update_atlas(&ctx.sprite_atlas.atlas)!; - ctx.sprite_atlas.should_update = false; - } - -// debug -$if DEBUG == 1: - // draw mouse position - Cmd cmd = { - .type = CMD_RECT, - .z_index = int.max-1, // hopefully over everything else - .rect.rect = { - .x = ctx.input.mouse.pos.x - 2, - .y = ctx.input.mouse.pos.y - 2, - .w = 4, - .h = 4, - }, - .rect.color = 0xff00ffffu.to_rgba() - }; - ctx.cmd_queue.enqueue(&cmd)!; -$endif - - // sort the command buffer by the z-index - ctx.cmd_queue.sort()!; -} - -<* - * @ensure elem != null -*> -macro bool Ctx.is_hovered(&ctx, Elem *elem) -{ - return ctx.input.mouse.pos.in_rect(elem.bounds); -} - -macro bool Ctx.elem_focus(&ctx, Elem *elem) -{ - return ctx.focus_id == elem.id; -} - -// TODO: add other events -// FIXME: this does not work with touch -// FIXME: hacked together, please do better -fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem) -{ - bool hover = ctx.is_hovered(elem); - bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT)); - - if (ctx.is_mouse_pressed(BTN_ANY) && !hover){ - focus = false; - if (ctx.focus_id == elem.id) ctx.focus_id = 0; - } - - if (hover) { ctx.hover_id = elem.id; } - if (focus) { ctx.focus_id = elem.id; } - - ElemEvents ev = { - .mouse_hover = hover, - .mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY), - .mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY), - .mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY), - .text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT), - }; - return ev; -} diff --git a/lib/ugui.c3l/src/ugui_div.c3 b/lib/ugui.c3l/src/ugui_div.c3 deleted file mode 100644 index 8b9e292..0000000 --- a/lib/ugui.c3l/src/ugui_div.c3 +++ /dev/null @@ -1,153 +0,0 @@ -module ugui; - -import std::io; -import std::math; - -// div element -struct ElemDiv { - Layout layout; - struct scroll_x { - bool enabled; - bool on; - float value; - } - struct scroll_y { - bool enabled; - bool on; - float value; - } - ushort scroll_size; - int z_index; - Rect children_bounds; // current frame children bounds - Rect pcb; // previous frame children bounds - Point origin_r, origin_c; -} - -// begin a widget container, or div, the size determines the offset (x,y) width and height. -// if the width or height are zero the width or height are set to the maximum available. -// if the width or height are negative the width or height will be calculated based on the children size -// sort similar to a flexbox, and the minimum size is set by the negative of the width or height -// FIXME: there is a bug if the size.w or size.h == -0 -macro Ctx.div_begin(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...) - => ctx.div_begin_id(@compute_id($vasplat), size, scroll_x, scroll_y); -fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y) -{ - id = ctx.gen_id(id)!; - - Elem* parent = ctx.get_parent()!; - Elem* elem = ctx.get_elem(id, ETYPE_DIV)!; - ctx.active_div = elem.tree_idx; - - Style* style = ctx.styles.get_style(@str_hash("default")); - Style* slider_style = ctx.styles.get_style(@str_hash("slider")); - - elem.div.scroll_x.enabled = scroll_x; - elem.div.scroll_y.enabled = scroll_y; - elem.div.scroll_size = slider_style.size ? slider_style.size : (style.size ? style.size : DEFAULT_STYLE.size); - elem.div.z_index = parent.div.z_index + 1; - - // 2. layout the element - Rect wanted_size = { - .x = size.x, - .y = size.y, - .w = size.w < 0 ? max(elem.div.pcb.w, (short)-size.w) : size.w, - .h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h, - }; - elem.bounds = ctx.position_element(parent, wanted_size, style); - elem.div.children_bounds = {}; - - // update the ctx scissor - ctx.div_scissor = elem.bounds; - ctx.push_scissor(elem.bounds, elem.div.z_index)!; - - // 4. Fill the div fields - elem.div.origin_c = { - .x = elem.bounds.x, - .y = elem.bounds.y - }; - elem.div.origin_r = elem.div.origin_c; - elem.div.layout = parent.div.layout; - - // Add the background to the draw stack - bool do_border = parent.div.layout == LAYOUT_FLOATING; - ctx.push_rect(elem.bounds, elem.div.z_index, style)!; - - elem.events = ctx.get_elem_events(elem); - - // TODO: check active - // TODO: check resizeable -} - -fn void? Ctx.div_end(&ctx) -{ - // swap the children bounds - Elem* parent = ctx.get_parent()!; - Elem* elem = ctx.get_active_div()!; - elem.div.pcb = elem.div.children_bounds; - - // FIXME: this causes all elements inside the div to loose focus since the mouse press happens - // both inside the element and inside the div bounds - //elem.events = ctx.get_elem_events(elem); - - Rect cb = elem.div.pcb; - // children bounds bottom-right corner - Point cbc = { - .x = cb.x + cb.w, - .y = cb.y + cb.h, - }; - // div bounds bottom-right corner - Point bc = { - .x = elem.bounds.x + elem.bounds.w, - .y = elem.bounds.y + elem.bounds.h, - }; - - // set the scrollbar flag, is used in layout - // horizontal overflow - elem.div.scroll_x.on = cbc.x > bc.x && elem.div.scroll_x.enabled; - // vertical overflow - elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled; - - Id hsid_raw = @str_hash("div_scrollbar_horizontal"); - Id vsid_raw = @str_hash("div_scrollbar_vertical"); - Id hsid_real = ctx.gen_id(@str_hash("div_scrollbar_horizontal"))!; - Id vsid_real = ctx.gen_id(@str_hash("div_scrollbar_vertical"))!; - short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid_real || ctx.is_hovered(ctx.find_elem(vsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0; - short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid_real || ctx.is_hovered(ctx.find_elem(hsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0; - - if (elem.div.scroll_y.on) { - if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) { - elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f; - elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f); - } - Rect vslider = { - .x = elem.bounds.x + elem.bounds.w - wdim, - .y = elem.bounds.y, - .w = wdim, - .h = elem.bounds.h - hdim, - }; - Layout prev_l = elem.div.layout; - elem.div.layout = LAYOUT_ABSOLUTE; - ctx.slider_ver_id(vsid_raw, vslider, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!; - elem.div.layout = prev_l; - } - - if (elem.div.scroll_x.on) { - if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) { - elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f; - elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f); - } - Rect hslider = { - .x = elem.bounds.x, - .y = elem.bounds.y + elem.bounds.h - hdim, - .w = elem.bounds.w - wdim, - .h = hdim, - }; - Layout prev_l = elem.div.layout; - elem.div.layout = LAYOUT_ABSOLUTE; - ctx.slider_hor_id(hsid_raw, hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!; - elem.div.layout = prev_l; - } - - // the active_div returns to the parent of the current one - ctx.active_div = ctx.tree.parentof(ctx.active_div)!; -} diff --git a/lib/ugui.c3l/src/ugui_input.c3 b/lib/ugui.c3l/src/ugui_input.c3 deleted file mode 100644 index ac70110..0000000 --- a/lib/ugui.c3l/src/ugui_input.c3 +++ /dev/null @@ -1,204 +0,0 @@ -module ugui; - -import grapheme; -import std::io; -import std::math; -import std::core::string; - -bitstruct InputEvents : uint { - bool resize : 0; // window size was changed - bool change_focus : 1; // window focus changed - bool mouse_move : 2; // mouse was moved - bool mouse_btn : 3; // mouse button pressed or released - bool mouse_scroll : 4; // mouse scroll wheel. x or y - bool text_input : 5; - bool mod_key : 6; -} - -bitstruct MouseButtons : uint { - bool btn_left : 0; - bool btn_middle : 1; - bool btn_right : 2; - bool btn_4 : 3; - bool btn_5 : 4; -} - -// FIXME: all of these names were prefixed with key_ idk if this is better, -// if it is remove the prefix on MouseButtons as well -// Modifier Keys, intended as any key that is not text -bitstruct ModKeys : uint { - bool lshift : 0; - bool rshift : 1; - bool lctrl : 2; - bool rctrl : 3; - bool lalt : 4; - bool ralt : 5; - bool lgui : 6; - bool rgui : 7; - bool num : 8; - bool caps : 9; - bool mode : 10; - bool scroll : 11; - bool bkspc : 12; - bool del : 13; - // arrow keys - bool up : 14; - bool down : 15; - bool left : 16; - bool right : 17; -} - -const ModKeys KMOD_CTRL = {.lctrl = true, .rctrl = true}; -const ModKeys KMOD_SHIFT = {.lshift = true, .rshift = true}; -const ModKeys KMOD_ALT = {.lalt = true, .ralt = true}; -const ModKeys KMOD_GUI = {.lgui = true, .rgui = true}; -const ModKeys KMOD_TXT = {.bkspc = true, .del = true}; // modkeys that act like text input -const ModKeys KMOD_NONE = {}; -const ModKeys KMOD_ANY = (ModKeys)(ModKeys.inner.max); - -const MouseButtons BTN_NONE = {}; -const MouseButtons BTN_ANY = (MouseButtons)(MouseButtons.inner.max); -const MouseButtons BTN_LEFT = {.btn_left = true}; -const MouseButtons BTN_MIDDLE = {.btn_middle = true}; -const MouseButtons BTN_RIGHT = {.btn_right = true}; -const MouseButtons BTN_4 = {.btn_4 = true}; -const MouseButtons BTN_5 = {.btn_5 = true}; - -const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max); - -fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String keys) -{ - bool is_mod = (bool)(ctx.input.keyboard.modkeys & mod); - bool is_keys = true; - String haystack = (String)ctx.input.keyboard.text[0..ctx.input.keyboard.text_len]; - char[2] needle; - foreach (c: keys) { - needle[0] = c; - is_keys = is_keys && haystack.contains((String)needle[..]); - } - return is_mod && is_keys; -} - -// Window size was changed -fn void? Ctx.input_window_size(&ctx, short width, short height) -{ - if (width <= 0 || height <= 0) { - return INVALID_SIZE?; - } - ctx.input.events.resize = ctx.width != width || ctx.height != height; - ctx.width = width; - ctx.height = height; -} - -// Window gained/lost focus -fn void Ctx.input_changefocus(&ctx, bool has_focus) -{ - // FIXME: raylib only has an API to query the focus status so we have to - // update the input flag only if the focus changed - ctx.input.events.change_focus = ctx.has_focus != has_focus; - ctx.has_focus = has_focus; -} - -macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down; -macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down; -macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down; - -// FIXME: hthis compairson could be done with a cast using MouseButtons.inner -// property but I could not figure out how -macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn) => (ctx.mouse_pressed() & btn) != BTN_NONE; -macro Ctx.is_mouse_released(&ctx, MouseButtons btn) => (ctx.mouse_released() & btn) != BTN_NONE; -macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE; - -// Mouse Buttons down -fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons) -{ - ctx.input.mouse.updated = ctx.input.mouse.down ^ buttons; - ctx.input.mouse.down = buttons; - ctx.input.events.mouse_btn = (uint)ctx.input.mouse.down != 0 || (uint)ctx.input.mouse.updated != 0; -} - -// Mouse was moved, report absolute position -fn void Ctx.input_mouse_abs(&ctx, short x, short y) -{ - ctx.input.mouse.pos.x = math::clamp(x, (short)0, ctx.width); - ctx.input.mouse.pos.y = math::clamp(y, (short)0, ctx.height); - - short dx, dy; - dx = x - ctx.input.mouse.pos.x; - dy = y - ctx.input.mouse.pos.y; - - ctx.input.mouse.delta.x = dx; - ctx.input.mouse.delta.y = dy; - - ctx.input.events.mouse_move = dx != 0 || dy != 0; -} - -// Mouse was moved, report relative motion -fn void Ctx.input_mouse_delta(&ctx, short dx, short dy) -{ - ctx.input.mouse.delta.x = dx; - ctx.input.mouse.delta.y = dy; - - short mx, my; - mx = ctx.input.mouse.pos.x + dx; - my = ctx.input.mouse.pos.y + dy; - - ctx.input.mouse.pos.x = math::clamp(mx, (short)0, ctx.width); - ctx.input.mouse.pos.y = math::clamp(my, (short)0, ctx.height); - - ctx.input.events.mouse_move = dx != 0 || dy != 0; -} - -fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0) -{ - ctx.input.mouse.scroll.x = (short)((float)-x*scale); - ctx.input.mouse.scroll.y = (short)((float)-y*scale); - ctx.input.events.mouse_scroll = x !=0 || y != 0; -} - -// append utf-8 encoded text to the context text input -fn void Ctx.input_text_utf8(&ctx, char[] text) -{ - if (text.len == 0) { return; } - - usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len; - usz len = text.len > remaining ? remaining : text.len; - char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..]; - s[..len-1] = text[..len-1]; - ctx.input.keyboard.text_len += len; - ctx.input.events.text_input = true; -} - -fn void Ctx.input_text_unicode(&ctx, char[] text) -{ - if (text.ptr == null || text.len == 0) { return; } - - char[32] tmp; - usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len; - char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..]; - - usz off; - foreach (idx, cp: text) { - if (off >= remaining) { break; } - usz enc = grapheme::encode_utf8(cp, tmp[..], tmp.len); - s[off..off+enc] = tmp[..enc]; - off += enc; - } - ctx.input.keyboard.text_len += off; - - ctx.input.events.text_input = true; -} - -fn void Ctx.input_char(&ctx, char c) -{ - char[1] b = {c}; - ctx.input_text_utf8(b[..]); -} - -// Modifier keys, like control or backspace -// TODO: make this call repetible to input modkeys one by one -fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys) -{ - ctx.input.keyboard.modkeys = modkeys; - ctx.input.events.mod_key = (uint)ctx.input.keyboard.modkeys != 0; -} diff --git a/lib/ugui.c3l/src/ugui_layout.c3 b/lib/ugui.c3l/src/ugui_layout.c3 deleted file mode 100644 index 84eaf8b..0000000 --- a/lib/ugui.c3l/src/ugui_layout.c3 +++ /dev/null @@ -1,196 +0,0 @@ -module ugui; - -enum Layout { - LAYOUT_ROW, - LAYOUT_COLUMN, - LAYOUT_FLOATING, - LAYOUT_ABSOLUTE, -} - -fn void? Ctx.layout_set_row(&ctx) -{ - Id parent_id = ctx.tree.get(ctx.active_div)!; - Elem *parent = ctx.cache.search(parent_id)!; - - if (parent.type != ETYPE_DIV) { - // what? - return UNEXPECTED_ELEMENT?; - } - - parent.div.layout = LAYOUT_ROW; -} - -fn void? Ctx.layout_set_column(&ctx) -{ - Id parent_id = ctx.tree.get(ctx.active_div)!; - Elem *parent = ctx.cache.search(parent_id)!; - - if (parent.type != ETYPE_DIV) { - // what? - return UNEXPECTED_ELEMENT?; - } - - parent.div.layout = LAYOUT_COLUMN; -} - -fn void? Ctx.layout_set_floating(&ctx) -{ - Id parent_id = ctx.tree.get(ctx.active_div)!; - Elem *parent = ctx.cache.search(parent_id)!; - - if (parent.type != ETYPE_DIV) { - // what? - return UNEXPECTED_ELEMENT?; - } - - parent.div.layout = LAYOUT_FLOATING; -} - -fn void? Ctx.layout_next_row(&ctx) -{ - Id parent_id = ctx.tree.get(ctx.active_div)!; - Elem *parent = ctx.cache.search(parent_id)!; - - if (parent.type != ETYPE_DIV) { - return UNEXPECTED_ELEMENT?; - } - - parent.div.origin_r = { - .x = parent.bounds.x, - .y = parent.div.children_bounds.bottom_right().y, - }; - parent.div.origin_c = parent.div.origin_r; -} - -fn void? Ctx.layout_next_column(&ctx) -{ - Id parent_id = ctx.tree.get(ctx.active_div)!; - Elem *parent = ctx.cache.search(parent_id)!; - - if (parent.type != ETYPE_DIV) { - return UNEXPECTED_ELEMENT?; - } - - parent.div.origin_c = { - .x = parent.div.children_bounds.bottom_right().x, - .y = parent.bounds.y, - }; - parent.div.origin_r = parent.div.origin_c; -} - - -macro Rect Elem.get_view(&elem) -{ - Rect off; - if (elem.div.scroll_x.enabled && elem.div.scroll_x.on) { - off.x = (short)((float)(elem.div.pcb.w - elem.bounds.w) * elem.div.scroll_x.value); - off.w = -elem.div.scroll_size; - } - if (elem.div.scroll_y.enabled && elem.div.scroll_y.on) { - off.y = (short)((float)(elem.div.pcb.h - elem.bounds.h) * elem.div.scroll_y.value); - off.h = -elem.div.scroll_size; - } - return elem.bounds.add(off); -} - -macro Point Elem.get_view_off(&elem) -{ - return elem.get_view().sub(elem.bounds).position(); -} - -// position the rectangle inside the parent according to the layout -// parent: parent div -// rect: the requested size -// style: apply style -<* -@require ctx != null -@require parent.type == ETYPE_DIV -*> -fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, Style* style) -{ - ElemDiv* div = &parent.div; - - Rect parent_bounds, parent_view; - Rect child_placement, child_occupied; - - // 1. Select the right origin - Point origin; - switch (div.layout) { - case LAYOUT_ROW: - origin = div.origin_r; - case LAYOUT_COLUMN: - origin = div.origin_c; - case LAYOUT_FLOATING: // none, relative to zero zero - case LAYOUT_ABSOLUTE: // absolute position, this is a no-op, return the rect - return rect; - default: // error - return {}; - } - - // 2. Compute the parent's view - parent_bounds = parent.bounds; - parent_view = parent.get_view(); - - // 3. Compute the placement and occupied area - - // grow rect (wanted size) when widht or height are less than zero - bool adapt_x = rect.w <= 0; - bool adapt_y = rect.h <= 0; - if (adapt_x) rect.w = parent_bounds.w - parent_bounds.x - origin.x; - if (adapt_y) rect.h = parent_bounds.h - parent_bounds.y - origin.y; - - // offset placement and area - child_placement = child_placement.off(origin.add(rect.position())); - child_occupied = child_occupied.off(origin.add(rect.position())); - - Rect margin = style.margin; - Rect 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/lib/ugui.c3l/src/ugui_slider.c3 b/lib/ugui.c3l/src/ugui_slider.c3 deleted file mode 100644 index a631dee..0000000 --- a/lib/ugui.c3l/src/ugui_slider.c3 +++ /dev/null @@ -1,132 +0,0 @@ -module ugui; - -import std::io; -import std::math; - -// slider element -struct ElemSlider { - Rect handle; -} - - -/* handle - * +----+-----+---------------------+ - * | |#####| | - * +----+-----+---------------------+ - */ -macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...) - => ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent); -<* -@require value != null -*> -fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25) -{ - id = ctx.gen_id(id)!; - - Elem* parent = ctx.get_parent()!; - Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!; - Style* style = ctx.styles.get_style(@str_hash("slider")); - - // 2. Layout - elem.bounds = ctx.position_element(parent, size, style); - - // handle width - short hw = (short)(elem.bounds.w * hpercent); - Rect handle = { - .x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value), - .y = elem.bounds.y, - .w = hw, - .h = elem.bounds.h, - }; - elem.slider.handle = handle; - - Point m = ctx.input.mouse.pos; - elem.events = ctx.get_elem_events(elem); - - if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) { - *value = calc_value(elem.bounds.x, m.x, elem.bounds.w, hw); - elem.slider.handle.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value); - elem.events.update = true; - } - - // Draw the slider background and handle - Style s = *style; - Rect padding = s.padding; - s.padding = {}; - ctx.push_rect(elem.bounds, parent.div.z_index, &s)!; - s.bg = s.primary; - s.padding = padding; - s.border = {}; - ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!; - - return elem.events; -} - - -/* - * +--+ - * | | - * | | - * +--+ - * |##| handle - * |##| - * +--+ - * | | - * | | - * +--+ - */ -macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...) - => ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent); -fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25) -{ - id = ctx.gen_id(id)!; - - Elem *parent = ctx.get_parent()!; - Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!; - Style* style = ctx.styles.get_style(@str_hash("slider")); - - // 1. Fill the element fields - if (elem.flags.is_new) { - elem.type = ETYPE_SLIDER; - } else if (elem.type != ETYPE_SLIDER) { - return WRONG_ELEMENT_TYPE?; - } - - // 2. Layout - elem.bounds = ctx.position_element(parent, size, style); - - // handle height - short hh = (short)(elem.bounds.h * hpercent); - Rect handle = { - .x = elem.bounds.x, - .y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value), - .w = elem.bounds.w, - .h = hh, - }; - elem.slider.handle = handle; - - Point m = ctx.input.mouse.pos; - elem.events = ctx.get_elem_events(elem); - - if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) { - *value = calc_value(elem.bounds.y, m.y, elem.bounds.h, hh); - elem.slider.handle.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value); - elem.events.update = true; - } - - // Draw the slider background and handle - Style s = *style; - Rect padding = s.padding; - s.padding = {}; - ctx.push_rect(elem.bounds, parent.div.z_index, &s)!; - s.bg = s.primary; - s.padding = padding; - s.border = {}; - ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!; - - return elem.events; -} - -macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value); -macro float calc_value(short off, short mouse, short dim, short slider) - => math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f); diff --git a/lib/ugui.c3l/src/ugui_text.c3 b/lib/ugui.c3l/src/ugui_text.c3 deleted file mode 100644 index 7d2d422..0000000 --- a/lib/ugui.c3l/src/ugui_text.c3 +++ /dev/null @@ -1,90 +0,0 @@ -module ugui; - -import std::io; - -struct ElemText { - char[] str; - usz cursor; // cursor offset -} - -macro Ctx.text_unbounded(&ctx, String text, ...) - => ctx.text_unbounded_id(@compute_id($vasplat), text); -fn void? Ctx.text_unbounded_id(&ctx, Id id, String text) -{ - id = ctx.gen_id(id)!; - - Elem *parent = ctx.get_parent()!; - Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!; - Style* style = ctx.styles.get_style(@str_hash("text")); - - elem.text.str = text; - - // if the element is new or the parent was updated then redo layout - Rect text_size = ctx.get_text_bounds(text)!; - // 2. Layout - elem.bounds = ctx.position_element(parent, text_size, style); - if (elem.bounds.is_null()) { return; } - - ctx.push_string(elem.bounds, text, parent.div.z_index, style.fg)!; -} - -macro Ctx.text_box(&ctx, Rect size, char[] text, usz* text_len, ...) - => ctx.text_box_id(@compute_id($vasplat), size, text, text_len); -fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_len) -{ - id = ctx.gen_id(id)!; - - Elem *parent = ctx.get_parent()!; - Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!; - Style* style = ctx.styles.get_style(@str_hash("text-box")); - - elem.text.str = text; - - // layout the text box - elem.bounds = ctx.position_element(parent, size, style); - - // check input and update the text - elem.events = ctx.get_elem_events(elem); - - if (elem.events.text_input) { - usz l = ctx.input.keyboard.text_len; - char[] t = ctx.input.keyboard.text[..l]; - - if (l != 0 && l < text.len - *text_len) { - text[*text_len..*text_len+l] = t[..]; - *text_len += l; - } - - if (ctx.input.keyboard.modkeys.bkspc) { - *text_len = *text_len > 0 ? *text_len-1 : 0; - } - - } - elem.text.cursor = *text_len; - - // draw the box - short line_height = (short)ctx.font.line_height(); - Rect text_box = elem.bounds.sub({0,0,0,line_height}); - Rect input_box = { - .x = elem.bounds.x, - .y = elem.bounds.y + elem.bounds.h - line_height, - .w = elem.bounds.w, - .h = line_height, - }; - Rect cursor; - Point b = ctx.get_cursor_position((String)text[:elem.text.cursor])!; - cursor = { - .x = b.x, - .y = b.y, - .w = 3, - .h = line_height, - }; - cursor = cursor.off(elem.bounds.position()); - - ctx.push_rect(text_box, parent.div.z_index, style)!; - ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg)!; - ctx.push_rect(input_box, parent.div.z_index, style)!; - ctx.push_rect(cursor, parent.div.z_index, style)!; - - return elem.events; -} diff --git a/lib/ugui.c3l/src/vtree.c3 b/lib/ugui.c3l/src/vtree.c3 deleted file mode 100644 index d18b749..0000000 --- a/lib/ugui.c3l/src/vtree.c3 +++ /dev/null @@ -1,338 +0,0 @@ -module vtree::faults; -faultdef CANNOT_SHRINK, INVALID_REFERENCE, TREE_FULL, REFERENCE_NOT_PRESENT, INVALID_ARGUMENT; - -module vtree{ElemType}; - -import std::core::mem; -import std::io; - -struct VTree { - usz elements; - ElemType[] vector; // vector of element ids - isz[] refs, ordered_refs; -} - - -macro VTree.ref_is_valid(&tree, isz ref) { return (ref >= 0 && ref < tree.refs.len); } -macro VTree.ref_is_present(&tree, isz ref) { return tree.refs[ref] >= 0; } -macro VTree.size(&tree) { return tree.refs.len; } - -// macro to zero an elemen -macro @zero() -{ - $if $assignable(0, ElemType): - return 0; - $else - return {}; - $endif -} - -fn void? VTree.init(&tree, usz size) -{ - tree.vector = mem::new_array(ElemType, size); - defer catch { (void)mem::free(tree.vector); } - - tree.refs = mem::new_array(isz, size); - defer catch { (void)mem::free(tree.refs); } - - tree.ordered_refs = mem::new_array(isz, size); - defer catch { (void)mem::free(tree.ordered_refs); } - - // set all refs to -1, meaning invalid (free) element - tree.refs[..] = -1; - - tree.elements = 0; -} - -fn void VTree.free(&tree) -{ - (void)mem::free(tree.vector); - (void)mem::free(tree.refs); - (void)mem::free(tree.ordered_refs); -} - -fn void VTree.pack(&tree) -{ - // TODO: add a PACKED flag to skip this - - isz free_spot = -1; - for (usz i = 0; i < tree.size(); i++) { - if (tree.refs[i] == -1) { - free_spot = i; - continue; - } - - // find a item that can be packed - if (free_spot >= 0 && tree.refs[i] >= 0) { - isz old_ref = i; - - // move the item - tree.vector[free_spot] = tree.vector[i]; - tree.refs[free_spot] = tree.refs[i]; - - tree.vector[i] = {}; - tree.refs[i] = -1; - - // and move all references - for (usz j = 0; j < tree.size(); j++) { - if (tree.refs[j] == old_ref) { - tree.refs[j] = free_spot; - } - } - - // mark the free spot as used - free_spot = -1; - } - } -} - -fn void? VTree.resize(&tree, usz newsize) -{ - // return error when shrinking with too many elements - if (newsize < tree.elements) { - return vtree::faults::CANNOT_SHRINK?; - } - - // pack the vector when shrinking to avoid data loss - if ((int)newsize < tree.size()) { - // FIXME: packing destroys all references to elements of vec - // so shrinking may cause dangling pointers - return vtree::faults::CANNOT_SHRINK?; - } - - usz old_size = tree.size(); - - tree.vector = ((ElemType*)mem::realloc(tree.vector, newsize*ElemType.sizeof))[:newsize]; - defer catch { (void)mem::free(tree.vector); } - - tree.refs = ((isz*)mem::realloc(tree.refs, newsize*isz.sizeof))[:newsize]; - defer catch { (void)mem::free(tree.refs); } - - tree.ordered_refs = ((isz*)mem::realloc(tree.ordered_refs, newsize*isz.sizeof))[:newsize]; - defer catch { (void)mem::free(tree.ordered_refs); } - - if (newsize > tree.size()) { - tree.vector[old_size..newsize-1] = @zero(); - tree.refs[old_size..newsize-1] = -1; - } -} - -// add an element to the tree, return it's ref -fn isz? VTree.add(&tree, ElemType elem, isz parent) -{ - // invalid parent - if (!tree.ref_is_valid(parent)) { - return vtree::faults::INVALID_REFERENCE?; - } - - // no space left - if (tree.elements >= tree.size()) { - return vtree::faults::TREE_FULL?; - } - - // check if the parent exists - // if there are no elements in the tree the first add will set the root - if (!tree.ref_is_present(parent) && tree.elements != 0) { - return vtree::faults::REFERENCE_NOT_PRESENT?; - } - - // get the first free spot - isz free_spot = -1; - for (usz i = 0; i < tree.size(); i++) { - if (tree.refs[i] == -1) { - free_spot = i; - break; - } - } - if (free_spot < 0) { - return vtree::faults::TREE_FULL?; - } - - // finally add the element - tree.vector[free_spot] = elem; - tree.refs[free_spot] = parent; - tree.elements++; - - return free_spot; -} - -// prune the tree starting from the ref -// returns the number of pruned elements -fn usz? VTree.prune(&tree, isz ref) -{ - if (!tree.ref_is_valid(ref)) { - return vtree::faults::INVALID_REFERENCE?; - } - - if (!tree.ref_is_present(ref)) { - return 0; - } - - tree.vector[ref] = @zero(); - tree.refs[ref] = -1; - tree.elements--; - - usz count = 1; - for (usz i = 0; tree.elements > 0 && i < tree.size(); i++) { - if (tree.refs[i] == ref) { - count += tree.prune(i)!; - } - } - - return count; -} - -fn usz VTree.nuke(&tree) -{ - tree.vector[0..] = @zero(); - tree.refs[0..] = -1; - usz x = tree.elements; - tree.elements = 0; - return x; -} - -// find the size of the subtree starting from ref -fn usz? VTree.subtree_size(&tree, isz ref) -{ - if (!tree.ref_is_valid(ref)) { - return vtree::faults::INVALID_REFERENCE?; - } - - if (!tree.ref_is_present(ref)) { - return 0; - } - - usz count = 1; - for (usz i = 0; i < tree.size(); i++) { - // only root has the reference to itself - if (tree.refs[i] == ref && ref != i) { - count += tree.subtree_size(i)!; - } - } - - return count; -} - -// iterate through the first level children, use a cursor like strtok_r -fn isz? VTree.children_it(&tree, isz parent, isz *cursor) -{ - if (cursor == null) { - return vtree::faults::INVALID_ARGUMENT?; - } - - // if the cursor is out of bounds then we are done for sure - if (!tree.ref_is_valid(*cursor)) { - return vtree::faults::INVALID_REFERENCE?; - } - - // same for the parent, if it's invalid it can't have children - if (!tree.ref_is_valid(parent) || !tree.ref_is_present(parent)) { - return vtree::faults::INVALID_REFERENCE?; - } - - // find the first child, update the cursor and return the ref - for (isz i = *cursor; i < tree.size(); i++) { - if (tree.refs[i] == parent) { - *cursor = i + 1; - return i; - } - } - - // if no children are found return -1 - *cursor = -1; - return -1; -} - -/* iterates trough every leaf of the subtree in the following manner - * node [x], x: visit order - * [0] - * / | \ - * / [2] [3] - * [1] | - * / \ [6] - * [4] [5] - */ -fn isz? VTree.level_order_it(&tree, isz ref, isz *cursor) -{ - if (cursor == null) { - return vtree::faults::INVALID_ARGUMENT?; - } - - isz[] queue = tree.ordered_refs; - - // TODO: this could also be done when adding or removing elements - // first call, create a ref array ordered like we desire - if (*cursor == -1) { - *cursor = 0; - queue[..] = -1; - - // iterate through the queue appending found children - isz pos, off; - do { - // printf ("ref=%d\n", ref); - for (isz i = 0; i < tree.size(); i++) { - if (tree.refs[i] == ref) { - queue[pos++] = i; - } - } - - for (; ref == queue[off] && off < tree.size(); off++); - ref = queue[off]; - - } while (tree.ref_is_valid(ref)); - // This line is why tree.ordered_refs has to be size+1 - queue[off + 1] = -1; - } - - // PRINT_ARR(queue, tree.size()); - // return -1; - - // on successive calls just iterate through the queue until we find an - // invalid ref, if the user set the cursor to -1 it means it has found what - // he needed, so free - if (*cursor < 0) { - return -1; - } else if (tree.ref_is_valid(*cursor)) { - return queue[(*cursor)++]; - } - - return -1; -} - -fn isz? VTree.parentof(&tree, isz ref) -{ - if (!tree.ref_is_valid(ref)) { - return vtree::faults::INVALID_REFERENCE?; - } - - if (!tree.ref_is_present(ref)) { - return vtree::faults::REFERENCE_NOT_PRESENT?; - } - - return tree.refs[ref]; -} - -fn ElemType? VTree.get(&tree, isz ref) -{ - if (!tree.ref_is_valid(ref)) { - return vtree::faults::INVALID_REFERENCE?; - } - - if (!tree.ref_is_present(ref)) { - return vtree::faults::REFERENCE_NOT_PRESENT?; - } - - return tree.vector[ref]; -} - -fn void VTree.print(&tree) -{ - for (isz i = 0; i < tree.size(); i++) { - if (tree.refs[i] == -1) { - continue; - } - io::printf("[%d] {parent=%d, data=", i, tree.refs[i]); - io::print(tree.vector[i]); - io::printn("}"); - } -} diff --git a/lib/vendor b/lib/vendor deleted file mode 160000 index db00622..0000000 --- a/lib/vendor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit db006221a8af625630fdb8b56707f3d07d4314a2 diff --git a/docs/.gitkeep b/linux-x64/.gitkeep similarity index 100% rename from docs/.gitkeep rename to linux-x64/.gitkeep diff --git a/lib/ugui.c3l/manifest.json b/manifest.json similarity index 76% rename from lib/ugui.c3l/manifest.json rename to manifest.json index 813ed39..e34f342 100644 --- a/lib/ugui.c3l/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "targets" : { "linux-x64" : { "link-args" : [], - "dependencies" : ["schrift", "grapheme"], + "dependencies" : ["schrift"], "linked-libraries" : [] } } diff --git a/project.json b/project.json deleted file mode 100644 index 5f672d7..0000000 --- a/project.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "langrev": "1", - "warnings": ["no-unused"], - "dependency-search-paths": ["lib", "lib/vendor/libraries"], - "dependencies": ["sdl3", "ugui"], - "features": [], - "authors": ["Alessandro Mauri "], - "version": "0.1.0", - "sources": ["src/**"], - "output": "build", - "target": "linux-x64", - "targets": { - "ugui": { - "type": "executable" - } - }, - "cpu": "native", - "opt": "O0", - "debug-info": "full" -} diff --git a/resources/.gitkeep b/resources/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/resources/hack-nerd.ttf b/resources/hack-nerd.ttf deleted file mode 100644 index f397f20..0000000 Binary files a/resources/hack-nerd.ttf and /dev/null differ diff --git a/resources/shaders/Makefile b/resources/shaders/Makefile deleted file mode 100644 index cdc4a01..0000000 --- a/resources/shaders/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -SOURCE_DIR := ./source -COMPILED_DIR := ./compiled - -SOURCE_FILES := $(wildcard $(SOURCE_DIR)/*.glsl) - -COMPILED_FILES := $(patsubst $(SOURCE_DIR)/%.glsl,$(COMPILED_DIR)/%.spv,$(SOURCE_FILES)) - -all: $(COMPILED_FILES) - @echo "Compiling shaders from $(SOURCE_DIR) -> $(COMPILED_DIR)" - -$(COMPILED_DIR)/%.spv: $(SOURCE_DIR)/%.glsl - @mkdir -p $(COMPILED_DIR) - @stage=$$(basename $< .glsl | cut -d. -f2); \ - if [ "$$stage" = "frag" ] || [ "$$stage" = "vert" ]; then \ - echo "$$stage $(notdir $<) > $(notdir $@)"; \ - glslc -O0 -g -fshader-stage=$$stage $< -o $@; \ - else \ - echo "Skipping $<: unsupported stage $$stage"; \ - fi - -$(COMPILED_DIR): - mkdir -p $(COMPILED_DIR) - -.PHONY: clean -clean: - rm -rf $(COMPILED_DIR) - -.PHONY: tree -tree: - tree $(COMPILED_DIR) - -.PHONY: compile_all -compile_all: clean all tree diff --git a/resources/shaders/source/font.frag.glsl b/resources/shaders/source/font.frag.glsl deleted file mode 100644 index 56207b4..0000000 --- a/resources/shaders/source/font.frag.glsl +++ /dev/null @@ -1,21 +0,0 @@ -#version 450 - -layout(set = 3, binding = 0) uniform Viewport { - ivec2 view; -}; -layout(set = 2, binding = 0) uniform sampler2D tx; - -layout(location = 0) in vec2 uv; -layout(location = 1) in vec4 color; - -layout(location = 0) out vec4 fragColor; - -void main() -{ - ivec2 ts = textureSize(tx, 0); - vec2 fts = vec2(ts); - vec2 real_uv = uv / fts; - - vec4 opacity = texture(tx, real_uv); - fragColor = vec4(color.rgb, color.a*opacity.r); -} \ No newline at end of file diff --git a/resources/shaders/source/msdf.frag.glsl b/resources/shaders/source/msdf.frag.glsl deleted file mode 100644 index f46be74..0000000 --- a/resources/shaders/source/msdf.frag.glsl +++ /dev/null @@ -1,35 +0,0 @@ -#version 450 - -layout(set = 3, binding = 0) uniform Viewport { - ivec2 view; -}; -layout(set = 2, binding = 0) uniform sampler2D tx; - -const float PX_RANGE = 4.0f; - -layout(location = 0) in vec2 uv; -layout(location = 1) in vec4 color; - -layout(location = 0) out vec4 fragColor; - -float screen_px_range(vec2 uv) { - vec2 unit_range = vec2(PX_RANGE)/vec2(textureSize(tx, 0)); - vec2 texel_size = vec2(1.0)/fwidth(uv); - return max(0.5*dot(unit_range, texel_size), 1.0); -} - -float median(float r, float g, float b) { - return max(min(r, g), min(max(r, g), b)); -} - -void main() { - ivec2 ts = textureSize(tx, 0); - vec2 fts = vec2(ts); - vec2 real_uv = uv / fts; - - vec3 msd = texture(tx, real_uv).rgb; - float sd = median(msd.r, msd.g, msd.b); - float distance = screen_px_range(real_uv)*(sd - 0.5); - float opacity = clamp(distance + 0.5, 0.0, 1.0); - fragColor = color * opacity; -} \ No newline at end of file diff --git a/resources/shaders/source/rect.frag.glsl b/resources/shaders/source/rect.frag.glsl deleted file mode 100644 index ad72ad6..0000000 --- a/resources/shaders/source/rect.frag.glsl +++ /dev/null @@ -1,32 +0,0 @@ -#version 450 - -layout(set = 3, binding = 0) uniform Viewport { - ivec2 view; -}; - -layout(location = 0) in vec4 in_color; -layout(location = 1) in vec4 in_quad_size; // x,y, w,h -layout(location = 2) in float in_radius; -layout(location = 3) in float thickness; - -layout(location = 0) out vec4 fragColor; - -// SDF for a rounded rectangle given the centerpoint, half size and radius, all in pixels -float sdf_rr(vec2 p, vec2 half_size, float radius) { - vec2 q = abs(p) - half_size + radius; - return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; -} - -const float smoothness = 0.9; - -void main() -{ - vec2 centerpoint = in_quad_size.xy + in_quad_size.zw * 0.5; - vec2 half_size = in_quad_size.zw * 0.5; - float distance = -sdf_rr(vec2(gl_FragCoord) - centerpoint, half_size, in_radius); - - float alpha_out = smoothstep(0.0-smoothness, 0.0, distance); - float alpha_in = 1.0 - smoothstep(thickness-smoothness, thickness, distance); - - fragColor = vec4(in_color.rgb, in_color.a * alpha_out * alpha_in); -} \ No newline at end of file diff --git a/resources/shaders/source/rect.vert.glsl b/resources/shaders/source/rect.vert.glsl deleted file mode 100644 index 3eb6bd4..0000000 --- a/resources/shaders/source/rect.vert.glsl +++ /dev/null @@ -1,31 +0,0 @@ -#version 450 - -layout(set = 1, binding = 0) uniform Viewport { - ivec2 view; -}; - -layout(location = 0) in ivec2 position; -layout(location = 1) in ivec4 attr; // quad x,y,w,h -layout(location = 2) in ivec2 uv; // x,y in the texture -layout(location = 3) in uvec4 color; - -layout(location = 0) out vec4 out_color; -layout(location = 1) out vec4 out_quad_size; -layout(location = 2) out float out_radius; -layout(location = 3) out float out_thickness; - -void main() -{ - // vertex position - ivec2 px_pos = attr.xy + position.xy * attr.zw; - vec2 clip_pos; - clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0; - clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0); - - gl_Position = vec4(clip_pos, 0.0, 1.0); - - out_color = vec4(color) / 255.0; - out_quad_size = vec4(attr); - out_radius = float(abs(uv.x)); - out_thickness = float(uv.y - uv.x); -} diff --git a/resources/shaders/source/sprite.frag.glsl b/resources/shaders/source/sprite.frag.glsl deleted file mode 100644 index ade47cb..0000000 --- a/resources/shaders/source/sprite.frag.glsl +++ /dev/null @@ -1,18 +0,0 @@ -#version 450 - -layout(set = 3, binding = 0) uniform Viewport { - ivec2 view; -}; - -layout(location = 0) in vec2 uv; -layout(location = 0) out vec4 fragColor; - -layout(set = 2, binding = 0) uniform sampler2D tx; - -void main() -{ - ivec2 ts = textureSize(tx, 0); - vec2 fts = vec2(ts); - vec2 real_uv = uv / fts; - fragColor = texture(tx, real_uv); -} \ No newline at end of file diff --git a/resources/shaders/source/sprite.vert.glsl b/resources/shaders/source/sprite.vert.glsl deleted file mode 100644 index 2dca841..0000000 --- a/resources/shaders/source/sprite.vert.glsl +++ /dev/null @@ -1,28 +0,0 @@ -#version 450 - -layout(set = 1, binding = 0) uniform Viewport { - ivec2 view; -}; - -layout(location = 0) in ivec2 position; -layout(location = 1) in ivec4 attr; // quad x,y,w,h -layout(location = 2) in ivec2 in_uv; -layout(location = 3) in uvec4 color; - -layout(location = 0) out vec2 out_uv; -layout(location = 1) out vec4 out_color; - -void main() -{ - // vertex position - ivec2 px_pos = attr.xy + position.xy * attr.zw; - vec2 clip_pos; - clip_pos.x = float(px_pos.x)*2.0 / view.x - 1.0; - clip_pos.y = -(float(px_pos.y)*2.0 / view.y - 1.0); - - gl_Position = vec4(clip_pos, 0.0, 1.0); - - vec2 px_uv = in_uv.xy + position.xy * attr.zw; - out_uv = vec2(px_uv); - out_color = vec4(color) / 255.0; -} diff --git a/resources/style.css b/resources/style.css deleted file mode 100644 index 5c21e83..0000000 --- a/resources/style.css +++ /dev/null @@ -1,68 +0,0 @@ -default { - bg: #282828ff; - fg: #fbf1c7ff; - primary: #cc241dff; - secondary: #458588ff; - accent: #fabd2fff; -} - -button { - margin: 2 2 2 2; - border: 2; - radius: 10; - - bg: #3c3836ff; - fg: #fbf1c7ff; - primary: #cc241dff; - secondary: #458588ff; - accent: #fabd2fff; -} - -button-active { - margin: 2 2 2 2; - border: 2; - radius: 10; - - bg: #504945ff; - fg: #fbf1c7ff; - primary: #cc241dff; - secondary: #cc241dff; - accent: #fabd2fff; -} - -checkbox { - margin: 2 2 2 2; - border: 2; - radius: 10; - size: 16; - bg: #3c3836ff; - fg: #fbf1c7ff; - primary: #cc241dff; - secondary: #458588ff; - accent: #fabd2fff; -} - -toggle { - margin: 2 2 2 2; - border: 2; - radius: 0; - size: 16; - bg: #3c3836ff; - fg: #fbf1c7ff; - primary: #cc241dff; - secondary: #458588ff; - accent: #fabd2fff; -} - -slider { - margin: 2 2 2 2; - padding: 4 4 4 4; - border: 1; - radius: 4; - size: 8; - bg: #3c3836ff; - fg: #fbf1c7ff; - primary: #cc241dff; - secondary: #458588ff; - accent: #fabd2fff; -} diff --git a/resources/tick_sdf.qoi b/resources/tick_sdf.qoi deleted file mode 100644 index 51a8f81..0000000 Binary files a/resources/tick_sdf.qoi and /dev/null differ diff --git a/resources/tux.qoi b/resources/tux.qoi deleted file mode 100644 index 57d6333..0000000 Binary files a/resources/tux.qoi and /dev/null differ diff --git a/resources/tux_inv.qoi b/resources/tux_inv.qoi deleted file mode 100644 index 947fb28..0000000 Binary files a/resources/tux_inv.qoi and /dev/null differ diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/ugui.c3l/src/ugui_atlas.c3 b/src/atlas.c3 similarity index 70% rename from lib/ugui.c3l/src/ugui_atlas.c3 rename to src/atlas.c3 index 7f3a40b..3bc2dbf 100644 --- a/lib/ugui.c3l/src/ugui_atlas.c3 +++ b/src/atlas.c3 @@ -4,9 +4,9 @@ import std::io; faultdef CANNOT_PLACE, INVALID_TYPE; -enum AtlasType { - ATLAS_GRAYSCALE, - ATLAS_R8G8B8A8, +enum AtlasType : (sz bpp, typeid underlying) { + ATLAS_GRAYSCALE {1, char}, + ATLAS_R8G8B8A8 {4, uint}, } // black and white atlas @@ -21,23 +21,6 @@ struct Atlas { ushort row_h; } -// bytes per pixel -macro usz AtlasType.bpp(type) -{ - switch (type) { - case ATLAS_GRAYSCALE: return 1; - case ATLAS_R8G8B8A8: return 4; - } -} - -macro typeid AtlasType.underlying(type) -{ - switch (type) { - case ATLAS_GRAYSCALE: return char; - case ATLAS_R8G8B8A8: return uint; - } -} - /* // FIXME: in and out types are not always known at compile time macro @pixel_convert(p, AtlasType $in, AtlasType $out) @@ -52,10 +35,10 @@ macro @pixel_convert(p, AtlasType $in, AtlasType $out) var b = ((p >> 16) & 0xff); var a = ((p >> 24) & 0xff); if (a == 0) return (char)0; - return (ATLAS_GRAYSCALE.underlying())(((float)r+g+b) / 3.0f); + return (ATLAS_GRAYSCALE.underlying)(((float)r+g+b) / 3.0f); $case $in == ATLAS_GRAYSCALE && $out == ATLAS_R8G8B8A8: var x = (char)(p/3.0); - return (ATLAS_R8G8B8A8.underlying())(x|(x<<8)|(x<<16)|(255<<24)); + return (ATLAS_R8G8B8A8.underlying)(x|(x<<8)|(x<<16)|(255<<24)); $default: $error "Unimplemented pixel format conversion"; $endswitch $endif @@ -69,7 +52,7 @@ fn void? Atlas.new(&atlas, Id id, AtlasType type, ushort width, ushort height) atlas.width = width; atlas.height = height; - atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height*type.bpp()); + atlas.buffer = mem::new_array(char, (sz)atlas.width*atlas.height*type.bpp); } fn void Atlas.free(&atlas) @@ -103,26 +86,26 @@ fn Point? Atlas.place(&atlas, char[] pixels, ushort w, ushort h, ushort stride) p = atlas.row; } else { atlas.row.x = 0; - atlas.row.y = atlas.row.y + atlas.row_h; + atlas.row.y = (short)atlas.row.y + (short)atlas.row_h; atlas.row_h = 0; if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) { p = atlas.row; } else { - return CANNOT_PLACE?; + return CANNOT_PLACE~; } } - usz bpp = atlas.type.bpp(); - for (usz y = 0; y < h; y++) { - for (usz x = 0; x < w; x++) { - char[] buf = atlas.buffer[(usz)(p.y+y)*atlas.width*bpp + (p.x+x)*bpp ..]; - char[] pix = pixels[(usz)y*stride*bpp + x*bpp ..]; + sz bpp = atlas.type.bpp; + for (sz y = 0; y < h; y++) { + for (sz x = 0; x < w; x++) { + char[] buf = atlas.buffer[(sz)(p.y+y)*atlas.width*bpp + (p.x+x)*bpp ..]; + char[] pix = pixels[(sz)y*stride*bpp + x*bpp ..]; buf[0..bpp-1] = pix[0..bpp-1]; } } - atlas.row.x += w; + atlas.row.x += (short)w; if (h > atlas.row_h) { atlas.row_h = h; } diff --git a/lib/ugui.c3l/src/cache.c3 b/src/cache.c3 similarity index 74% rename from lib/ugui.c3l/src/cache.c3 rename to src/cache.c3 index 130ffd1..53141a4 100644 --- a/lib/ugui.c3l/src/cache.c3 +++ b/src/cache.c3 @@ -1,4 +1,4 @@ -module cache{Key, Value, SIZE}; +module cache; /* LRU Cache * The cache uses a pool (array) to store all the elements, each element has @@ -9,22 +9,19 @@ module cache{Key, Value, SIZE}; * the elements that were not recently used. */ -// FIXME: this module should really allocate all resources on an arena or temp -// allocator, since all memory allocations are connected and freeing -// happens at the same time - import std::core::mem; -import std::core::mem::allocator; +import std::core::mem::alloc; import std::collections::bitset; import std::collections::map; alias BitArr = bitset::BitSet{SIZE}; -alias IdTable = map::HashMap{Key, usz}; -alias IdTableEntry = map::Entry{Key, usz}; +alias IdTable = map::HashMap{Key, sz}; +alias IdTableEntry = map::Entry{Key, sz}; const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0); struct Cache { + Allocator allocator; BitArr present, used; IdTable table; Value[] pool; @@ -32,7 +29,7 @@ struct Cache { } // Every CACHE_CYCLES operations mark as not-present the unused elements -macro Cache.cycle(&cache) @private { +macro Cache.cycle(&cache) { cache.cycle_count++; if (cache.cycle_count > CACHE_NCYCLES) { for (usz i = 0; i < cache.present.data.len; i++) { @@ -43,19 +40,20 @@ macro Cache.cycle(&cache) @private { } } -fn void? Cache.init(&cache) +fn void? Cache.init(&cache, Allocator allocator) { - cache.table.init(allocator::heap(), capacity: SIZE); + cache.allocator = allocator; + cache.table.init(allocator, capacity: SIZE); // FIXME: this shit is SLOW foreach (idx, bit : cache.used) { cache.used[idx] = false; } foreach (idx, bit : cache.present) { cache.present[idx] = false; } - cache.pool = mem::new_array(Value, SIZE); + cache.pool = alloc::new_array(allocator, Value, SIZE); } fn void Cache.free(&cache) { (void)cache.table.free(); - (void)mem::free(cache.pool); + (void)alloc::free(cache.allocator, cache.pool); } fn Value*? Cache.search(&cache, Key id) @@ -66,17 +64,18 @@ fn Value*? Cache.search(&cache, Key id) /* MISS, wrong key */ if (entry.key != id) { cache.table.remove(id)!; - return NOT_FOUND?; + return NOT_FOUND~; } /* MISS, the data is not valid (not present) */ if (!cache.present[entry.value]) { // if the data is not present but it is still in the table, remove it cache.table.remove(id)!; - return NOT_FOUND?; + return NOT_FOUND~; } /* HIT, set as recently used */ + //io::printfn("HIT: %d [%d]", entry.value, entry.key); cache.used[entry.value] = true; return &(cache.pool[entry.value]); } @@ -95,20 +94,22 @@ fn void Cache.remove(&cache, Key id) /* Look for a free spot in the present bitmap and return its index */ /* If there is no free space left then just return the first position */ -fn usz Cache.get_free_spot(&cache) @private +fn sz Cache.get_free_spot(&cache) { - const BITS = $typeof(cache.present.data[0]).sizeof*8; + const BITS = @bitsizeof(cache.present.data[0]); foreach (idx, d: cache.present.data) { - if (d.clz() != BITS) { - return idx*BITS + BITS-d.clz(); + if (d != $Typeof(d)::max) { + sz spot = idx*BITS + BITS-d.clz(); + if (cache.used[spot]) unreachable("free spot is not actually free: %d", spot); + return spot; } } return 0; } -fn Value*? Cache.insert_at(&cache, Value *g, Key id, usz index) @private +<* @require g != null *> +fn Value*? Cache.insert_at(&cache, Value* g, Key id, sz index) { - // TODO: verify index, g and id Value* spot; /* Set used and present */ @@ -123,18 +124,20 @@ fn Value*? Cache.insert_at(&cache, Value *g, Key id, usz index) @private } // Insert an element in the cache, returns the index +<* @require g != null *> fn Value*? Cache.insert_new(&cache, Value* g, Key id) { - usz index = cache.get_free_spot(); + sz index = cache.get_free_spot(); return cache.insert_at(g, id, index); } +<* @require g != null *> fn Value*? Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null) { Value*? c = cache.search(id); if (catch e = c) { if (e != NOT_FOUND) { - return e?; + return e~; } else { // if the element is new (inserted) set the is_new flag if (is_new) *is_new = true; diff --git a/src/cmd.c3 b/src/cmd.c3 new file mode 100644 index 0000000..4e98cc0 --- /dev/null +++ b/src/cmd.c3 @@ -0,0 +1,216 @@ +module ugui; + +import std::io; +import std::collections::list; + +// command type +enum CmdType { + CMD_RECT, + CMD_UPDATE_ATLAS, + CMD_SPRITE, + CMD_SCISSOR, + CMD_REQ_SKIP_FRAME, +} + +// command to draw a rect +struct CmdRect { + Rect rect; + ushort radius; + Color color; +} + +struct CmdUpdateAtlas { + Id id; + char* raw_buffer; + short width, height, bpp; +} + +struct CmdSprite { + Id texture_id; + SpriteType type; + Rect rect; + Rect texture_rect; + Color hue; +} + +// if rect is zero Rect{0} then reset the scissor +struct CmdScissor { + Rect rect; +} + +// command structure +struct Cmd (Printable) { + CmdType type; + int z_index; + union { + CmdRect rect; + CmdUpdateAtlas update_atlas; + CmdSprite sprite; + CmdScissor scissor; + } +} + +// command queue +alias CmdQueue = list::List{Cmd}; + + +fn int Cmd.compare_to(Cmd a, Cmd b) +{ + if (a.z_index == b.z_index) return 0; + return a.z_index > b.z_index ? 1 : -1; +} + + +// FIXME: This sorting method does not fully respect layer ordering for popups. +// Popups that start on the same layer but are enqueued at different times +// may still be rendered on top of each other, even if the first popup +// has elements in higher layers than the second. The current approach +// does not truly sort by layer; it only moves elements from higher layers +// to the end of the queue as they are encountered. +fn void? CmdQueue.sort(&queue) +{ + CmdQueue stack; + stack.init(tmem); + + for (sz i = queue.len()-1; i > 0; i--) { + Cmd cur = (*queue)[i]; + Cmd next = (*queue)[i - 1]; + // cur < next + if (cur.compare_to(next) < 0) { + stack.push(next); + queue.remove_at(i-1); + } + } + + sz l = stack.len(); + for (sz i; i < l; i++) { + queue.push(stack.pop())!; + } + +} + +// implement the Printable interface +fn sz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic +{ + sz ret; + + ret += f.printf("Cmd{ type: %s, z_index: %d, ", cmd.type, cmd.z_index)!; + switch (cmd.type) { + case CMD_RECT: + ret += f.print("CmdRect")!; + ret += io::struct_to_format(cmd.rect, f, false)!; + case CMD_SCISSOR: + ret += f.print("CmdScissor")!; + ret += io::struct_to_format(cmd.scissor, f, false)!; + case CMD_SPRITE: + ret += f.print("CmdSprite")!; + ret += io::struct_to_format(cmd.sprite, f, false)!; + case CMD_UPDATE_ATLAS: + ret += f.print("CmdUpdateAtlas")!; + ret += io::struct_to_format(cmd.update_atlas, f, false)!; + case CMD_REQ_SKIP_FRAME: + ret += f.print("Skip Frame Request")!; + } + + ret += f.print("}")!; + return ret; +} + +macro bool cull_rect(Rect rect, Rect clip = {0,0,short::max,short::max}) +{ + bool no_area = rect.w <= 0 || rect.h <= 0; + return no_area || !rect.collides(clip); +} + +// FIXME: this whole thing could be done at compile time, maybe +macro Ctx.push_cmd(&ctx, Cmd cmd, int z_index) +{ + cmd.z_index = z_index; + Rect rect; + switch (cmd.type) { + case CMD_RECT: rect = cmd.rect.rect; + case CMD_SPRITE: rect = cmd.sprite.rect; + default: return ctx.cmd_queue.push(cmd); + } + if (cull_rect(rect, ctx.div_scissor)) { +// println("NOPE: ", cmd.rect.rect, cmd.z_index); +// unreachable(); + return; + } + return ctx.cmd_queue.push(cmd); +} + +fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index) +{ + Cmd sc = { + .type = CMD_SCISSOR, + .scissor.rect = rect.intersection(ctx.div_scissor), + }; + ctx.push_cmd(sc, z_index); +} + +fn void? Ctx.reset_scissor(&ctx, int z_index) => ctx.push_cmd({.type=CMD_SCISSOR,.scissor.rect=ctx.div_scissor}, z_index); + +fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style) +{ + Rect border = style.border; + ushort radius = style.radius; + Color bg = style.bg; + Color border_color = style.secondary; + + // FIXME: this implies that the border has to be uniform + if (!border.is_null()) { + Cmd cmd = { + .type = CMD_RECT, + .rect.rect = rect, + .rect.color = border_color, + .rect.radius = radius + (ushort)border.x, + }; + ctx.push_cmd(cmd, z_index); + } + + Cmd cmd = { + .type = CMD_RECT, + .rect.rect = { + .x = rect.x + border.x, + .y = rect.y + border.y, + .w = rect.w - (border.x+border.w), + .h = rect.h - (border.y+border.h), + }, + .rect.color = bg, + .rect.radius = radius, + }; + ctx.push_cmd(cmd, z_index); +} + +// TODO: accept a Sprite* instead of all this shit +fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL) +{ + Cmd cmd = { + .type = CMD_SPRITE, + .sprite.type = type, + .sprite.rect = bounds, + .sprite.texture_rect = texture, + .sprite.texture_id = texture_id, + .sprite.hue = hue, + }; + ctx.push_cmd(cmd, z_index); +} + +fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas) +{ + Cmd up = { + .type = CMD_UPDATE_ATLAS, + .update_atlas = { + .id = atlas.id, + .raw_buffer = atlas.buffer, + .width = (short)atlas.width, + .height = (short)atlas.height, + .bpp = (short)atlas.type.bpp, + }, + }; + // update the atlases before everything else + ctx.push_cmd(up, -1); +} + +macro Ctx.dbg_rect(&ctx, Rect r, uint c = 0xff000042u) => ctx.push_rect(r, int::max, &&(Style){.bg=c.to_rgba()})!!; diff --git a/src/core.c3 b/src/core.c3 new file mode 100644 index 0000000..9cd4fcc --- /dev/null +++ b/src/core.c3 @@ -0,0 +1,398 @@ +module ugui; + +import mtree; +import cache; + + +import std::io; +import std::core::string; +import std::core::mem::allocator; +import std::collections::pair; +import std::sort; + + +macro println(...) +{ + $for var $i = 0; $i < $vacount; $i++: + io::print($vaexpr[$i]); + $endfor + io::printn(); +} + + +// element ids are just long ints +alias Id = uint; + +enum ElemType { + ETYPE_NONE, + ETYPE_DIV, + ETYPE_BUTTON, + ETYPE_SLIDER, + ETYPE_TEXT, + ETYPE_SPRITE, +} + +bitstruct ElemFlags : uint { + bool updated : 0; + bool is_new : 1; // element is new in the cache + bool shown : 2; // element has been shown (drawn) this frame +} + +bitstruct ElemEvents : uint { + bool key_press : 0; + bool key_release : 1; + bool key_repeat : 2; + bool mouse_hover : 3; + bool mouse_press : 4; + bool mouse_release : 5; + bool mouse_hold : 6; + bool update : 7; + bool text_input : 8; + bool has_focus : 9; +} + +// element structure +struct Elem { + Id id; + int tree_idx; + ElemFlags flags; + ElemEvents events; + Rect bounds; + Rect children_bounds; + ElemType type; + Layout layout; + int z_index; + union { + ElemDiv div; + ElemButton button; + ElemSlider slider; + ElemText text; + ElemSprite sprite; + } +} + + +const uint MAX_ELEMENTS = 256; +const uint MAX_COMMANDS = 2048; +const uint STACK_STEP = 10; +const uint ROOT_ID = 1; +const uint TEXT_MAX = 64; + + +// Tuple of Element pointers, used when it is useful to get both parent and child +alias PElemTuple = pair::Pair{Elem*, Elem*}; + +// relationships between elements are stored in a tree, it stores just the ids +alias IdTree = mtree::MTree{Id}; + +// elements themselves are kept in a cache +alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS}; + +faultdef INVALID_SIZE, EVENT_UNSUPPORTED, WRONG_ELEMENT_TYPE, WRONG_ID; + +struct InputData { + InputEvents events; + struct mouse { + Point pos, delta; + // mouse_down: bitmap of mouse buttons that are held + // mouse_updated: bitmap of mouse buttons that have been updated + // mouse_released = mouse_updated & ~mouse_down + // mouse_pressed = mouse_updated & mouse_down + MouseButtons down; + MouseButtons updated; + // scroll wheel + Point scroll; + } + struct keyboard { + char[TEXT_MAX] text; + sz text_len; + ModKeys modkeys; + } +} + +struct Ctx { + IdTree tree; + ElemCache cache; + CmdQueue cmd_queue; + StyleMap styles; + // total size in pixels of the context + ushort width, height; + Font font; + SpriteAtlas sprite_atlas; + + bool skip_frame; + + bool has_focus; + InputData input, current_input; + + Id hover_id; + Id focus_id; + + Rect div_scissor; // the current div bounds used for scissor test + int active_div; // tree node indicating the current active div +} + +// return a pointer to the parent of the current active div +fn Elem*? Ctx.get_parent(&ctx) +{ + Id parent_id = ctx.tree[ctx.active_div]!; + Elem* parent = ctx.cache.search(parent_id)!; + return parent; +} + +macro @bits(#a) => $Typeof(#a)::size*8; +macro Id.rotate_left(id, uint $n) => (id << $n) | (id >> (@bits(id) - $n)); +const uint GOLDEN_RATIO = 0x9E3779B9; + +// generate an id combining the hashes of the parent id and the label +// with the Cantor pairing function +fn Id? Ctx.gen_id(&ctx, Id id2) +{ + // FIXME: this is SHIT + Id id1 = ctx.tree.get(ctx.active_div)!; + // Mix the two IDs non-linearly + Id mixed = id1 ^ id2.rotate_left(13); + mixed ^= id1.rotate_left(7); + mixed += GOLDEN_RATIO; + return mixed; +} + +// compute the id from arguments and the line of the call +macro Id @compute_id(...) +{ + Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE); + $for var $i = 0; $i < $vaarg.len; $i++: + id ^= (Id)$vaarg[$i].hash(); + $endfor + return id; +} + +// get or push an element from the cache, return a pointer to it +// resets all flags except is_new which is set accordingly +fn PElemTuple? Ctx.get_elem(&ctx, Id id, ElemType type) +{ + bool is_new; + Elem* parent; + Elem* elem; + parent = ctx.get_parent() ?? &&(Elem){}; + + elem = ctx.cache.get_or_insert(&&(Elem){}, id, &is_new)!; + elem.flags = (ElemFlags)0; + elem.flags.is_new = is_new; + elem.flags.shown = true; + elem.id = id; + elem.layout = {}; + if (is_new == false && elem.type != type) { + return WRONG_ELEMENT_TYPE~; + } else { + elem.type = type; + } + elem.z_index = parent.z_index; + elem.tree_idx = ctx.tree.add(ctx.active_div, id)!; + return {elem, parent}; +} + +// find an element, does not allocate a new one in cache +// THIS HAS TO BE A MACRO SINCE IT RETURNS A POINTER TO A TEMPORARY VALUE +macro Elem* Ctx.find_elem(&ctx, Id id) +{ + Elem*? elem; + elem = ctx.cache.search(id); + if (catch elem) { + return &&(Elem){}; + } + return elem; +} + +fn Elem*? Ctx.get_active_div(&ctx) +{ + Id id = ctx.tree.get(ctx.active_div)!; + return ctx.cache.search(id); +} + +fn void? Ctx.init(&ctx, Allocator allocator) +{ + ctx.tree.init(MAX_ELEMENTS, allocator); + defer catch { (void)ctx.tree.free(); } + + ctx.cache.init(allocator)!; + defer catch { (void)ctx.cache.free(); } + + ctx.cmd_queue.init(mem, MAX_COMMANDS); + defer catch { (void)ctx.cmd_queue.free(); } + + ctx.styles.init(allocator); + ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default")); + defer catch { ctx.styles.free(); } + + ctx.active_div = 0; +} + +fn void Ctx.free(&ctx) +{ + (void)ctx.tree.free(); + (void)ctx.cache.free(); + (void)ctx.cmd_queue.free(); + (void)ctx.font.free(); + (void)ctx.sprite_atlas.free(); + (void)ctx.styles.free(); +} + +fn void? Ctx.frame_begin(&ctx) +{ + // 1. Reset the active div + // 2. Get the root element from the cache and update it + ctx.active_div = 0; + Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!.first; + ctx.active_div = elem.tree_idx; + // The root should have the updated flag only if the size of the window + // was changed between frasmes, this propagates an element size recalculation + // down the element tree + elem.flags.updated = ctx.input.events.resize; + // if the window has focus then the root element also has focus, no other + // computation needed, child elements need to check the mouse positon and + // other stuff + //elem.flags.has_focus = ctx.has_focus; + + elem.bounds = {0, 0, (short)ctx.width, (short)ctx.height}; + elem.z_index = 0; + elem.div.scroll_x.enabled = false; + elem.div.scroll_y.enabled = false; + elem.layout.dir = ROW; + elem.layout.anchor = TOP_LEFT; + elem.layout.w = @exact((short)ctx.width); + elem.layout.h = @exact((short)ctx.height); + + ctx.div_scissor = elem.bounds; + + ctx.skip_frame = false; +} + +fn void? Ctx.frame_end(&ctx) +{ + Elem* root = ctx.get_active_div()!; + if (root.id != ROOT_ID) { + return WRONG_ID~; + } + + // 2. clear input fields + Point mdelta = ctx.current_input.mouse.pos - ctx.input.mouse.pos; + ctx.input = ctx.current_input; + ctx.input.mouse.delta = mdelta; + ctx.current_input.events = {}; + ctx.current_input.mouse.scroll = {}; + ctx.current_input.mouse.updated = BTN_NONE; + ctx.current_input.keyboard.text_len = 0; + + // DO THE LAYOUT + ctx.layout_element_tree()!; + + foreach (idx, id : ctx.tree.elem_vec) { + if (!ctx.tree.is_used((int)idx)) continue; + Elem* c = ctx.find_elem(id); + // reset events + c.events = {}; + // reset shown flag + // TODO: use shown_last_frame to avoid this loop entirely + c.flags.shown = false; + } + + // Propagate input events to the right elements + ctx.set_elem_events(ctx.hover_id); + ctx.set_elem_events(ctx.focus_id); + + + // 1. clear the tree + ctx.tree.nuke(); + + // send atlas updates + if (ctx.font.should_update) { + ctx.push_update_atlas(&ctx.font.atlas)!; + ctx.font.should_update = false; + } + if (ctx.sprite_atlas.should_update) { + ctx.push_update_atlas(&ctx.sprite_atlas.atlas)!; + ctx.sprite_atlas.should_update = false; + } + + // send skip frame request + if (ctx.skip_frame) { + ctx.cmd_queue.push({.type = CMD_REQ_SKIP_FRAME}); + } + + // sort the command buffer by the z-index + // FIXME: sorting the buffer fucks with scissor commands that have to be kept in place + // TODO: instead of sorting at the end perform ordered inserts into the command buffer + //sort::countingsort(ctx.cmd_queue, fn uint(Cmd c) => c.z_index+1); + ctx.cmd_queue.sort()!; + + // debug +$if $feature(DEBUG_POINTER): + // draw mouse position + Cmd cmd = { + .type = CMD_RECT, + .z_index = int::max-1, // hopefully over everything else + .rect.rect = { + .x = ctx.input.mouse.pos.x - 2, + .y = ctx.input.mouse.pos.y - 2, + .w = 4, + .h = 4, + }, + .rect.color = 0xff00ffffu.to_rgba() + }; + ctx.cmd_queue.push(cmd); +$endif +} + + +macro bool Ctx.is_hovered(&ctx, Elem *elem) => ctx.input.mouse.pos.in_rect(elem.bounds); + +// Check if the element is hovered and/or focused, if it is update the context ids. +// The order in which the elements are passed to this function is not relevant +fn void Ctx.update_hover_and_focus(&ctx, Elem* elem) +{ + bool hover = ctx.is_hovered(elem); + bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_ANY)); + + if (hover) { + Elem* prev_hover = ctx.find_elem(ctx.hover_id); + bool different = prev_hover.id != elem.id; + bool still_hovered = ctx.is_hovered(prev_hover); + bool shown = prev_hover.flags.shown; + bool above = prev_hover.z_index > elem.z_index; + + hover = !(different && still_hovered && shown && above); + } + + if (focus) { + Elem* prev_focus = ctx.find_elem(ctx.hover_id); + bool different = prev_focus.id != elem.id; + bool shown = prev_focus.flags.shown; + bool above = prev_focus.z_index > elem.z_index; + + focus = !(different && shown && above); + } + + if (hover) ctx.hover_id = elem.id; + if (focus) ctx.focus_id = elem.id; +} + +// FIXME: this does not work with touch +fn void Ctx.set_elem_events(&ctx, Id id) +{ + bool hover = id == ctx.hover_id; + bool focus = id == ctx.focus_id; + Elem* e = ctx.find_elem(id); + + e.events = { + .has_focus = focus, + .mouse_hover = hover, + .mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY), + .mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY), + .mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY), + .key_press = focus && ctx.input.events.key_press, + .key_release = focus && ctx.input.events.key_release, + .key_repeat = focus && ctx.input.events.key_repeat, + .text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT), + }; +} diff --git a/lib/ugui.c3l/src/ugui_font.c3 b/src/font.c3 similarity index 58% rename from lib/ugui.c3l/src/ugui_font.c3 rename to src/font.c3 index 020ed75..0bc233e 100644 --- a/lib/ugui.c3l/src/ugui_font.c3 +++ b/src/font.c3 @@ -1,19 +1,26 @@ -module ugui; +module ugui::font; import schrift; -import grapheme; import std::collections::map; import std::core::mem; import std::core::mem::allocator; import std::io; -import std::ascii; +import std::core::ascii; +// ---------------------------------------------------------------------------------- // +// CODEPOINT // +// ---------------------------------------------------------------------------------- // + // unicode code point, different type for a different hash alias Codepoint = uint; //macro uint Codepoint.hash(self) => ((uint)self).hash(); +// ---------------------------------------------------------------------------------- // +// FONT ATLAS // +// ---------------------------------------------------------------------------------- // + /* width and height of a glyph contain the kering advance * (u,v) * +-------------*---+ - @@ -58,9 +65,18 @@ struct Font { bool should_update; // should send update_atlas command, resets at frame_end() } -fn void? Font.load(&font, String name, ZString path, uint height, float scale) +macro Rect Glyph.bounds(&g) => {.x = (short)g.ox, .y = (short)g.oy, .w = (short)g.w, .h = (short)g.h}; +macro Rect Glyph.uv(&g) => {.x = (short)g.u, .y = (short)g.v, .w = (short)g.w, .h = (short)g.h}; + +<* +@param [&inout] font +@param [in] name +@param [&in] path +@require height > 0, scale > 0: "height and scale must be positive non-zero" +*> +fn void? Font.load(&font, Allocator allocator, String name, ZString path, uint height, float scale) { - font.table.init(allocator::heap(), capacity: FONT_CACHED); + font.table.init(allocator, capacity: FONT_CACHED); font.id = name.hash(); font.size = height*scale; @@ -74,7 +90,7 @@ fn void? Font.load(&font, String name, ZString path, uint height, float scale) font.sft.font = schrift::loadfile(path); if (font.sft.font == null) { font.table.free(); - return TTF_LOAD_FAILED?; + return TTF_LOAD_FAILED~; } schrift::SftLMetrics lmetrics; @@ -84,17 +100,19 @@ fn void? Font.load(&font, String name, ZString path, uint height, float scale) font.linegap = (float)lmetrics.lineGap; //io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap); - // TODO: allocate buffer based on FONT_CACHED and the size of a sample letter - // like the letter 'A' ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED)); font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!; // preallocate the ASCII range for (char c = ' '; c < '~'; c++) { - font.get_glyph((Codepoint)c)!; + // FIXME: without @inline, this crashes with O1 or greater + font.get_glyph((Codepoint)c) @inline!; } } +<* +@param [&inout] font +*> fn Glyph*? Font.get_glyph(&font, Codepoint code) { Glyph*? gp; @@ -102,12 +120,13 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code) if (catch excuse = gp) { if (excuse != NOT_FOUND) { - return excuse?; + return excuse~; } } else { return gp; } + // missing glyph, render and place into an atlas Glyph glyph; @@ -115,21 +134,21 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code) schrift::SftGMetrics gmtx; if (schrift::lookup(&font.sft, (SftUChar)code, &gid) < 0) { - return MISSING_GLYPH?; + return MISSING_GLYPH~; } if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) { - return BAD_GLYPH_METRICS?; + return BAD_GLYPH_METRICS~; } schrift::SftImage img = { .width = gmtx.minWidth, .height = gmtx.minHeight, }; - char[] pixels = mem::new_array(char, (usz)img.width * img.height); + char[] pixels = mem::new_array(char, (usz)img.width * (usz)img.height); img.pixels = pixels; if (schrift::render(&font.sft, gid, img) < 0) { - return RENDER_ERROR?; + return RENDER_ERROR~; } glyph.code = code; @@ -142,8 +161,8 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code) // glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv); Point uv = font.atlas.place(pixels, glyph.w, glyph.h, (ushort)img.width)!; - glyph.u = uv.x; - glyph.v = uv.y; + glyph.u = (ushort)uv.x; + glyph.v = (ushort)uv.y; mem::free(pixels); @@ -153,6 +172,9 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code) return font.table.get_ref(code); } +<* +@param [&inout] font +*> fn void Font.free(&font) { font.atlas.free(); @@ -160,107 +182,44 @@ fn void Font.free(&font) schrift::freefont(font.sft.font); } -fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0) + +// ---------------------------------------------------------------------------------- // +// FONT LOAD AND QUERY // +// ---------------------------------------------------------------------------------- // + +module ugui; + +<* +@param [&inout] ctx +@param [in] name +@param [&in] path +@require height > 0, scale > 0: "height and scale must be positive non-zero" +*> +fn void? Ctx.load_font(&ctx, Allocator allocator, String name, ZString path, uint height, float scale = 1.0) { - return ctx.font.load(name, path, height, scale); + return ctx.font.load(allocator, name, path, height, scale); } <* -@require off != null +@param [&in] ctx +@param [in] label +@require ctx.font.id != 0 : "font not loaded" *> -fn Codepoint str_to_codepoint(char[] str, usz* off) -{ - Codepoint cp; - isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp); - if (b == 0 || b > str.len) { - return 0; - } - *off = b; - return cp; -} - -fn Rect? Ctx.get_text_bounds(&ctx, String text) -{ - Rect text_bounds; - short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; - short line_gap = (short)ctx.font.linegap; - text_bounds.h = line_height; - Glyph* gp; - - // TODO: account for unicode codepoints - short line_len; - Codepoint cp; - usz off, x; - while ((cp = str_to_codepoint(text[off..], &x)) != 0) { - off += x; - bool n; - if (!ascii::is_cntrl((char)cp)) { - gp = ctx.font.get_glyph(cp)!; - line_len += gp.adv; - } else if (cp == '\n'){ - text_bounds.h += line_height + line_gap; - line_len = 0; - } else { - continue; - } - if (line_len > text_bounds.w) { - text_bounds.w = line_len; - } - } - - return text_bounds; -} - -fn Point? Ctx.get_cursor_position(&ctx, String text) -{ - short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; - short line_gap = (short)ctx.font.linegap; - Glyph* gp; - - // TODO: account for unicode codepoints - Point line; - Codepoint cp; - usz off, x; - while ((cp = str_to_codepoint(text[off..], &x)) != 0) { - off += x; - bool n; - if (!ascii::is_cntrl((char)cp)) { - gp = ctx.font.get_glyph(cp)!; - line.x += gp.adv; - } else if (cp == '\n'){ - line.y += line_height + line_gap; - line.x = 0; - } else { - continue; - } - } - - return line; -} - - -fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds) -{ - short dw = bounds.w - text_bounds.w; - short dh = bounds.h - text_bounds.h; - - return {.x = dw/2, .y = dh/2}; -} - // TODO: check if the font is present in the context -fn Id Ctx.get_font_id(&ctx, String label) -{ - return (Id)label.hash(); -} +fn Id Ctx.get_font_id(&ctx, String label) => (Id)ctx.font.id; +<* +@param [&in] ctx +@param [in] name +*> fn Atlas*? Ctx.get_font_atlas(&ctx, String name) { - // TODO: use the font name, for now there is only one font if (name.hash() != ctx.font.id) { - return WRONG_ID?; + return WRONG_ID~; } return &ctx.font.atlas; } +<* @param [&in] font *> fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5); diff --git a/src/input.c3 b/src/input.c3 new file mode 100644 index 0000000..29dca3c --- /dev/null +++ b/src/input.c3 @@ -0,0 +1,240 @@ +module ugui; + +import std::io; +import std::math; +import std::core::string; + + +bitstruct InputEvents : uint { + bool resize : 0; // window size was changed + bool change_focus : 1; // window focus changed + bool mouse_move : 2; // mouse was moved + bool mouse_btn : 3; // mouse button pressed or released + bool mouse_scroll : 4; // mouse scroll wheel. x or y + bool text_input : 5; + bool mod_key : 6; + bool key_press : 7; + bool key_release : 8; + bool key_repeat : 9; +} + +bitstruct MouseButtons : uint { + bool btn_left : 0; + bool btn_middle : 1; + bool btn_right : 2; + bool btn_4 : 3; + bool btn_5 : 4; +} + +// FIXME: all of these names were prefixed with key_ idk if this is better, +// if it is remove the prefix on MouseButtons as well +// Modifier Keys, intended as any key that is not text +bitstruct ModKeys : uint { + bool lshift : 0; + bool rshift : 1; + bool lctrl : 2; + bool rctrl : 3; + bool lalt : 4; + bool ralt : 5; + bool lgui : 6; + bool rgui : 7; + bool num : 8; + bool caps : 9; + bool mode : 10; + bool scroll : 11; + bool bkspc : 12; + bool del : 13; + bool home : 14; + bool end : 15; + // arrow keys + bool up : 16; + bool down : 17; + bool left : 18; + bool right : 19; +} + +const ModKeys KMOD_CTRL = {.lctrl = true, .rctrl = true}; +const ModKeys KMOD_SHIFT = {.lshift = true, .rshift = true}; +const ModKeys KMOD_ALT = {.lalt = true, .ralt = true}; +const ModKeys KMOD_GUI = {.lgui = true, .rgui = true}; +const ModKeys KMOD_TXT = {.bkspc = true, .del = true}; // modkeys that act like text input +const ModKeys KMOD_NONE = {}; +const ModKeys KMOD_ANY = (ModKeys)(uint::max); +//const ModKeys KMOD_ANY = (ModKeys)(ModKeys::inner::max); + +const MouseButtons BTN_NONE = {}; +const MouseButtons BTN_ANY = (MouseButtons)(uint::max); +//const MouseButtons BTN_ANY = (MouseButtons)(MouseButtons::max); +const MouseButtons BTN_LEFT = {.btn_left = true}; +const MouseButtons BTN_MIDDLE = {.btn_middle = true}; +const MouseButtons BTN_RIGHT = {.btn_right = true}; +const MouseButtons BTN_4 = {.btn_4 = true}; +const MouseButtons BTN_5 = {.btn_5 = true}; + +const ModKeys KEY_ANY = (ModKeys)(uint::max); +//const ModKeys KEY_ANY = (ModKeys)(ModKeys::max); + +<* @param [&inout] ctx *> +fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String ...keys) +{ + bool is_mod = (bool)(ctx.current_input.keyboard.modkeys & mod); + bool is_keys = true; + String haystack = (String)ctx.get_keys(); + foreach (needle: keys) { + is_keys = is_keys && haystack.contains(needle); + } + return is_mod && is_keys; +} + +// Window size was changed +<* @param [&inout] ctx *> +fn void? Ctx.input_window_size(&ctx, short width, short height) +{ + if (width <= 0 || height <= 0) { + return INVALID_SIZE~; + } + ctx.current_input.events.resize = ctx.width != (ushort)width || ctx.height != (ushort)height; + ctx.width = (ushort)width; + ctx.height = (ushort)height; + if (ctx.current_input.events.resize) ctx.skip_frame = true; +} + +// Window gained/lost focus +<* @param [&inout] ctx *> +fn void Ctx.input_changefocus(&ctx, bool has_focus) +{ + // FIXME: raylib only has an API to query the focus status so we have to + // update the input flag only if the focus changed + ctx.current_input.events.change_focus = ctx.has_focus != has_focus; + ctx.has_focus = has_focus; +} + +// NOTE: all of these refer to the previous frame's data +macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down; +macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down; +macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down; +macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn) => (ctx.mouse_pressed() & btn) != BTN_NONE; +macro Ctx.is_mouse_released(&ctx, MouseButtons btn) => (ctx.mouse_released() & btn) != BTN_NONE; +macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE; + +// Mouse Buttons down +<* @param [&inout] ctx *> +fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons) +{ + ctx.current_input.mouse.updated = ctx.current_input.mouse.down ^ buttons; + ctx.current_input.mouse.down = buttons; + ctx.current_input.events.mouse_btn = ctx.current_input.mouse.down != BTN_NONE || ctx.current_input.mouse.updated != BTN_NONE; +} + +// Mouse was moved, report absolute position +<* @param [&inout] ctx *> +fn void Ctx.input_mouse_abs(&ctx, short x, short y) +{ + ctx.current_input.mouse.pos.x = math::clamp(x, (short)0, (short)ctx.width); + ctx.current_input.mouse.pos.y = math::clamp(y, (short)0, (short)ctx.height); + + short dx, dy; + dx = x - ctx.current_input.mouse.pos.x; + dy = y - ctx.current_input.mouse.pos.y; + + ctx.current_input.mouse.delta.x = dx; + ctx.current_input.mouse.delta.y = dy; + + ctx.current_input.events.mouse_move = dx != 0 || dy != 0; +} + +// Mouse was moved, report relative motion +<* @param [&inout] ctx *> +fn void Ctx.input_mouse_delta(&ctx, short dx, short dy) +{ + ctx.current_input.mouse.delta.x = dx; + ctx.current_input.mouse.delta.y = dy; + + short mx, my; + mx = ctx.current_input.mouse.pos.x + dx; + my = ctx.current_input.mouse.pos.y + dy; + + ctx.current_input.mouse.pos.x = math::clamp(mx, (short)0, (short)ctx.width); + ctx.current_input.mouse.pos.y = math::clamp(my, (short)0, (short)ctx.height); + + ctx.current_input.events.mouse_move = dx != 0 || dy != 0; +} + +<* @param [&inout] ctx *> +fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0) +{ + ctx.current_input.mouse.scroll.x = (short)((float)-x*scale); + ctx.current_input.mouse.scroll.y = (short)((float)-y*scale); + ctx.current_input.events.mouse_scroll = x !=0 || y != 0; +} + +<* @param [&inout] ctx *> +fn void Ctx.input_key_press(&ctx) +{ + ctx.current_input.events.key_press = true; +} + +<* @param [&inout] ctx *> +fn void Ctx.input_key_release(&ctx) +{ + ctx.current_input.events.key_release = true; +} + +<* @param [&inout] ctx *> +fn void Ctx.input_key_repeat(&ctx) +{ + ctx.current_input.events.key_repeat = true; +} + +// append utf-8 encoded text to the context text input +<* @param [&inout] ctx *> +fn void Ctx.input_text_utf8(&ctx, char[] text) +{ + if (text == "") return; + + sz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len; + sz len = text.len > remaining ? remaining : text.len; + char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..]; + s[..len-1] = text[..len-1]; + ctx.current_input.keyboard.text_len += len; + ctx.current_input.events.text_input = true; +} + +<* +@param [&inout] ctx +@param [in] text +*> +fn void? Ctx.input_text_unicode(&ctx, uint[] text) +{ + if (text.len == 0) return; + + sz remaining = ctx.current_input.keyboard.text.len - ctx.current_input.keyboard.text_len; + char[] s = ctx.current_input.keyboard.text[ctx.current_input.keyboard.text_len ..]; + + sz off = conv::utf32to8(text, s)!; + ctx.current_input.keyboard.text_len += off; + + ctx.current_input.events.text_input = true; +} + +<* @param [&inout] ctx *> +fn void Ctx.input_char(&ctx, char c) +{ + char[1] b = {c}; + ctx.input_text_utf8(b[..]); +} + +fn String Ctx.get_keys(&ctx) => (String)ctx.input.keyboard.text[:ctx.input.keyboard.text_len]; +fn ModKeys Ctx.get_mod(&ctx) => ctx.input.keyboard.modkeys; + +// Modifier keys, like control or backspace +<* @param [&inout] ctx *> +fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys, bool set) +{ + if (set) { + ctx.current_input.keyboard.modkeys |= modkeys; + } else { + ctx.current_input.keyboard.modkeys &= ~modkeys; + } + ctx.current_input.events.mod_key = (uint)ctx.current_input.keyboard.modkeys != 0; +} diff --git a/src/layout.c3 b/src/layout.c3 new file mode 100644 index 0000000..a4a523c --- /dev/null +++ b/src/layout.c3 @@ -0,0 +1,365 @@ +module ugui; + +import std::math; +import std::io; + +enum LayoutDirection { + ROW, + COLUMN, +} + +enum Anchor { + TOP_LEFT, + LEFT, + BOTTOM_LEFT, + BOTTOM, + BOTTOM_RIGHT, + RIGHT, + TOP_RIGHT, + TOP, + CENTER +} + +bitstruct ResizeDirection : char { + bool x : 0; + bool y : 1; +} + +bitstruct ResizeAnchor : char { + bool top : 0; + bool bottom : 1; + bool left : 2; + bool right : 3; +} + +struct Layout { + Size w, h; // size of the CONTENT, does not include margin, border and padding + struct children { // the children size includes the children's margin/border/pading + Size w, h; + } + TextSize text; + ushort grow_children; + short occupied; + Point origin; + Point scroll_offset; + // false: the element is laid out according to the parent + // true: the element is laid out separate from all other children and the relative position to + // the parent is the .origin field + bool absolute; + LayoutDirection dir; // the direction the children are laid out + Anchor anchor; // how the children are positioned + Rect content_offset; // combined effect of margin, border and padding +} + +// Returns the width and height of a @fit() element based on it's wanted size (min/max) +// and the content size, this function is used to both update the parent's children size and +// give the dimensions of a fit element +// TODO: test and cleanup this function +macro Point Layout.get_dimensions(&el) +{ + Point dim; + // if the direction is ROW then the text is placed horizontally with the children + if (el.dir == ROW) { + Size content_width = el.children.w + el.text.width; + Size width = el.w.combine(content_width); + short final_width = width.greater(); + + short text_height; + if (el.text.area != 0) { + short text_width = (@exact(final_width) - el.children.w).combine(el.text.width).min; + text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min; + } + + Size content_height = el.children.h.comb_max(@exact(text_height)); + Size height = el.h.combine(content_height); + short final_height = height.greater(); + + dim = { + .x = final_width + el.content_offset.x + el.content_offset.w, + .y = final_height + el.content_offset.y + el.content_offset.h, + }; + } else { + // if the direction is COLUMN the text and children are one on top of the other + Size content_width = el.children.w.comb_max(el.text.width); + Size width = el.w.combine(content_width); + short final_width = width.greater(); + + short text_height; + if (el.text.area != 0) { + short text_width = @exact(final_width).combine(el.text.width).min; + text_height = @exact((short)(el.text.area / text_width)).combine(el.text.height).min; + } + + Size content_height = el.children.h + @exact(text_height); + Size height = el.h.combine(content_height); + short final_height = height.greater(); + + dim = { + .x = final_width + el.content_offset.x + el.content_offset.w, + .y = final_height + el.content_offset.y + el.content_offset.h, + }; + } + + // GROSS HACK FOR EXACT DIMENSIONS + if (el.w.@is_exact()) dim.x = el.w.min + el.content_offset.x + el.content_offset.w; + if (el.h.@is_exact()) dim.y = el.h.min + el.content_offset.y + el.content_offset.h; + + // GROSS HACK FOR GROW DIMENSIONS + // FIXME: does this always work? + if (el.w.@is_grow()) dim.x = 0; + if (el.h.@is_grow()) dim.y = 0; + + return dim; +} + +// The content space of the element +macro Point Elem.content_space(&e) +{ + return { + .x = (short)max(e.bounds.w - e.layout.content_offset.x - e.layout.content_offset.w, 0), + .y = (short)max(e.bounds.h - e.layout.content_offset.y - e.layout.content_offset.h, 0), + }; +} + +// Update the parent element's children size +fn void update_parent_size(Elem* child, Elem* parent) +{ + Layout* cl = &child.layout; + Layout* pl = &parent.layout; + + // if the element has absolute position do not update the parent + if (cl.absolute) return; + + Point child_size = cl.get_dimensions(); + + switch (pl.dir) { + case ROW: // on rows grow the ch width by the child width and only grow ch height if it exceeds + pl.children.w += @exact(child_size.x); + pl.children.h = pl.children.h.comb_max(@exact(child_size.y)); + if (child.layout.w.@is_grow()) parent.layout.grow_children++; + case COLUMN: // do the opposite on column + pl.children.w = pl.children.w.comb_max(@exact(child_size.x)); + pl.children.h += @exact(child_size.y); + if (child.layout.h.@is_grow()) parent.layout.grow_children++; + } +} + +macro Rect Elem.content_bounds(&elem) => elem.bounds.pad(elem.layout.content_offset); + +// Assign the width and height of an element in the directions that it doesn't need to grow +fn void resolve_dimensions(Elem* e, Elem* p) +{ + Layout* el = &e.layout; + Layout* pl = &p.layout; + + Point elem_dimensions = el.get_dimensions(); + e.bounds.w = elem_dimensions.x; + e.bounds.h = elem_dimensions.y; + + // if the element has absolute position do not update the parent + if (el.absolute) return; + + switch (pl.dir) { + case ROW: + if (!el.w.@is_grow()) pl.occupied += e.bounds.w; + case COLUMN: + if (!el.h.@is_grow()) pl.occupied += e.bounds.h; + } +} + +fn void resolve_grow_elements(Elem* e, Elem* p) +{ + // WIDTH + if (e.layout.w.@is_grow()) { + if (e.layout.absolute) { // absolute children do not need to share space + e.bounds.w = p.content_space().x; + } else if (p.layout.dir == ROW) { // grow along the axis, divide the parent size + short slot = (short)max(((p.content_space().x - p.layout.occupied) / p.layout.grow_children), 0); + e.bounds.w = slot; + p.layout.grow_children--; + p.layout.occupied += slot; + } else if (p.layout.dir == COLUMN) { // grow across the layout axis, inherit width of the parent + e.bounds.w = p.content_space().x + p.layout.scroll_offset.x; + } + } + + // HEIGHT + if (e.layout.h.@is_grow()) { + if (e.layout.absolute) { // absolute children do not need to share space + e.bounds.h = p.content_space().y; + } else if (p.layout.dir == COLUMN) { // grow along the axis, divide the parent size + short slot = (short)max(((p.content_space().y - p.layout.occupied) / p.layout.grow_children), 0); + e.bounds.h = slot; + p.layout.grow_children--; + p.layout.occupied += slot; + } else if (p.layout.dir == ROW) { // grow across the layout axis, inherit width of the parent + e.bounds.h = p.content_space().y + p.layout.scroll_offset.y; + } + } +} + +fn void resolve_placement(Elem* c, Elem* p) +{ + Layout* pl = &p.layout; + Layout* cl = &c.layout; + + Point off = { + .x = p.bounds.x + pl.origin.x + pl.content_offset.x, + .y = p.bounds.y + pl.origin.y + pl.content_offset.y, + }; + + // if the element has absolute position assign the origin and do not update the parent + if (cl.absolute) { + c.bounds.x = p.bounds.x + pl.content_offset.x + cl.origin.x; + c.bounds.y = p.bounds.y + pl.content_offset.y + cl.origin.y; + return; + } + + switch (pl.anchor) { + case TOP_LEFT: + c.bounds.x = off.x; + c.bounds.y = off.y; + case LEFT: + c.bounds.x = off.x; + c.bounds.y = off.y + p.content_space().y/2; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied/2; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h/2; + } + case BOTTOM_LEFT: + c.bounds.x = off.x; + c.bounds.y = off.y + p.content_space().y ; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h; + } + case BOTTOM: + c.bounds.x = off.x + p.content_space().x/2; + c.bounds.y = off.y + p.content_space().y; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied; + c.bounds.x -= c.bounds.w/2; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h; + c.bounds.x -= pl.occupied/2; + } + case BOTTOM_RIGHT: + c.bounds.x = off.x + p.content_space().x; + c.bounds.y = off.y + p.content_space().y; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied; + c.bounds.x -= c.bounds.w; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h; + c.bounds.x -= pl.occupied; + } + case RIGHT: + c.bounds.x = off.x + p.content_space().x; + c.bounds.y = off.y + p.content_space().y/2; + if (pl.dir == COLUMN) { + c.bounds.y -= pl.occupied/2; + c.bounds.x -= c.bounds.w; + } else if (pl.dir == ROW) { + c.bounds.y -= c.bounds.h/2; + c.bounds.x -= pl.occupied; + } + case TOP_RIGHT: + c.bounds.x = off.x + p.content_space().x; + c.bounds.y = off.y; + if (pl.dir == COLUMN) { + c.bounds.x -= c.bounds.w; + } else if (pl.dir == ROW) { + c.bounds.x -= pl.occupied; + } + case TOP: + c.bounds.x = off.x + p.content_space().x/2; + c.bounds.y = off.y; + if (pl.dir == COLUMN) { + c.bounds.x -= c.bounds.w/2; + } else if (pl.dir == ROW) { + c.bounds.x -= pl.occupied/2; + } + case CENTER: + c.bounds.x = off.x + p.content_space().x/2; + c.bounds.y = off.y + p.content_space().y/2; + if (pl.dir == COLUMN) { + c.bounds.x -= c.bounds.w/2; + c.bounds.y -= pl.occupied/2; + } else if (pl.dir == ROW) { + c.bounds.x -= pl.occupied/2; + c.bounds.y -= c.bounds.h/2; + } + break; + } + + switch (pl.dir) { + case ROW: + pl.origin.x += c.bounds.w; + case COLUMN: + pl.origin.y += c.bounds.h; + default: unreachable("unknown layout direction"); + } + + // update the parent children_bounds + // FIXME: this causes scollbars to flicker in/out during resize because the current frames children_bounds are updated + // with the previous' frame child bounds. It would be better to implement another pass during layout. + // FIXME: this long way around to compute the children bounds works and reduces flickering, but it is very ugly + Rect ncb = c.children_bounds; + ncb.x = c.bounds.x; + ncb.y = c.bounds.y; + Rect cb = containing_rect(c.bounds, ncb); + Point o = p.layout.scroll_offset; + p.children_bounds = containing_rect(cb + o, p.children_bounds); + + // reset the children bounds + c.children_bounds = { + .x = c.bounds.x, + .y = c.bounds.y + }; +} + +fn void? Ctx.layout_element_tree(&ctx) +{ + int current; + for (int n; (current = ctx.tree.level_order_it(0, n)) >= 0; n++) { + Elem* p = ctx.find_elem(ctx.tree.get(current))!; + p.layout.origin = -p.layout.scroll_offset; + + int ch; + // RESOLVE KNOWN DIMENSIONS + for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) { + Elem* c = ctx.find_elem(ctx.tree.get(ch))!; + if (ctx.tree.is_root(ch)) { + resolve_dimensions(p, &&{}); + } else { + resolve_dimensions(c, p); + } + } + + // RESOLVE GROW CHILDREN + for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) { + Elem* c = ctx.find_elem(ctx.tree.get(ch))!; + if (ctx.tree.is_root(ch)) { + resolve_grow_elements(p, &&{}); + } else { + resolve_grow_elements(c, p); + } + } + + // RESOLVE CHILDREN PLACEMENT + for (int i; (ch = ctx.tree.children_it(current, i)) >= 0; i++) { + Elem* c = ctx.find_elem(ctx.tree.get(ch))!; + if (ctx.tree.is_root(ch)) { + resolve_placement(p, &&{}); + } else { + resolve_placement(c, p); + } + + // FIXME: this stuff would be better elsewhere but we are already iteraring through all + // elements so here it fits really well + ctx.update_hover_and_focus(c); + } + } +} diff --git a/src/main.c3 b/src/main.c3 deleted file mode 100644 index fdbe79e..0000000 --- a/src/main.c3 +++ /dev/null @@ -1,314 +0,0 @@ -import std::io; -import vtree; -import cache; -import ugui; -import std::time; -import std::collections::ringbuffer; -import std::core::string; -import std::ascii; -import sdlrenderer::ren; -import sdl3::sdl; - -alias Times = ringbuffer::RingBuffer{time::NanoDuration[128]}; - -fn void Times.print_stats(×) -{ - if (times.written == 0); - time::NanoDuration min, max, avg, x; - min = times.get(0); - for (usz i = 0; i < times.written; i++) { - x = times.get(i); - if (x < min) { min = x; } - if (x > max) { max = x; } - avg += x; - } - avg = (NanoDuration)((ulong)avg/times.written); - - io::printfn("min=%s, max=%s, avg=%s", min, max, avg); -} - -struct TimeStats { - time::NanoDuration min, max, avg; -} - -fn TimeStats Times.get_stats(×) -{ - if (times.written == 0) return {}; - time::NanoDuration min, max, avg, x; - min = times.get(0); - for (usz i = 0; i < times.written; i++) { - x = times.get(i); - if (x < min) { min = x; } - if (x > max) { max = x; } - avg += x; - } - avg = (NanoDuration)((ulong)avg/times.written); - - return {.min = min, .max = max, .avg = avg}; -} - - -const char[*] MSDF_FS_PATH = "resources/shaders/compiled/msdf.frag.spv"; -const char[*] SPRITE_FS_PATH = "resources/shaders/compiled/sprite.frag.spv"; -const char[*] FONT_FS_PATH = "resources/shaders/compiled/font.frag.spv"; -const char[*] RECT_FS_PATH = "resources/shaders/compiled/rect.frag.spv"; - -const char[*] SPRITE_VS_PATH = "resources/shaders/compiled/sprite.vert.spv"; -const char[*] RECT_VS_PATH = "resources/shaders/compiled/rect.vert.spv"; - -const char[*] STYLESHEET_PATH = "resources/style.css"; - -fn int main(String[] args) -{ - ugui::Ctx ui; - ui.init()!!; - defer ui.free(); - - ren::Renderer ren; - ren.init("Ugui Test", 800, 600, true); - defer ren.free(); - ui.input_window_size(800, 600)!!; - - // - // FONT LOADING - // - { - // import font in the ui context - ui.load_font("font1", "resources/hack-nerd.ttf", 16)!!; - - // create the rendering pipeline - ren.font_atlas_id = ui.get_font_id("font1"); - ren.load_spirv_shader_from_file("UGUI_PIPELINE_FONT", SPRITE_VS_PATH, FONT_FS_PATH, 1, 0); - ren.create_pipeline("UGUI_PIPELINE_FONT", SPRITE); - - // send the atlas to the gpu - Atlas* font_atlas = ui.get_font_atlas("font1")!!; - ren.new_texture("font1", JUST_ALPHA, font_atlas.buffer, font_atlas.width, font_atlas.height); - } - - // - // ICON LOADING - // - { - // create the atlas and upload some icons - ui.sprite_atlas_create("icons", AtlasType.ATLAS_R8G8B8A8, 512, 512)!!; - ui.import_sprite_file_qoi("tux", "resources/tux.qoi")!!; - ui.import_sprite_file_qoi("tick", "resources/tick_sdf.qoi", SpriteType.SPRITE_MSDF)!!; - - // create the rendering pipelines - ren.sprite_atlas_id = ui.get_sprite_atlas_id("icons"); - // normal sprite pipeline - ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE", SPRITE_VS_PATH, SPRITE_FS_PATH, 1, 0); - ren.create_pipeline("UGUI_PIPELINE_SPRITE", SPRITE); - // msdf sprite pipeline - ren.load_spirv_shader_from_file("UGUI_PIPELINE_SPRITE_MSDF", SPRITE_VS_PATH, MSDF_FS_PATH, 1, 0); - ren.create_pipeline("UGUI_PIPELINE_SPRITE_MSDF", SPRITE); - - // upload the atlas to the gpu - Atlas atlas = ui.sprite_atlas.atlas; - ren.new_texture("icons", FULL_COLOR, atlas.buffer, atlas.width, atlas.height); - } - - // - // RECT PIPELINE - // - ren.load_spirv_shader_from_file("UGUI_PIPELINE_RECT", RECT_VS_PATH, RECT_FS_PATH, 0, 0); - ren.create_pipeline("UGUI_PIPELINE_RECT", RECT); - - - // CSS INPUT - io::printfn("imported %d styles", ui.import_style_from_file(STYLESHEET_PATH)); - - isz frame; - double fps; - bool toggle = true; - time::Clock clock; - time::Clock fps_clock; - time::Clock sleep_clock; - Times ui_times; - Times draw_times; - - - // - // MAIN LOOP - // - sdl::start_text_input(ren.win); - - sdl::Event e; - bool quit = false; - ugui::ModKeys mod; - ugui::MouseButtons btn; - while (!quit) { - clock.mark(); - fps_clock.mark(); - sleep_clock.mark(); - - do { - switch (e.type) { - case EVENT_QUIT: - quit = true; - case EVENT_KEY_UP: nextcase; - case EVENT_KEY_DOWN: - mod.rctrl = e.key.key == K_RCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.rctrl; - mod.lctrl = e.key.key == K_LCTRL ? !!(e.type == EVENT_KEY_DOWN) : mod.lctrl; - mod.bkspc = e.key.key == K_BACKSPACE ? !!(e.type == EVENT_KEY_DOWN) : mod.bkspc; - - // pressing ctrl+key or alt+key does not generate a character as such no - // TEXT_INPUT event is generated. When those keys are pressed we have to - // do manual text input, bummer - if (e.type == EVENT_KEY_DOWN && (mod.lctrl || mod.rctrl)) { - if (ascii::is_alnum_m((uint)e.key.key)) { - ui.input_char((char)e.key.key); - } - } - - if (e.type == EVENT_KEY_DOWN && e.key.key == K_RETURN) ui.input_char('\n'); - - case EVENT_TEXT_INPUT: - ui.input_text_utf8(e.text.text.str_view()); - case EVENT_WINDOW_RESIZED: - ui.input_window_size((short)e.window.data1, (short)e.window.data2)!!; - case EVENT_WINDOW_FOCUS_GAINED: - ui.input_changefocus(true); - case EVENT_WINDOW_FOCUS_LOST: - ui.input_changefocus(false); - case EVENT_MOUSE_MOTION: - ui.input_mouse_abs((short)e.motion.x, (short)e.motion.y); - case EVENT_MOUSE_WHEEL: - ui.input_mouse_wheel((short)e.wheel.integer_x, (short)e.wheel.integer_y); - case EVENT_MOUSE_BUTTON_DOWN: nextcase; - case EVENT_MOUSE_BUTTON_UP: - sdl::MouseButtonFlags mb = sdl::get_mouse_state(null, null); - btn = { - .btn_left = !!(mb & BUTTON_LMASK), - .btn_right = !!(mb & BUTTON_RMASK), - .btn_middle = !!(mb & BUTTON_MMASK), - .btn_4 = !!(mb & BUTTON_X1MASK), - .btn_5 = !!(mb & BUTTON_X2MASK), - }; - case EVENT_POLL_SENTINEL: break; - default: - io::eprintfn("unhandled event: %s", e.type); - } - } while(sdl::poll_event(&e)); - ui.input_mod_keys(mod); - ui.input_mouse_button(btn); - - /* End Input Handling */ - - /* Start UI Handling */ - ui.frame_begin()!!; - - if (ui.check_key_combo(ugui::KMOD_CTRL, "q")) quit = true; - - ui.div_begin({.w=-100})!!; - { - ui.layout_set_column()!!; - if (ui.button({0,0,30,30}, toggle)!!.mouse_press) { - io::printn("press button0"); - toggle = !toggle; - } - //ui.layout_next_column()!!; - if (ui.button({0,0,30,30})!!.mouse_press) { - io::printn("press button1"); - } - //ui.layout_next_column()!!; - if (ui.button({0,0,30,30})!!.mouse_release) { - io::printn("release button2"); - } - - ui.layout_set_row()!!; - ui.layout_next_row()!!; - static float rf, gf, bf, af; - ui.slider_ver({0,0,30,100}, &rf)!!; - ui.slider_ver({0,0,30,100}, &gf)!!; - ui.slider_ver({0,0,30,100}, &bf)!!; - ui.slider_ver({0,0,30,100}, &af)!!; - - ui.layout_next_column()!!; - ui.text_unbounded("Ciao Mamma\nAbilità âš¡\n'\udb80\udd2c'")!!; - - ui.layout_next_column()!!; - ui.button_label("Continua!")!!; - - ui.layout_next_row()!!; - static bool check; - ui.checkbox("", {}, &check, "tick")!!; - ui.checkbox("", {}, &check)!!; - ui.toggle("", {}, &toggle)!!; - }; - ui.sprite("tux")!!; - - static char[128] text_box = "ciao mamma"; - static usz text_len = "ciao mamma".len; - ui.text_box({0,0,200,200}, text_box[..], &text_len)!!; - - ui.div_end()!!; - - ui.div_begin(ugui::DIV_FILL, scroll_x: true, scroll_y: true)!!; - { - ui.layout_set_column()!!; - static float slider2 = 0.5; - if (ui.slider_ver({0,0,30,100}, &slider2)!!.update) { - io::printfn("other slider: %f", slider2); - } - ui.button({0,0,50,50})!!; - ui.button({0,0,50,50})!!; - ui.button({0,0,50,50})!!; - ui.button({0,0,50,50})!!; - if (toggle) { - ui.button({0,0,50,50})!!; - ui.button({0,0,50,50})!!; - ui.button({0,0,50,50})!!; - ui.button({0,0,50,50})!!; - } - ui.layout_next_column()!!; - ui.layout_set_row()!!; - static float f1, f2; - ui.slider_hor({0,0,100,30}, &f1)!!; - ui.slider_hor({0,0,100,30}, &f2)!!; - }; - ui.div_end()!!; - - // Timings counter - TimeStats dts = draw_times.get_stats(); - TimeStats uts = ui_times.get_stats(); - - ui.layout_set_floating()!!; - // FIXME: I cannot anchor shit to the bottom of the screen - ui.div_begin({0, ui.height-150, -300, 150})!!; - { - ui.layout_set_column()!!; - ui.text_unbounded(string::tformat("frame %d, fps = %.2f", frame, fps))!!; - ui.text_unbounded(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!; - ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[..]))!!; - }; - ui.div_end()!!; - - ui.frame_end()!!; - /* End UI Handling */ - ui_times.push(clock.mark()); - //ui_times.print_stats(); - - - /* Start UI Drawing */ - ren.begin_render(true); - - ren.render_ugui(&ui.cmd_queue); - - ren.end_render(); - - draw_times.push(clock.mark()); - //draw_times.print_stats(); - /* End Drawing */ - - // wait for the next event, timeout after 100ms - - sdl::wait_event_timeout(&e, (int)(100.0-sleep_clock.mark().to_ms()-0.5)); - - fps = 1.0 / fps_clock.mark().to_sec(); - frame++; - } - - return 0; -} diff --git a/src/mtree.c3 b/src/mtree.c3 new file mode 100644 index 0000000..c0042bf --- /dev/null +++ b/src/mtree.c3 @@ -0,0 +1,367 @@ +module mtree; + +/* ================================================================================================ + * MTree, Bitmap-based tree + * ================================================================================================ + * + * Overview + * -------- + * The MTree is a bitmap-based tree structure composed of three core elements: + * - Element Vector: Stores user data. + * - Reference Node Vector: Manages node relationships. + * - Bitmap: Marks used indices. + * + * The name "MTree" originates from "Matrix Tree," where the vector is divided into + * sectors of power-of-two sizes. Each node's bitmap marks the positions of its + * children within the same sector. + * + * If a parent and its children are in different sectors, a new node is created. + * The parent's "next" field points to this new node, forming a chain that must + * be traversed during iteration. + * + * + * Example (sector size = 8) + * ------------------------- + * + * _________________________________ + * |__ __ _______________________ | + * | | | | _ | + * | v v vv |v + * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + * refs_vec:| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|13|14|15|16|... + * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + * \__________ __________/ \__________ __________/ \__ + * V V + * sector 0 sector 1 + * + * + * Node Relationships: + * ------------------- + * - Root (Element 0) has three direct children: 1, 2, and 10. + * - Node 10 is in a different sector than the root, so root.next points to Node 11. + * - Node 11 has Node 10 as a direct child and Node 0 (root) as its parent. + * + * Bitmap Representation: + * --------------------- + * + * root = { + * .parent = -1; // Root has no parent + * .next = 11; // Points to Node 11 + * .children = 0b00000110; // [0|1|1|0|0|0|0|0] (Children: 1, 2) + * } + * + * node11 = { + * .parent = 0; // Parent is root (Node 0) + * .next = -1; // Last in the chain + * .children = 0b00000100; // [0|0|1|0|0|0|0|0] (Child: 10) + * } + * + * ================================================================================================ + */ + +import std::core::mem; +import std::core::mem::alloc; +import std::io; +import std::bits; +import std::collections::list; + + +alias Bitmap = ulong; +const BITS = Bitmap::size*8; + +alias IdxList = List{int}; + +// next: if positive it contains the index of the next node that contains the children information +struct RefNode { + int next; + int parent; + Bitmap children; +} + +struct MTree { + usz elements; + Allocator allocator; + IdxList queue; + Bitmap[] used; + Type[] elem_vec; // element vector + RefNode[] refs_vec; // relationship vector +} + + +<* @param [&inout] tree *> +fn void MTree.init(&tree, usz size, Allocator allocator = mem) +{ + // round size to the nearest multiple of BITS + size = size + size%BITS; + + tree.elements = 0; + tree.allocator = allocator; + tree.queue.init(tree.allocator, (sz)size); + tree.used = alloc::new_array(tree.allocator, Bitmap, (sz)size/BITS); + tree.elem_vec = alloc::new_array(tree.allocator, Type, (sz)size); + tree.refs_vec = alloc::new_array(tree.allocator, RefNode, (sz)size); + + foreach (&r: tree.refs_vec) { + r.next = -1; + } +} + + +<* @param [&inout] tree *> +fn void MTree.free(&tree) +{ + tree.elements = 0; + tree.queue.free(); + (void)alloc::free(tree.allocator, tree.used); + (void)alloc::free(tree.allocator, tree.elem_vec); + (void)alloc::free(tree.allocator, tree.refs_vec); +} + + +<* @param [&inout] tree *> +fn int? MTree.get_free_spot(&tree) +{ + foreach (idx, d: tree.used) { + if (d != $Typeof(d)::max) { + int spot = (int)idx*BITS + BITS-(int)d.clz(); + return spot; + } + } + return CAPACITY_EXCEEDED~; +} + +<* @require idx >= 0 *> +macro void MTree.set_used(&tree, int idx) +{ + int r = idx % BITS; + int q = idx / BITS; + tree.used[q] |= (1l << r); +} + +<* @require idx >= 0 *> +macro void MTree.unset_used(&tree, int idx) +{ + int r = idx % BITS; + int q = idx / BITS; + tree.used[q] &= ~(1l << r); +} + +<* @require idx >= 0 *> +macro bool MTree.is_used(&tree, int idx) +{ + int r = idx % BITS; + int q = idx / BITS; + return !!(tree.used[q] & (1l << r)); +} + + +// get the last node in the "next" chain +<* @require tree.is_used(parent) == true *> +fn int MTree.last_node(&tree, int parent) +{ + while(tree.refs_vec[parent].next >= 0) { + parent = tree.refs_vec[parent].next; + } + return parent; +} + + +<* +@require tree.elements == 0 || tree.is_used(parent) == true +@param [&inout] tree +*> +fn int? MTree.add(&tree, int parent, Type t) +{ + int idx = tree.get_free_spot()!; + int subtree = idx / BITS; + + tree.set_used(idx); + tree.elem_vec[idx] = t; + tree.refs_vec[idx] = (RefNode){ + .parent = parent, + .next = -1, + }; + + tree.elements++; + // root element, has no parent + if (tree.elements == 1) { + tree.refs_vec[idx].parent = -1; + return idx; + } + + + // if the parent already has a node in the same subtree as the child then update that node's + // children bitmap + bool done; + for (int p = parent; p >= 0; p = tree.refs_vec[p].next) { + int ps = p/BITS; + if (ps == subtree) { + tree.refs_vec[p].children |= (1l << (idx%BITS)); + done = true; + break; + } + } + // on fail we need to create another parent node + if (!done) { + int new_next = tree.get_free_spot()!; + // if the new node does not land in the same subtree as the child we cannot do + // anything since the references are immutable + if (new_next/BITS != subtree) { + return CAPACITY_EXCEEDED~; + } + tree.set_used(new_next); + tree.elements++; + // update the "next" chain + int last_link = tree.last_node(parent); + tree.refs_vec[last_link].next = new_next; + tree.refs_vec[new_next].next = -1; + tree.refs_vec[new_next].children |= (long)(1 << (idx%BITS)); + tree.refs_vec[new_next].parent = last_link; + // FIXME: the elem_vec is not updated, do we need to? + } + + return idx; +} + + +// get the index of the n-th children of parent, -1 otherwise +// usage: for (int i, c; (c = tree.children_it(parent, i)) >= 0; i++) { ... } +<* @param [&in] tree *> +fn int MTree.children_it(&tree, int parent, int n) +{ + int tot_children; + int child; + for (int p = parent; p >= 0; p = tree.refs_vec[p].next) { + int cn = (int)tree.refs_vec[p].children.popcount(); + tot_children += cn; + + // we are in the right subtree + if (tot_children > n) { + child = (p/BITS) * BITS; // start at the parent's subtree index + int j = cn - (tot_children - n); // we need the j-th children of this node + Bitmap u = tree.refs_vec[p].children; + + child += j; // add the children number + do { + child += (int)u.ctz(); // increment by the skipped zeroes + u >>= u.ctz() + 1; + j--; + } while (j >= 0); + + return child; + } + } + return -1; +} + +<* @param [&in] tree *> +fn int MTree.children_num(&tree, int parent) +{ + int n; + for (int p = parent; p >= 0; p = tree.refs_vec[p].next) { + n += (int)tree.refs_vec[p].children.popcount(); + } + return n; +} + +<* @param [&in] tree *> +fn int MTree.subtree_size(&tree, int parent) +{ + int x = tree.children_num(parent); + int c; + for (int n; (c = tree.children_it(parent, n)) >= 0; n++) { + x += tree.subtree_size(c); + } + return x; +} + +<* @param [&inout] tree *> +fn int MTree.level_order_it(&tree, int parent, int i) +{ + if (i == 0) { + tree.queue.clear(); + tree.queue.push(parent); + } + + if (tree.queue.len() == 0) return -1; + + int p = tree.queue.pop_first()!!; + int c; + for (int n; (c = tree.children_it(p, n)) >= 0; n++) { + tree.queue.push(c); + } + return p; +} + +<* @param [&inout] tree *> +fn void MTree.prune(&tree, int parent) +{ + if (!tree.is_used(parent)) return; + + int c; + for (int i = 0; (c = tree.children_it(parent, i)) >= 0; i++) { + tree.prune(c); // prune the subtree + + // delete all children including their next chain + for (int p = c; p >= 0;) { + int next = tree.refs_vec[p].next; + tree.unset_used(p); + tree.refs_vec[p] = {.next = -1}; + p = next; + } + + } + + // finally delete the parent + for (int p = parent; p >= 0;) { + int next = tree.refs_vec[p].next; + tree.unset_used(p); + tree.elements--; + tree.refs_vec[p] = {.next = -1}; + p = next; + } + +} + +<* +@require ref >= 0 , ref < tree.elem_vec.len +@param [&inout] tree +*> +fn Type? MTree.get(&tree, int ref) @operator([]) +{ + if (tree.is_used(ref)) return tree.elem_vec[ref]; + return NOT_FOUND~; +} + +<* @param [&in] tree *> +fn int? MTree.parentof(&tree, int ref) +{ + if (!tree.is_used(ref)) return NOT_FOUND~; + return tree.refs_vec[ref].parent; +} + +<* @param [&inout] tree *> +fn void MTree.nuke(&tree) +{ + foreach (idx, &b: tree.used) { + *b = 0; + tree.refs_vec[idx] = {.next = -1}; + } + tree.elements = 0; +} + +<* @param [&in] t *> +macro bool MTree.is_root(&t, int i) => t.is_used(i) && t.refs_vec[i].parent == -1; + +<* @param [&in] tree *> +fn void MTree.print(&tree) +{ + foreach (idx, c: tree.elem_vec) { + if (tree.is_used((int)idx)) { + io::printfn("[%d](%s) parent:%d next:%d children:%b", + idx, c, tree.refs_vec[idx].parent, tree.refs_vec[idx].next, + tree.refs_vec[idx].children + ); + } + } +} diff --git a/src/renderer.c3 b/src/renderer.c3 deleted file mode 100644 index 0b286c9..0000000 --- a/src/renderer.c3 +++ /dev/null @@ -1,962 +0,0 @@ -module idlist{Type}; - -// extends the List type to search elements that have a type.id property -// TODO: check that type has an id - -import std::collections::list; - -alias IdList = List{Type}; - -macro Type* IdList.get_from_name(&self, String name) -{ - return self.get_from_id(name.hash()); -} - -macro Type* IdList.get_from_id(&self, id) -{ - foreach(&s: self) { - if (s.id == id) { - return s; - } - } - return null; -} - -module sdlrenderer::ren; - -// 2D renderer for ugui, based on SDL3 using the new GPU API - -import std::io; -import std::core::mem; -import sdl3::sdl; -import idlist; -import ugui; - -struct Shader { - sdl::GPUShader* frag; - sdl::GPUShader* vert; - ugui::Id id; -} - -struct Pipeline { - sdl::GPUGraphicsPipeline* pipeline; - ugui::Id id; -} - -struct Texture { - sdl::GPUTexture* texture; - sdl::GPUSampler* sampler; - ushort width, height; - ugui::Id id; -} - -// The GPU buffers that contain quad info, the size is determined by MAX_QUAD_BATCH -const int MAX_QUAD_BATCH = 2048; -struct QuadBuffer { - sdl::GPUBuffer* vert_buf; // on-gpu vertex buffer - sdl::GPUBuffer* idx_buf; // on-gpu index buffer - sdl::GPUBuffer* attr_buf; // on-gpu quad attribute buffer - - sdl::GPUTransferBuffer* attr_ts; - - QuadAttributes[] attr_ts_mapped; - - // how many quads are currently stored - int count; - - bool initialized; -} - -alias ShaderList = IdList{Shader}; -alias PipelineList = IdList{Pipeline}; -alias TextureList = IdList{Texture}; - -struct Renderer { - sdl::Window* win; - sdl::GPUDevice* gpu; - sdl::GPURenderPass* render_pass; - sdl::GPUTexture* swapchain_texture; - sdl::GPUCommandBuffer* render_cmdbuf; - - QuadBuffer quad_buffer; - ShaderList shaders; - PipelineList pipelines; - TextureList textures; - - Id sprite_atlas_id; - Id font_atlas_id; - - int scissor_x, scissor_y, scissor_w, scissor_h; -} - -// How each vertex is represented in the gpu -struct Vertex { - short x, y; -} - -// Attributes of each quad instance -struct QuadAttributes { - struct pos { - short x, y, w, h; - } - struct uv { - short u, v; - } - uint color; -} - -// A single quad -struct Quad { - struct vertices { - Vertex v1,v2,v3,v4; - } - struct indices { - short i1,i2,i3,i4,i5,i6; - } -} - -struct ViewsizeUniform @align(16) { - int w, h; -} - -const int DEBUG = 1; -const bool CYCLE = true; - -fn void Renderer.init(&self, ZString title, uint width, uint height, bool vsync) -{ - // set wayland hint automagically -$if DEBUG == 0: - bool has_wayland = false; - for (int i = 0; i < sdl::get_num_video_drivers(); i++) { - ZString driver = sdl::get_video_driver(i); - if (driver.str_view() == "wayland") { - has_wayland = true; - break; - } - } - - if (has_wayland) { - sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "wayland"); - } -$else - // in debug mode set the video driver to X11 because renderdoc - // doesn't support debugging in wayland yet. - sdl::set_hint(sdl::HINT_VIDEO_DRIVER, "x11"); - sdl::set_hint(sdl::HINT_RENDER_GPU_DEBUG, "1"); -$endif - - // init subsystems - if (!sdl::init(INIT_VIDEO)) { - unreachable("sdl error: %s", sdl::get_error()); - } - - // create the window - self.win = sdl::create_window(title, width, height, WINDOW_RESIZABLE|WINDOW_VULKAN); - if (self.win == null) { - unreachable("sdl error: %s", sdl::get_error()); - } - - // get the gpu device handle - self.gpu = sdl::create_gpu_device(GPU_SHADERFORMAT_SPIRV, true, "vulkan"); - if (self.gpu == null) { - unreachable("failed to create gpu device: %s", sdl::get_error()); - } - - if (!sdl::claim_window_for_gpu_device(self.gpu, self.win)) { - unreachable("failed to claim window for use with gpu: %s", sdl::get_error()); - } - - // set swapchain parameters, like vsync - GPUPresentMode present_mode = vsync ? GPU_PRESENTMODE_VSYNC : GPU_PRESENTMODE_IMMEDIATE; - sdl::set_gpu_swapchain_parameters(self.gpu, self.win, GPU_SWAPCHAINCOMPOSITION_SDR, present_mode); - - // - // initialize the quad buffer - // ========================== - QuadBuffer* qb = &self.quad_buffer; - - // since instanced rendering is used, on the gpu there is only one mesh, a single quad. - - // create the vertex and index buffer on the gpu - qb.vert_buf = sdl::create_gpu_buffer(self.gpu, - &&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = Quad.vertices.sizeof} - ); - if (qb.vert_buf == null) { - unreachable("failed to initialize quad buffer (vertex): %s", sdl::get_error()); - } - - qb.idx_buf = sdl::create_gpu_buffer(self.gpu, - &&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_INDEX, .size = Quad.indices.sizeof} - ); - if (qb.idx_buf == null) { - unreachable("failed to initialize quad buffer (index): %s", sdl::get_error()); - } - - qb.attr_buf = sdl::create_gpu_buffer(self.gpu, - &&(GPUBufferCreateInfo){.usage = GPU_BUFFERUSAGE_VERTEX, .size = QuadAttributes.sizeof * MAX_QUAD_BATCH} - ); - if (qb.attr_buf == null) { - unreachable("failed to initialize quad buffer (index): %s", sdl::get_error()); - } - - - // upload the quad mesh - GPUTransferBuffer *ts = sdl::create_gpu_transfer_buffer(self.gpu, - &&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = Quad.sizeof} - ); - if (ts == null) { - unreachable("failed to create gpu transfer buffer: %s", sdl::get_error()); - } - Quad* quad = (Quad*)sdl::map_gpu_transfer_buffer(self.gpu, ts, false); - - /* v1 v4 - * +-------------+ - * | _/| - * | _/ | - * | 1 _/ | - * | _/ | - * | _/ | - * | _/ 2 | - * |/ | - * +-------------+ - * v2 v3 - */ - quad.vertices.v1 = {.x = 0, .y = 0}; - quad.vertices.v2 = {.x = 0, .y = 1}; - quad.vertices.v3 = {.x = 1, .y = 1}; - quad.vertices.v4 = {.x = 1, .y = 0}; - // triangle 1 indices - quad.indices.i1 = 0; // v1 - quad.indices.i2 = 1; // v2 - quad.indices.i3 = 3; // v4 - // triangle 2 indices - quad.indices.i4 = 1; // v2 - quad.indices.i5 = 2; // v3 - quad.indices.i6 = 3; // v4 - - sdl::unmap_gpu_transfer_buffer(self.gpu, ts); - - GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu); - if (cmd == null) { - unreachable("failed to upload quad at acquiring command buffer: %s", sdl::get_error()); - } - GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd); - - // upload vertices - sdl::upload_to_gpu_buffer(cpy, - &&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.vertices.offsetof}, - &&(GPUBufferRegion){.buffer = qb.vert_buf, .offset = 0, .size = Quad.vertices.sizeof}, - false - ); - // upload indices - sdl::upload_to_gpu_buffer(cpy, - &&(GPUTransferBufferLocation){.transfer_buffer = ts, .offset = Quad.indices.offsetof}, - &&(GPUBufferRegion){.buffer = qb.idx_buf, .offset = 0, .size = Quad.indices.sizeof}, - false - ); - - sdl::end_gpu_copy_pass(cpy); - if (!sdl::submit_gpu_command_buffer(cmd)) { - unreachable("failed to upload quads at submit command buffer: %s", sdl::get_error()); - } - sdl::release_gpu_transfer_buffer(self.gpu, ts); - - - // create and map the quad attributes transfer buffer - qb.attr_ts = sdl::create_gpu_transfer_buffer(self.gpu, - &&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = QuadAttributes.sizeof * MAX_QUAD_BATCH} - ); - if (qb.attr_ts == null) { - unreachable("failed to create gpu transfer buffer: %s", sdl::get_error()); - } - qb.attr_ts_mapped = ((QuadAttributes*)sdl::map_gpu_transfer_buffer(self.gpu, qb.attr_ts, false))[:MAX_QUAD_BATCH]; - if (qb.attr_ts_mapped.ptr == null) { - unreachable("failed to map vertex or index buffers: %s", sdl::get_error()); - } - - - qb.initialized = true; -} - -fn void Renderer.free(&self) -{ - foreach (&s: self.shaders) { - sdl::release_gpu_shader(self.gpu, s.frag); - sdl::release_gpu_shader(self.gpu, s.vert); - } - self.shaders.free(); - - foreach (&p: self.pipelines) { - sdl::release_gpu_graphics_pipeline(self.gpu, p.pipeline); - } - self.pipelines.free(); - - foreach (&t: self.textures) { - sdl::release_gpu_texture(self.gpu, t.texture); - sdl::release_gpu_sampler(self.gpu, t.sampler); - } - self.textures.free(); - - QuadBuffer* qb = &self.quad_buffer; - sdl::unmap_gpu_transfer_buffer(self.gpu, qb.attr_ts); - sdl::release_gpu_transfer_buffer(self.gpu, qb.attr_ts); - sdl::release_gpu_buffer(self.gpu, qb.vert_buf); - sdl::release_gpu_buffer(self.gpu, qb.idx_buf); - sdl::release_gpu_buffer(self.gpu, qb.attr_buf); - - sdl::release_window_from_gpu_device(self.gpu, self.win); - sdl::destroy_gpu_device(self.gpu); - sdl::destroy_window(self.win); - sdl::quit(); -} - -fn void Renderer.resize_window(&self, uint width, uint height) -{ - sdl::set_window_size(self.win, width, height); -} - -fn void Renderer.get_window_size(&self, int* width, int* height) -{ - sdl::get_window_size_in_pixels(self.win, width, height); -} - - -// Both the vertex shader and fragment shader have an implicit uniform buffer at binding 0 that -// contains the viewport size. It is populated automatically at every begin_render() call -fn void Renderer.load_spirv_shader_from_mem(&self, String name, char[] vert_code, char[] frag_code, uint textures, uint uniforms) -{ - Shader s; - s.id = name.hash(); - - if (vert_code.len == 0 || frag_code.len == 0) { - unreachable("vertex shader and fragment shader cannot be empty"); - } - - if (vert_code.len > 0) { - // FIXME: these should be passed by parameter and/or automatically determined by parsing - // the shader code - GPUShaderCreateInfo shader_info = { - .code = vert_code.ptr, - .code_size = vert_code.len, - .entrypoint = "main", - .format = GPU_SHADERFORMAT_SPIRV, - .stage = GPU_SHADERSTAGE_VERTEX, - .num_samplers = 0, - .num_uniform_buffers = 1+uniforms, - .num_storage_buffers = 0, - .num_storage_textures = 0 - }; - - s.vert = sdl::create_gpu_shader(self.gpu, &shader_info); - if (s.vert == null) { - unreachable("failed to create gpu vertex shader: %s", sdl::get_error()); - } - } - - if (frag_code.len > 0) { - // FIXME: these should be passed by parameter and/or automatically determined by parsing - // the shader code - GPUShaderCreateInfo shader_info = { - .code = frag_code.ptr, - .code_size = frag_code.len, - .entrypoint = "main", - .format = GPU_SHADERFORMAT_SPIRV, - .stage = GPU_SHADERSTAGE_FRAGMENT, - .num_samplers = textures, - .num_uniform_buffers = 1, - .num_storage_buffers = 0, - .num_storage_textures = 0 - }; - - s.frag = sdl::create_gpu_shader(self.gpu, &shader_info); - if (s.frag == null) { - unreachable("failed to create gpu fragment shader: %s", sdl::get_error()); - } - } - - // push the shader into the list - self.shaders.push(s); -} - -fn void Renderer.load_spirv_shader_from_file(&self, String name, String vert_path, String frag_path, uint textures, uint uniforms) -{ - if (vert_path == "" || frag_path == "") { - unreachable("need both a vertex shader and fragment shader path"); - } - - char[] vert_code; - char[] frag_code; - - // create vertex shader - usz size = file::get_size(vert_path)!!; - vert_code = mem::new_array(char, size + size%4); - file::load_buffer(vert_path, vert_code)!!; - defer mem::free(vert_code); - - // create fragment shader - size = file::get_size(frag_path)!!; - frag_code = mem::new_array(char, size + size%4); - file::load_buffer(frag_path, frag_code)!!; - defer mem::free(frag_code); - - self.load_spirv_shader_from_mem(name, vert_code, frag_code, textures, uniforms); -} - -// this describes what we want to draw, since for drawing different things we have to change -// the GPUPrimitiveType and GPURasterizerState for the pipeline. -enum PipelineType : (GPUPrimitiveType primitive_type, GPURasterizerState raster_state) { - RECT = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}}, - SPRITE = {GPU_PRIMITIVETYPE_TRIANGLELIST, {.fill_mode = GPU_FILLMODE_FILL, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}}, - LINE = {GPU_PRIMITIVETYPE_LINELIST, {.fill_mode = GPU_FILLMODE_LINE, .cull_mode = GPU_CULLMODE_NONE, .front_face = GPU_FRONTFACE_COUNTER_CLOCKWISE}}, -} - -// create a graphics pipeline to draw to the window using a set of vertex/fragment shaders -// the pipeline is pushed into the renderer's pipeline list and it will have the same id as -// the shader set. -fn void Renderer.create_pipeline(&self, String shader_name, PipelineType type) -{ - Shader *s = self.shaders.get_from_name(shader_name); - if (s == null) { - unreachable("error in creating pipeline: no shader named %s", shader_name); - } - - GPUGraphicsPipelineCreateInfo ci = { - .vertex_shader = s.vert, - .fragment_shader = s.frag, - // This structure specifies how the vertex buffer looks in memory, what it contains - // and what is passed where to the gpu. Each vertex has three attributes, position, - // color and uv coordinates. Since this is a 2D pixel-based renderer the position - // is represented by two floats, the color as 32 bit rgba and the uv also as intgers. - .vertex_input_state = { - // the description of each vertex buffer, for now I use only one buffer - .vertex_buffer_descriptions = (GPUVertexBufferDescription[]){ - { // first slot, per-vertex attributes - .slot = 0, - .pitch = Vertex.sizeof, - .input_rate = GPU_VERTEXINPUTRATE_VERTEX, - }, - { // second slot, per-instance attributes - .slot = 1, - .pitch = QuadAttributes.sizeof, - .input_rate = GPU_VERTEXINPUTRATE_INSTANCE, - } - }, - .num_vertex_buffers = 2, - // the description of each vertex - .vertex_attributes = (GPUVertexAttribute[]){ - { // at location zero there is the position of the vertex - .location = 0, - .buffer_slot = 0, // buffer slot zero so per-vertex - .format = GPU_VERTEXELEMENTFORMAT_SHORT2, // x,y - .offset = 0, - }, - { // at location one there is the per-quad position - .location = 1, - .buffer_slot = 1, // buffer slot one so per-instance - .format = GPU_VERTEXELEMENTFORMAT_SHORT4, // x,y,w,h - .offset = QuadAttributes.pos.offsetof, - }, - { // at location two there are the per-quad uv coordinates - .location = 2, - .buffer_slot = 1, - .format = GPU_VERTEXELEMENTFORMAT_SHORT2, - .offset = QuadAttributes.uv.offsetof, - }, - { // at location three there is the quad color - .location = 3, - .buffer_slot = 1, - .format = GPU_VERTEXELEMENTFORMAT_UBYTE4, - .offset = QuadAttributes.color.offsetof, - } - }, - .num_vertex_attributes = 4, - }, - // the pipeline's primitive type and rasterizer state differs based on what needs to - // be drawn - .primitive_type = type.primitive_type, - .rasterizer_state = type.raster_state, - .multisample_state = {}, // no multisampling, all zeroes - .depth_stencil_state = {}, // no stencil test, all zeroes - .target_info = { // the target (texture) description - .color_target_descriptions = (GPUColorTargetDescription[]){{ - // rendering happens to the window, so get it's format - .format = sdl::get_gpu_swapchain_texture_format(self.gpu, self.win), - .blend_state = { - // alpha blending on everything - // https://en.wikipedia.org/wiki/Alpha_compositing - .src_color_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA, - .dst_color_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, - .color_blend_op = GPU_BLENDOP_ADD, - .src_alpha_blendfactor = GPU_BLENDFACTOR_SRC_ALPHA, - .dst_alpha_blendfactor = GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, - .alpha_blend_op = GPU_BLENDOP_ADD, - .enable_blend = true, - // color write mask is not enabled so all rgba channels are written to - }, - }}, - .num_color_targets = 1, - .depth_stencil_format = {}, // FIXME: no stencil, no depth buffering - .has_depth_stencil_target = false, - }, - }; - - // create the pipeline and add it to the pipeline list - Pipeline p = { - .id = s.id, - .pipeline = sdl::create_gpu_graphics_pipeline(self.gpu, &ci), - }; - - if (p.pipeline == null) { - unreachable("failed to create pipeline (shaders: %s, type: %s): %s", shader_name, type.nameof, sdl::get_error()); - } - - self.pipelines.push(p); -} - -// NOTE: with TEXTUREUSAGE_SAMPLER the texture format cannot be intger _UINT so it has to be nermalized -enum TextureType : (GPUTextureFormat format) { - FULL_COLOR = GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, - JUST_ALPHA = GPU_TEXTUREFORMAT_R8_UNORM -} - -macro void Renderer.new_texture(&self, name_or_id, TextureType type, char[] pixels, uint width, uint height) -{ - $switch $typeof(name_or_id): - $case uint: return self.new_texture_by_id(id, type, pixels, width, height); - $case String: return self.new_texture_by_id(name_or_id.hash(), type, pixels, width, height); - $default: unreachable("texture must have a name (String) or an id (uint)"); - $endswitch -} - -macro void Renderer.update_texture(&self, name_or_id, char[] pixels, uint width, uint height, uint x = 0, uint y = 0) -{ - $switch $typeof(name_or_id): - $case uint: return self.update_texture_by_id(name_or_id, pixels, width, height, x, y); - $case String: return self.update_texture_by_id(name_or_id.hash(), pixels, width, height, x, y); - $default: unreachable("texture must have a name (String) or an id (uint)"); - $endswitch -} - - -// create a new gpu texture from a pixel buffer, the format has to be specified -// the new texture s given an id and pushed into a texture list -fn void Renderer.new_texture_by_id(&self, Id id, TextureType type, char[] pixels, uint width, uint height) -{ - // the texture description - GPUTextureCreateInfo tci = { - .type = GPU_TEXTURETYPE_2D, - .format = type.format, - // all textures are used with samplers, which means read-only textures that contain data to be sampled - .usage = GPU_TEXTUREUSAGE_SAMPLER, - .width = width, - .height = height, - .layer_count_or_depth = 1, - .num_levels = 1, // no mip maps so just one level - // .sample_count not used since the texture is not a render target - }; - - GPUTexture* texture = sdl::create_gpu_texture(self.gpu, &tci); - if (texture == null) { - unreachable("failed to create texture (id: %s, type: %s): %s", id, type.nameof, sdl::get_error()); - } - - // the sampler description, how the texture should be sampled - GPUSamplerCreateInfo sci = { - .min_filter = GPU_FILTER_LINEAR, // linear interpolation for textures - .mag_filter = GPU_FILTER_LINEAR, - .mipmap_mode = GPU_SAMPLERMIPMAPMODE_NEAREST, - .address_mode_u = GPU_SAMPLERADDRESSMODE_REPEAT, // tiling textures - .address_mode_v = GPU_SAMPLERADDRESSMODE_REPEAT, - .address_mode_w = GPU_SAMPLERADDRESSMODE_REPEAT, - // everything else is not used and not needed - }; - - GPUSampler* sampler = sdl::create_gpu_sampler(self.gpu, &sci); - if (sampler == null) { - unreachable("failed to create sampler (texture id: %s, type: %s): %s", id, type.nameof, sdl::get_error()); - } - - Texture t = { - .id = id, - .texture = texture, - .sampler = sampler, - }; - self.textures.push(t); - - // upload the texture data - self.update_texture_by_id(id, pixels, width, height, 0, 0); -} - -fn void Renderer.update_texture_by_id(&self, Id id, char[] pixels, uint width, uint height, uint x, uint y) -{ - Texture* t = self.textures.get_from_id(id); - if (t == null || t.texture == null) { - unreachable("failed updating texture: no texture with id %s", id); - } - GPUTexture* texture = t.texture; - - // FIXME: do a better job at validating the copy - if (x > t.width || y > t.height) { - unreachable("failed updating texture: attempting to copy outside of the texture region"); - } - - // upload image data - GPUCommandBuffer* cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu); - if (cmdbuf == null) { - unreachable("failed to upload texture data at acquiring command buffer: %s", sdl::get_error()); - } - GPUCopyPass* copypass = sdl::begin_gpu_copy_pass(cmdbuf); - if (copypass == null) { - unreachable("failed to upload texture data at beginning copy pass: %s", sdl::get_error()); - } - - GPUTransferBuffer* buf = sdl::create_gpu_transfer_buffer(self.gpu, - &&(GPUTransferBufferCreateInfo){.usage = GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = pixels.len} - ); - if (buf == null) { - unreachable("failed to upload texture data at creating the transfer buffer: %s", sdl::get_error()); - } - - char* gpu_mem = (char*)sdl::map_gpu_transfer_buffer(self.gpu, buf, CYCLE); - if (gpu_mem == null) { - unreachable("failed to upload texture data at mapping the transfer buffer: %s", sdl::get_error()); - } - // copy the data to the driver's memory - gpu_mem[:pixels.len] = pixels[..]; - sdl::unmap_gpu_transfer_buffer(self.gpu, buf); - - // upload the data to gpu memory - sdl::upload_to_gpu_texture(copypass, - &&(GPUTextureTransferInfo){.transfer_buffer = buf, .offset = 0}, - &&(GPUTextureRegion){.texture = texture, .x = x, .y = y, .w = width, .h = height, .d = 1}, - false - ); - - sdl::end_gpu_copy_pass(copypass); - if (!sdl::submit_gpu_command_buffer(cmdbuf)) { - unreachable("failed to upload texture data at command buffer submission: %s", sdl::get_error()); - } - sdl::release_gpu_transfer_buffer(self.gpu, buf); -} - - -fn bool Renderer.push_sprite(&self, short x, short y, short w, short h, short u, short v, uint color = 0xffffffff) -{ - QuadAttributes qa = { - .pos = {.x = x, .y = y, .w = w, .h = h}, - .uv = {.u = u, .v = v}, - .color = color - }; - - return self.map_quad(qa); -} - -fn bool Renderer.push_quad(&self, short x, short y, short w, short h, uint color, ushort radius = 0, ushort thickness = 0) -{ - QuadAttributes qa = { - .pos = {.x = x, .y = y, .w = w, .h = h}, - .uv = {.u = radius, .v = radius+thickness}, - .color = color - }; - - return self.map_quad(qa); -} - -// this does not upload a quad, but it simply copies the quad data to the correct transfer buffers. -// Data transfer to the GPU only happens in draw_quads() to save time -fn bool Renderer.map_quad(&self, QuadAttributes qa) -{ - if (self.quad_buffer.count >= MAX_QUAD_BATCH) { - return false; - } - QuadBuffer* qb = &self.quad_buffer; - - // upload the quad data to the gpu - if (qb.initialized == false) { - unreachable("quad buffer not initialized"); - } - - qb.attr_ts_mapped[qb.count] = qa; - - qb.count++; - - return true; -} - -fn void Renderer.upload_quads(&self) -{ - QuadBuffer* qb = &self.quad_buffer; - - GPUCommandBuffer* cmd = sdl::acquire_gpu_command_buffer(self.gpu); - if (cmd == null) { - unreachable("failed to upload quad at acquiring command buffer: %s", sdl::get_error()); - } - GPUCopyPass* cpy = sdl::begin_gpu_copy_pass(cmd); - - // upload quad attributes - sdl::upload_to_gpu_buffer(cpy, - &&(GPUTransferBufferLocation){.transfer_buffer = qb.attr_ts, .offset = 0}, - &&(GPUBufferRegion){.buffer = qb.attr_buf, .offset = 0, .size = QuadAttributes.sizeof * qb.count}, - false - ); - - sdl::end_gpu_copy_pass(cpy); - if (!sdl::submit_gpu_command_buffer(cmd)) { - unreachable("failed to upload quads at submit command buffer: %s", sdl::get_error()); - } -} - -// draw all quads in the quad buffer, since uniforms are per-drawcall it makes no sense -// to draw them one a the time -fn void Renderer.draw_quads(&self, uint off, uint count) -{ - QuadBuffer* qb = &self.quad_buffer; - - // too many quads to draw - if (off >= qb.count || count > qb.count - off) { - unreachable("too many quads, have %d, requested %d, offset %d", qb.count, count, off); - } - - sdl::bind_gpu_vertex_buffers(self.render_pass, 0, - (GPUBufferBinding[]){ - {.buffer = qb.vert_buf, .offset = 0}, - {.buffer = qb.attr_buf, .offset = 0}, - }, 2); - sdl::bind_gpu_index_buffer(self.render_pass, &&(GPUBufferBinding){.buffer = qb.idx_buf, .offset = 0}, GPU_INDEXELEMENTSIZE_16BIT); - - sdl::draw_gpu_indexed_primitives(self.render_pass, 6, count, 0, 0, off); -} - -fn void Renderer.reset_quads(&self) -{ - self.quad_buffer.count = 0; -} - - -fn void Renderer.begin_render(&self, bool clear_screen) -{ - self.render_cmdbuf = sdl::acquire_gpu_command_buffer(self.gpu); - sdl::wait_and_acquire_gpu_swapchain_texture(self.render_cmdbuf, self.win, &self.swapchain_texture, null, null); - - // push the window size as a uniform - // TODO: maybe make this configurable and/or add more things - ViewsizeUniform v; - self.get_window_size(&v.w, &v.h); - sdl::push_gpu_vertex_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof); - sdl::push_gpu_fragment_uniform_data(self.render_cmdbuf, 0, &v, ViewsizeUniform.sizeof); - - if (clear_screen) { - GPURenderPass* pass = sdl::begin_gpu_render_pass(self.render_cmdbuf, - &&(GPUColorTargetInfo){ - .texture = self.swapchain_texture, - .mip_level = 0, - .layer_or_depth_plane = 0, - .clear_color = {.r = 1.0, .g = 0.0, .b = 1.0, .a = 1.0}, - .load_op = GPU_LOADOP_CLEAR, // clear the screen at the start of the render pass - .store_op = GPU_STOREOP_STORE, - .resolve_texture = null, - .resolve_mip_level = 0, - .resolve_layer = 0, - .cycle = false, - .cycle_resolve_texture = false - }, - 1, - null // huh - ); - if (pass == null) { - unreachable("render pass creation went wrong: %s", sdl::get_error()); - } - - sdl::end_gpu_render_pass(pass); - } -} - -fn void Renderer.end_render(&self) -{ - sdl::submit_gpu_command_buffer(self.render_cmdbuf); - self.reset_quads(); -} - -fn void Renderer.start_render_pass(&self, String pipeline_name) -{ - self.render_pass = sdl::begin_gpu_render_pass(self.render_cmdbuf, - &&(GPUColorTargetInfo){ - .texture = self.swapchain_texture, - .mip_level = 0, - .layer_or_depth_plane = 0, - .clear_color = {.r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0}, - .load_op = GPU_LOADOP_DONT_CARE, - .store_op = GPU_STOREOP_STORE, - .resolve_texture = null, - .resolve_mip_level = 0, - .resolve_layer = 0, - .cycle = false, - .cycle_resolve_texture = false - }, - 1, - null // huh - ); - - if (self.render_pass == null) { - unreachable("render pass creation went wrong: %s", sdl::get_error()); - } - - sdl::GPUGraphicsPipeline* p; - p = self.pipelines.get_from_name(pipeline_name).pipeline; - if (p == null) { - unreachable("no pipeline"); - } - - sdl::bind_gpu_graphics_pipeline(self.render_pass, p); -} - -fn void Renderer.end_render_pass(&self) -{ - sdl::end_gpu_render_pass(self.render_pass); -} - -fn void Renderer.bind_texture(&self, String texture_name) -{ - ren::Texture* tx = self.textures.get_from_name(texture_name); - sdl::bind_gpu_fragment_samplers(self.render_pass, 0, - (GPUTextureSamplerBinding[]){{.texture = tx.texture, .sampler = tx.sampler}}, 1 - ); -} - -fn void Renderer.set_scissor(&self, int x, int y, int w, int h) -{ - // in vulkan scissor size must be positive, clamp to zero - w = max(w, 0); - h = max(h, 0); - sdl::set_gpu_scissor(self.render_pass, &&(sdl::Rect){x,y,w,h}); -} - -fn void Renderer.reset_scissor(&self) -{ - int w, h; - sdl::get_window_size(self.win, &w, &h); - self.set_scissor(0, 0, w, h); -} - -/// === NOTES === -/* 1. The uniform data is per-render pass. So you can do: - * - push uniform - * - draw 1 - * - draw 2 - * But not: - * - push uniform - * - draw - * - push new uniform - * - draw - * And not even: - * - draw - * - push uniform - * - draw - * - * 2. The GPU buffers are read per-command-buffer and not per - * render pass. So I cannot override an element in the buffer - * before submitting the command buffer. - */ - /// === END NOTES === - - -fn void Renderer.render_ugui(&self, CmdQueue* queue) -{ - // upload pass - foreach (&c : queue) { - if (c.type == CMD_RECT) { - CmdRect r = c.rect; - self.push_quad(r.rect.x, r.rect.y, r.rect.w, r.rect.h, r.color.to_uint(), r.radius, r.thickness); - } else if (c.type == CMD_SPRITE) { - CmdSprite s = c.sprite; - self.push_sprite(s.rect.x, s.rect.y, s.texture_rect.w, s.texture_rect.h, s.texture_rect.x, s.texture_rect.y, s.hue.to_uint()); - } - } - self.upload_quads(); - - Cmd* last_command; - uint off; - uint count; - for (Cmd* cmd; (cmd = queue.dequeue() ?? null) != null;) { - if (last_command == null || last_command.type != cmd.type) { - self.end_command(last_command, off, count); - self.begin_command(cmd); - off += count; - count = 0; - } - - switch (cmd.type) { - case CMD_RECT: nextcase; - case CMD_SPRITE: - count++; - case CMD_UPDATE_ATLAS: - // TODO: verify the correct type - CmdUpdateAtlas u = cmd.update_atlas; - char[] pixels = u.raw_buffer[..u.width*u.height*u.bpp]; - self.update_texture(u.id, pixels, u.width, u.height); - case CMD_SCISSOR: - ugui::Rect s = cmd.scissor.rect; - if (s.x == 0 && s.y == 0 && s.w == 0 && s.h == 0) { - self.get_window_size((int*)&s.w, (int*)&s.h); - } - self.scissor_x = s.x; - self.scissor_y = s.y; - self.scissor_w = s.w; - self.scissor_h = s.h; - default: unreachable("unknown command: %s", cmd.type); - } - - last_command = cmd; - } - self.end_command(last_command, off, count); -} - -fn void Renderer.begin_command(&self, Cmd* cmd) -{ - if (cmd == null) return; - - switch (cmd.type) { - case CMD_RECT: - self.start_render_pass("UGUI_PIPELINE_RECT"); - self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h); - case CMD_SPRITE: - // TODO: support multiple sprite and font atlases - CmdSprite s = cmd.sprite; - String pipeline; - String texture; - if (s.texture_id == self.sprite_atlas_id) { - switch (s.type) { - case SPRITE_NORMAL: pipeline = "UGUI_PIPELINE_SPRITE"; - case SPRITE_SDF: pipeline = "UGUI_PIPELINE_SPRITE_SDF"; - case SPRITE_MSDF: pipeline = "UGUI_PIPELINE_SPRITE_MSDF"; - case SPRITE_ANIMATED: unreachable("animated sprtes are unsupported for now"); - default: unreachable("unknown sprite type %s", s.type); - } - texture = "icons"; - } else if (s.texture_id == self.font_atlas_id) { - pipeline = "UGUI_PIPELINE_FONT"; - texture = "font1"; - } - self.start_render_pass(pipeline); - self.bind_texture(texture); - self.set_scissor(self.scissor_x, self.scissor_y, self.scissor_w, self.scissor_h); - case CMD_UPDATE_ATLAS: break; - case CMD_SCISSOR: break; - default: unreachable("unknown command: %s", cmd.type); - } -} - -fn void Renderer.end_command(&self, Cmd* cmd, uint off, uint count) -{ - if (cmd == null) return; - - switch (cmd.type) { - case CMD_RECT: nextcase; - case CMD_SPRITE: - self.draw_quads(off, count); - self.end_render_pass(); - case CMD_UPDATE_ATLAS: break; - case CMD_SCISSOR: break; - default: unreachable("unknown command: %s", cmd.type); - } -} diff --git a/lib/ugui.c3l/src/ugui_shapes.c3 b/src/shapes.c3 similarity index 56% rename from lib/ugui.c3l/src/ugui_shapes.c3 rename to src/shapes.c3 index 9e778b7..96d9710 100644 --- a/lib/ugui.c3l/src/ugui_shapes.c3 +++ b/src/shapes.c3 @@ -1,5 +1,5 @@ module ugui; - +import std::math; // ---------------------------------------------------------------------------------- // // RECTANGLE // @@ -10,6 +10,9 @@ struct Rect { short x, y, w, h; } +// TODO: find another name +const Rect RECT_MAX = {0, 0, short::max, short::max}; + // return true if rect a contains b macro bool Rect.contains(Rect a, Rect b) { @@ -33,11 +36,26 @@ macro bool Rect.collides(Rect a, Rect b) return !(a.x > b.x+b.w || a.x+a.w < b.x || a.y > b.y+b.h || a.y+a.h < b.y); } +// return a rect that contains both rects, a bounding box of both +macro Rect containing_rect(Rect a, Rect b) +{ + short min_x = (short)min(a.x, b.x); + short min_y = (short)min(a.y, b.y); + short max_x = (short)max(a.x + a.w, b.x + b.w); + short max_y = (short)max(a.y + a.h, b.y + b.h); + return { + .x = min_x, + .y = min_y, + .w = (short)(max_x - min_x), + .h = (short)(max_y - min_y) + }; +} + // check for empty rect macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0; // returns the element-wise addition of r1 and r2 -macro Rect Rect.add(Rect r1, Rect r2) +macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+) { return { .x = r1.x + r2.x, @@ -48,7 +66,7 @@ macro Rect Rect.add(Rect r1, Rect r2) } // returns the element-wise subtraction of r1 and r2 -macro Rect Rect.sub(Rect r1, Rect r2) +macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-) { return { .x = r1.x - r2.x, @@ -59,7 +77,7 @@ macro Rect Rect.sub(Rect r1, Rect r2) } // returns the element-wise multiplication of r1 and r2 -macro Rect Rect.mul(Rect r1, Rect r2) +macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*) { return { .x = r1.x * r2.x, @@ -106,7 +124,7 @@ macro Rect Rect.min(Rect a, Rect b) } // Offset a rect by a point -macro Rect Rect.off(Rect r, Point p) +macro Rect Rect.off(Rect r, Point p) @operator_s(+) { return { .x = r.x + p.x, @@ -136,6 +154,35 @@ macro Point Rect.bottom_right(Rect r) }; } +macro Rect Rect.center_to(Rect a, Rect b) +{ + return { + .x = b.x + (b.w - a.w)/2, + .y = b.y + (b.h - a.h)/2, + .w = a.w, + .h = a.h, + }; +} + +macro Rect Rect.pad(Rect a, Rect b) +{ + return { + .x = a.x + b.x, + .y = a.y + b.y, + .w = a.w - b.x - b.w, + .h = a.h - b.y - b.h, + }; +} + +macro Rect Rect.expand(Rect a, Rect b) +{ + return { + .x = a.x - b.x, + .y = a.y - b.y, + .w = a.w + b.x + b.w, + .h = a.h + b.y + b.h, + }; +} // ---------------------------------------------------------------------------------- // // POINT // @@ -151,39 +198,16 @@ macro bool Point.in_rect(Point p, Rect r) return (p.x >= r.x && p.x <= r.x + r.w) && (p.y >= r.y && p.y <= r.y + r.h); } -macro Point Point.add(Point a, Point b) -{ - return { - .x = a.x + b.x, - .y = a.y + b.y, - }; -} +macro bool Point.outside(Point p, Rect r) => !p.in_rect(r); -macro Point Point.sub(Point a, Point b) -{ - return { - .x = a.x - b.x, - .y = a.y - b.y, - }; -} +macro Point Point.add(Point a, Point b) @operator_s(+) => {.x = a.x+b.x, .y = a.y+b.y}; +macro Point Point.sub(Point a, Point b) @operator_s(-) => {.x = a.x-b.x, .y = a.y-b.y}; +macro Point Point.neg(Point p) @operator_s(-) => {-p.x, -p.y}; -macro Point Point.neg(Point p) => {-p.x, -p.y}; +macro Point Point.max(Point a, Point b) => {.x = max(a.x, b.x), .y = max(a.y, b.y)}; +macro Point Point.min(Point a, Point b) => {.x = min(a.x, b.x), .y = min(a.y, b.y)}; -macro Point Point.max(Point a, Point b) -{ - return { - .x = max(a.x, b.x), - .y = max(a.y, b.y), - }; -} - -macro Point Point.min(Point a, Point b) -{ - return { - .x = min(a.x, b.x), - .y = min(a.y, b.y), - }; -} +macro bool Point.equals(Point a, Point b) @operator_s(==) => a.x == b.x && a.y == b.y; // ---------------------------------------------------------------------------------- // // COLOR // @@ -214,8 +238,29 @@ macro Color uint.@to_rgba($u) }; } -macro uint Color.to_uint(c) -{ - uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24); - return u; +macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24); + +// ---------------------------------------------------------------------------------- // +// SIZE // +// ---------------------------------------------------------------------------------- // + +struct Size { + short min, max; } + +macro Size @grow() => {.min = 0, .max = 0}; +macro Size @exact(short s) => {.min = s, .max = s}; +macro Size @fit(short min = 0, short max = short::max) => {.min = min, .max = max}; + +macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0); +macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0); +macro bool Size.@is_fit(s) => (s.min != s.max); + +macro Size Size.add(a, Size b) @operator_s(+) => {.min = a.min.sat_add(b.min), .max = a.max.sat_add(b.max)}; +macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min.sat_sub(b.min), .max = a.max.sat_sub(b.max)}; + +macro Size Size.combine(a, Size b) => {.min = max(a.min, b.min), .max = min(a.max, b.max)}; +macro Size Size.comb_max(a, Size b) => {.min = max(a.min, b.min), .max = max(a.max, b.max)}; +macro Size Size.comb_min(a, Size b) => {.min = min(a.min, b.min), .max = min(a.max, b.max)}; + +macro short Size.greater(a) => a.min > a.max ? a.min : a.max; diff --git a/lib/ugui.c3l/src/ugui_sprite.c3 b/src/sprite.c3 similarity index 58% rename from lib/ugui.c3l/src/ugui_sprite.c3 rename to src/sprite.c3 index 724262d..2b01e65 100644 --- a/lib/ugui.c3l/src/ugui_sprite.c3 +++ b/src/sprite.c3 @@ -30,21 +30,17 @@ struct SpriteAtlas { bool should_update; } -struct ElemSprite { - Id id; -} - // name: some examples are "icons" or "images" fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, ushort height) { // FIXME: for now only R8G8B8A8 format is supported if (type != ATLAS_R8G8B8A8) { - return INVALID_TYPE?; + return INVALID_TYPE~; } this.id = name.hash(); this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!; - this.sprites.init(allocator::heap(), capacity: SRITES_PER_ATLAS); + this.sprites.init(mem, capacity: SRITES_PER_ATLAS); this.should_update = false; } @@ -64,8 +60,8 @@ fn Sprite*? SpriteAtlas.insert(&this, String name, SpriteType type, char[] pixel Point uv = this.atlas.place(pixels, w, h, stride)!; s.w = w; s.h = h; - s.u = uv.x; - s.v = uv.y; + s.u = (ushort)uv.x; + s.v = (ushort)uv.y; this.sprites.set(s.id, s); this.should_update = true; return this.sprites.get_ref(s.id); @@ -82,8 +78,8 @@ fn Sprite*? SpriteAtlas.get_by_id(&this, Id id) return this.sprites.get_ref(id); } -macro Rect Sprite.rect(s) => {0,0,s.w,s.h}; -macro Rect Sprite.uv(s) => {s.u,s.v,s.w,s.h}; +macro Rect Sprite.rect(s) => {0, 0, (short)s.w, (short)s.h}; +macro Rect Sprite.uv(s) => {(short)s.u, (short)s.v, (short)s.w, (short)s.h}; fn void? Ctx.sprite_atlas_create(&ctx, String name, AtlasType type, ushort w, ushort h) { @@ -104,50 +100,8 @@ fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType t { QOIDesc desc; - char[] pixels = qoi::read(allocator::heap(), path, &desc, QOIChannels.RGBA)!; + char[] pixels = qoi::read(mem, path, &desc, QOIChannels.RGBA)!; defer mem::free(pixels); ctx.sprite_atlas.insert(name, type, pixels, (ushort)desc.width, (ushort)desc.height, (ushort)desc.width)!; } - - -macro Ctx.sprite(&ctx, String name, Point off = {0,0}, ...) - => ctx.sprite_id(@compute_id($vasplat), name, off); -fn void? Ctx.sprite_id(&ctx, Id id, String name, Point off) -{ - id = ctx.gen_id(id)!; - - Elem *parent = ctx.get_parent()!; - Elem *elem = ctx.get_elem(id, ETYPE_SPRITE)!; - - Style* style = ctx.styles.get_style(@str_hash("sprite")); - Sprite* sprite = ctx.sprite_atlas.get(name)!; - - Rect uv = { sprite.u, sprite.v, sprite.w, sprite.h }; - Rect bounds = { 0, 0, sprite.w, sprite.h }; - - elem.bounds = ctx.position_element(parent, bounds.off(off), style); - elem.sprite.id = ctx.get_sprite_atlas_id(name); - - // if the bounds are null the element is outside the div view, - // no interaction should occur so just return - if (elem.bounds.is_null()) return; - - Id tex_id = ctx.sprite_atlas.id; - - return ctx.push_sprite(elem.bounds, uv, tex_id, parent.div.z_index)!; -} - -fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds, bool center = false) -{ - Elem *parent = ctx.get_parent()!; - Sprite* sprite = ctx.sprite_atlas.get(name)!; - Id tex_id = ctx.sprite_atlas.id; - - if (center) { - Point off = {.x = (bounds.w - sprite.w) / 2, .y = (bounds.h - sprite.h) / 2}; - bounds = bounds.off(off); - } - - return ctx.push_sprite(bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!; -} diff --git a/src/string.c3 b/src/string.c3 new file mode 100644 index 0000000..01b2840 --- /dev/null +++ b/src/string.c3 @@ -0,0 +1,582 @@ +module ugui; + +import std::collections::list; +import std::core::string; + +struct LineInfo @local { + sz start, end; + short width, height; + short first_off; // first character offset +} + +macro sz LineInfo.len(li) => li.end-li.start; + +alias LineStack @local = list::List{LineInfo}; + +fn short Rect.y_off(Rect bounds, short height, Anchor anchor) +{ + short off; + + switch (anchor) { + case TOP_LEFT: nextcase; + case TOP: nextcase; + case TOP_RIGHT: + off = 0; + case LEFT: nextcase; + case CENTER: nextcase; + case RIGHT: + off = (short)(bounds.h - height)/2; + case BOTTOM_LEFT: nextcase; + case BOTTOM: nextcase; + case BOTTOM_RIGHT: + off = (short)(bounds.h - height); + } + return off; +} + +fn short Rect.x_off(Rect bounds, short width, Anchor anchor) +{ + short off; + + switch (anchor) { + case TOP_LEFT: nextcase; + case LEFT: nextcase; + case BOTTOM_LEFT: + off = 0; + case TOP: nextcase; + case CENTER: nextcase; + case BOTTOM: + off = (short)(bounds.w - width)/2; + case TOP_RIGHT: nextcase; + case RIGHT: nextcase; + case BOTTOM_RIGHT: + off = (short)(bounds.w - width); + } + + return off; +} + + +// ---------------------------------------------------------------------------------- // +// STRING LAYOUT // +// ---------------------------------------------------------------------------------- // + + +struct GlyphIterator { + short baseline; + short line_height; + short line_gap; + short space_width; + short tab_width; + Rect bounds; + Anchor anchor; + bool reflow; + Font* font; + + LineStack lines; + sz line_off, line_idx; + String text; + + Codepoint cp; + Glyph* gp; + short adv; // prev advance + Point o; + Rect str_bounds; +} + +<* +@param [&inout] self +@param [in] text +@param [&inout] font +*> +fn void? GlyphIterator.init(&self, Allocator allocator, String text, Rect bounds, Font* font, Anchor anchor, bool reflow, uint tab_size) +{ + self.font = font; + + self.line_height = (short)font.line_height(); + self.baseline = (short)font.ascender; + self.line_gap = (short)font.linegap; + self.space_width = font.get_glyph(' ').adv!; + self.tab_width = self.space_width * (short)tab_size; + + self.bounds = bounds; + self.o = bounds.position(); + self.reflow = reflow; + self.text = text; + self.anchor = anchor; + + // if the anchor is top_left we can skip dividing the string line by line, in GlyphIterator.next + // this has to be accounted for + if (anchor != TOP_LEFT) { + self.lines.init(allocator, 4); + self.populate_lines_stack()!; + self.line_off = 0; + self.line_idx = 0; + + if (self.lines.len() > 0) { + self.o.y += bounds.y_off(self.str_bounds.h, anchor); + self.o.x += bounds.x_off(self.lines[0].width, anchor) - self.lines[0].first_off; + } + } +} + + +fn void? GlyphIterator.populate_lines_stack(&self) +{ + sz line_start; + LineInfo li; + Point o = self.o; + StringIterator ti = self.text.iterator(); + + sz prev_off; + for (Codepoint cp; ti.has_next();) { + cp = ti.next()!; + sz off = ti.current; + bool push = false; + + li.height = self.line_height; + switch { + case cp == '\n': + push = true; + case cp == '\t': + o.x += self.tab_width; + case ascii::is_cntrl((char)cp): + break; + default: + Glyph* gp = self.font.get_glyph(cp)!; + + if (off == line_start) { + li.first_off = gp.ox; + o.x -= gp.ox; + } + + Rect b = gp.bounds().off(o); + b.y += self.baseline; + + if (self.reflow && b.x + b.w > self.bounds.x + self.bounds.w) { + push = true; + // roll back this character since it is on the next line + ti.current = prev_off; + off = prev_off; + } else { + o.x += gp.adv; + li.width += gp.adv; + } + } + + if (push) { + li.start = line_start; + li.end = off; + self.lines.push(li); + self.str_bounds.w = max(self.str_bounds.w, li.width); + self.str_bounds.h += li.height; + + o.x = self.bounds.x; + o.y += self.line_height; + line_start = off; + + li.height = 0; + li.width = 0; + } + + prev_off = off; + } + if (line_start != ti.current) { + // FIXME: crap, can we not repeat this code? + li.start = line_start; + li.end = ti.current; + self.lines.push(li); + self.str_bounds.w = max(self.str_bounds.w, li.width); + self.str_bounds.h += li.height; + } + + self.str_bounds.h += (short)(self.lines.len()-1) * self.line_gap; +} + +fn String GlyphIterator.current_line(&self) +{ + LineInfo li = self.lines[self.line_idx]; + return self.text[li.start:li.len()]; +} + + +fn Rect? GlyphIterator.next(&self) +{ + // check if there is a next glyph and maybe update the line and offset indices + if (self.anchor != TOP_LEFT) { + if (self.line_idx >= self.lines.len()) { + return NO_MORE_ELEMENT~; + } + + LineInfo li = self.lines[self.line_idx]; + if (self.line_off >= li.len()) { + self.line_idx++; + + if (self.line_idx >= self.lines.len()) { + return NO_MORE_ELEMENT~; + } + + self.line_off = 0; + li = self.lines[self.line_idx]; + + self.o.y += self.line_height + self.line_gap; + self.o.x = self.bounds.x + self.bounds.x_off(li.width, self.anchor) - li.first_off; + } + } else if (self.line_off >= self.text.len) { + return NO_MORE_ELEMENT~; + } + + String t; + if (self.anchor != TOP_LEFT) { + t = self.current_line()[self.line_off..]; + } else { + t = self.text[self.line_off..]; + } + + sz read = t.len < 4 ? t.len : 4; + self.cp = conv::utf8_to_char32(&t[0], &read)!; + self.line_off += read; + self.gp = self.font.get_glyph(self.cp)!; + + Rect b = {.x = self.o.x, .y = self.o.y}; + + Point prev_o = self.o; + self.adv = 0; + switch { + case self.cp == '\n': + if (self.anchor == TOP_LEFT) { + self.o.x = self.bounds.x; + self.o.y += self.line_height + self.line_gap; + self.line_idx++; + } + break; + case self.cp == '\t': + self.o.x += self.tab_width; + case ascii::is_cntrl((char)self.cp): + break; + default: + b = self.gp.bounds().off(self.o); + b.y += self.baseline; + if (self.anchor == TOP_LEFT) { + //if (self.o.x == self.bounds.x) self.bounds.x -= self.gp.ox; + if (self.reflow && b.bottom_right().x > self.bounds.bottom_right().x) { + self.o.x = self.bounds.x - self.gp.ox; + self.o.y += self.line_height + self.line_gap; + self.line_idx++; + b = self.gp.bounds().off(self.o); + b.y += self.baseline; + } + } + self.o.x += self.gp.adv; + self.adv = self.o.x - prev_o.x; + } + + return b; +} + +fn bool GlyphIterator.has_next(&self) +{ + if (self.anchor == TOP_LEFT) { + return self.line_off < self.text.len; + } + + if (self.line_idx >= self.lines.len()) { + return false; + } + + LineInfo li = self.lines[self.line_idx]; + if (self.line_idx == self.lines.len() - 1 && self.line_off >= li.len()) { + return false; + } + + return true; +} + +fn sz GlyphIterator.current_offset(&self) +{ + if (self.anchor == TOP_LEFT) return self.line_off; + return self.lines[self.line_idx].start + self.line_off; +} + + +// layout a string inside a bounding box, following the given alignment (anchor). +<* +@param [&in] ctx +@param [in] text +*> +fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false) +{ + if (text == "") return; + if (bounds.w <= 0 || bounds.h <= 0) return; + + Font* font = &ctx.font; + Id texture_id = font.id; + + GlyphIterator gi; + gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!; + while (gi.has_next()) { + Rect b = gi.next()!; + Rect uv = gi.gp.uv(); + ctx.push_sprite(b, uv, texture_id, z_index, hue)!; + } +} + + +// ---------------------------------------------------------------------------------- // +// CURSOR AND MOUSE // +// ---------------------------------------------------------------------------------- // + + +fn Rect? Ctx.get_cursor_position(&ctx, String text, Rect bounds, Anchor anchor, sz cursor, bool reflow = false) +{ + if (bounds.w <= 0 || bounds.h <= 0) return {}; + + Font* font = &ctx.font; + Id texture_id = font.id; + + if (text == "") text = "\f"; + + GlyphIterator gi; + gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!; + Rect cursor_rect; + cursor_rect.x = gi.o.x; + cursor_rect.y = gi.o.y; + cursor_rect.h = (short)font.line_height(); + if (cursor == 0) return cursor_rect; + + while (gi.has_next()) { + Rect b = gi.next()!; + if (gi.current_offset() == cursor) { + if (gi.cp == '\n') { + if (!gi.has_next()) { + cursor_rect.x = bounds.x + bounds.x_off(0, anchor); + cursor_rect.y = b.y + gi.line_height + gi.line_gap; + } else { + gi.next()!; + cursor_rect.x = gi.o.x - gi.gp.adv; + cursor_rect.y = gi.o.y; + } + } else { + // Use the updated origin position instead of glyph bounds + cursor_rect.x = gi.o.x; + cursor_rect.y = gi.o.y; + } + return cursor_rect; + } + } + + return {}; +} + +<* +@param [&in] ctx +@param [in] text +*> +fn sz? Ctx.hit_test_string(&ctx, String text, Rect bounds, Anchor anchor, Point p, bool reflow = false) +{ + if (text == "") return 0; + if (bounds.w <= 0 || bounds.h <= 0) return 0; + + Font* font = &ctx.font; + + GlyphIterator gi; + gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!; + + sz prev_offset = 0; + Point prev_o = gi.o; + + while (gi.has_next()) { + Point o_before = gi.o; + sz offset_before = gi.current_offset(); + Rect b = gi.next()!; + + switch { + case gi.cp == '\n': + // Check if point is on this line before the newline + Rect line_rect = { + .x = prev_o.x, + .y = prev_o.y, + .w = (short)(o_before.x - prev_o.x), + .h = gi.line_height + }; + if (p.in_rect(line_rect)) return offset_before; + prev_o = gi.o; + break; + case gi.cp == '\t': + // Check if point is in the tab space + Rect tab_rect = { + .x = o_before.x, + .y = o_before.y, + .w = gi.tab_width, + .h = gi.line_height + }; + if (p.in_rect(tab_rect)) return offset_before; + break; + case ascii::is_cntrl((char)gi.cp): + break; + default: + // Create a hit test rect for this character + Rect hit_rect = { + .x = o_before.x, + .y = o_before.y, + .w = gi.gp.adv, + .h = gi.line_height + }; + + if (p.in_rect(hit_rect)) { + // Check if cursor should be before or after this character + // by checking which half of the character was clicked + short mid_x = o_before.x + gi.gp.adv / 2; + if (p.x < mid_x) { + return offset_before; + } else { + return gi.current_offset(); + } + } + } + + prev_offset = gi.current_offset(); + } + + // Point is after all text + return text.len; +} + +// TODO: implement a function `layout_string_with_selection` to avoid iterating over the string twice +fn void? Ctx.draw_string_selection(&ctx, String text, Rect bounds, Anchor anchor, sz start, sz end, int z_index, Color hue, bool reflow = false) +{ + if (text == "") return; + if (bounds.w <= 0 || bounds.h <= 0) return; + + if (start > end) @swap(start, end); + + // Ensure start < end + if (start > end) { + sz temp = start; + start = end; + end = temp; + } + + ctx.push_scissor(bounds, z_index)!; + + Font* font = &ctx.font; + + GlyphIterator gi; + gi.init(tmem, text, bounds, font, anchor, reflow, TAB_SIZE)!; + + Rect sel_rect = { .h = gi.line_height }; // selection rect + sz sel_line = -1; // selection line + while (gi.has_next()) { + Rect b = gi.next()!; + sz off = gi.current_offset()-1; + sz line = gi.line_idx; + bool in_selection = start <= off && off <= end; + + if (in_selection && line != sel_line) { + if (sel_line != -1) { + ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!; + } + sel_rect = {.x = gi.o.x - gi.adv, .y = gi.o.y, .w = 0, .h = gi.line_height}; + sel_line = line; + } + if (in_selection) { + sel_rect.w = gi.o.x - sel_rect.x; + if (gi.cp == '\n') sel_rect.w += gi.space_width; + } + + if (off > end) break; + } + ctx.push_rect(sel_rect, z_index, &&{.bg = hue})!; + + ctx.reset_scissor(z_index)!; +} + + +// ---------------------------------------------------------------------------------- // +// TEXT MEASUREMENT // +// ---------------------------------------------------------------------------------- // + + +const uint TAB_SIZE = 4; + +struct TextSize { + Size width, height; + int area; +} + +// Measeure the size of a string. +// width.min: as if each word is broken up by a new line +// width.max: the width of the string left as-is +// height.min: the height of the string left as-is +// height.max: the height of the string with each word broken up by a new line +<* +@param [&in] ctx +@param [in] text +*> +fn TextSize? Ctx.measure_string(&ctx, String text) +{ + if (text == "") return (TextSize){}; + + Font* font = &ctx.font; + short baseline = (short)font.ascender; + short line_height = (short)font.line_height(); + short line_gap = (short)font.linegap; + short space_width = font.get_glyph(' ').adv!; + short tab_width = (usz)space_width * TAB_SIZE; + + sz off; + usz x; + + TextSize ts; + + short word_width; + short words = 1; + Rect bounds; // unaltered text bounds; + Point origin; + + StringIterator it = text.iterator(); + for (Codepoint cp; it.has_next();) { + cp = it.next()!; + Glyph* gp = font.get_glyph(cp)!; + + // update the text bounds + switch { + case cp == '\n': + origin.x = 0; + origin.y += line_height + line_gap; + case cp == '\t': + origin.x += tab_width; + case ascii::is_cntrl((char)cp): + break; + default: + Rect b = gp.bounds().off(origin); + b.y += baseline; + bounds = containing_rect(bounds, b); + origin.x += gp.adv; + } + + // update the word width + switch { + case ascii::is_space((char)cp): + if (word_width > ts.width.min) ts.width.min = word_width; + word_width = 0; + words++; + default: + //word_width += gp.w + gp.ox; + if (off < text.len) { + word_width += gp.adv; + } else { + word_width += (short)gp.w + (short)gp.ox; + } + } + } + // end of string is also end of word + if (word_width > ts.width.min) ts.width.min = word_width; + + ts.width.max = bounds.w; + ts.height.min = bounds.h; + ts.height.max = words * line_height + line_gap * (words-1); + ts.area = bounds.w * bounds.h; + + return ts; +} diff --git a/lib/ugui.c3l/src/ugui_style.c3 b/src/style.c3 similarity index 71% rename from lib/ugui.c3l/src/ugui_style.c3 rename to src/style.c3 index aa5d20b..5957403 100644 --- a/lib/ugui.c3l/src/ugui_style.c3 +++ b/src/style.c3 @@ -7,10 +7,8 @@ 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 @@ -18,20 +16,22 @@ 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}, - .padding = {}, - .border = 2, - .radius = 12, + .border = {2, 2, 2, 2}, + .padding = {1, 1, 1, 1}, + .radius = 0, .size = 16, - - .bg = 0x282828ffu.@to_rgba(), - .fg = 0xfbf1c7ffu.@to_rgba(), - .primary = 0xcc241dffu.@to_rgba(), + + .bg = 0x282828ffu.@to_rgba(), + .fg = 0xfbf1c7ffu.@to_rgba(), + .primary = 0xcc241dffu.@to_rgba(), .secondary = 0x458588ffu.@to_rgba(), - .accent = 0xfabd2fffu.@to_rgba(), + .accent = 0xfabd2fffu.@to_rgba(), }; // style is stored in a hashmap, each style has an Id that can be generated by a string or whatever @@ -60,7 +60,7 @@ fn int StyleMap.import_style_string(&map, String text) Parser p; p.lex.text = text; int added; - + while (p.parse_style() == true) { added++; // set the default style correctly @@ -75,11 +75,11 @@ fn int Ctx.import_style_from_string(&ctx, String text) => ctx.styles.import_styl fn int Ctx.import_style_from_file(&ctx, String path) { char[] text; - usz size = file::get_size(path)!!; + sz size = file::get_size(path)!!; text = mem::new_array(char, size); file::load_buffer(path, text)!!; defer mem::free(text); - + int added = ctx.import_style_from_string((String)text); return added; } @@ -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 *