diff --git a/.ecode/project_build.json b/.ecode/project_build.json new file mode 100644 index 0000000..593ac60 --- /dev/null +++ b/.ecode/project_build.json @@ -0,0 +1,43 @@ +{ + "Debug": { + "build": [ + { + "args": "-C resources/shaders", + "command": "make", + "working_dir": "" + }, + { + "args": "build -g", + "command": "c3c", + "working_dir": "" + } + ], + "build_types": [], + "clean": [ + { + "args": "clean", + "command": "c3c", + "working_dir": "" + } + ], + "config": { + "clear_sys_env": false + }, + "os": [ + "linux" + ], + "output_parser": { + "config": { + "relative_file_paths": true + } + }, + "run": [ + { + "args": "", + "command": "build/ugui", + "name": "Custom Executable", + "working_dir": "" + } + ] + } +} \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index e69de29..74cac9a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,16 @@ +[submodule "lib/grapheme.c3l/thirdparty/libgrapheme"] + path = lib/grapheme.c3l/thirdparty/libgrapheme + url = git://git.suckless.org/libgrapheme + ignore = dirty +[submodule "lib/schrift.c3l/thirdparty/libschrift"] + path = lib/schrift.c3l/thirdparty/libschrift + url = https://github.com/tomolt/libschrift + ignore = dirty +[submodule "lib/sdl3.c3l"] + path = lib/sdl3.c3l + url = https://git.alemauri.eu/alema/sdl3.c3l + ignore = dirty +[submodule "lib/vendor"] + path = lib/vendor + url = https://github.com/c3lang/vendor + ignore = dirty diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..b533650 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,184 @@ +## High level overview + +Under the hood every element has an id, this id allows the library to store state +between frames. +Elements are also cached such that when the ui tree is rebuilt at the beginning of +every frame the element data structure doesn't have to be rebuilt. + +Elements are arranged in a tree, nodes are container elements that can contain other +elements, leafs are elements that cannot contain other elements. + +Every element has a size and a position, containers also have to keep track of their +layout information and some other state. + +Elements can push commands into the draw stack, which is a structure that contains +all the draw commands that the user application has to perform do display the ui +correctly, such commands include drawing lines, rectangles, sprites, text, etc. + + +```text + +-----------+ + | ug_init() | + +-----+-----+ + | + | + | + +---------v----------+ + |ug_input_keyboard() | + |ug_input_mouse() <----+ + |ug_input_clipboard()| | + | ... | | + +---------+----------+ | + | | + | | + +-------v--------+ | + |ug_frame_begin()| | + +-------+--------+ | + | | + | | + +---------v----------+ | + |ug_window_start() | | + +---->ug_container_start()| | + | |ug_div_start() | | + | | ... | | + | +---------+----------+ | + | | | + | | | +multiple +--------v---------+ | + times |ug_layout_row() | | + | |ug_layout_column()| | + | |ug_layout_float() | | + | | ... | | + | +--------+---------+ | + | | | + | | | + | +------v------+ | + | |ug_button() | | + | |ug_text_box()| | + | |ug_slider() | | + | | ... | | + | +------+------+ | + | | | + +--------------+ | + | | + +--------v---------+ | + |ug_window_end() | | + |ug_container_end()| | + |ug_div_end() | | + | ... | | + +--------+---------+ | + | | + | | + | | + +------v-------+ | + |ug_frame_end()| | + +------+-------+ | + | | + | | + | | + +------v-------+ | + |user draws the| | + | ui +-------+ + +------+-------+ + | + |quit + | + +------v-------+ + | ug_destroy() | + +--------------+ +``` + +### Layouting + +Layouting happens in a dynamic grid, when a new element is inserted in a non-floating +manner it reserves a space in the grid, new elements are placed following this grid. + +Every div has two points of origin, one for the row layout and one for the column +layout, named origin_r and origin_c respectively + +origin_r is used when the row layout is used and it is used to position the child +elements one next to the other, as such it always points to the top-right edge +of the last row element + +```text +Layout: row +#: lost space + Parent div +x---------------------------------+ +|[origin_c] | +|[origin_r] | +| | +| | +| | +| | +| | +| | +| | ++---------------------------------+ + + Parent div ++-----x---------------------------+ +| |[origin_r] | +| E1 | | +| | | +x-----+---------------------------+ +|[origin_c] | +| | | +| | | +| | | +| | | +| | | ++-----+---------------------------+ + + Parent div ++-----+----------+-----x----------+ +| | E2 | |[origin_r]| +| E1 +----------+ | | +| |##########| E3 | | ++-----+##########| | | +|################| | | ++----------------x-----+----------+ +| [origin_c] | +| | | +| | | +| | | ++----------------+----------------+ +``` + +TODO: handle when the content overflows the div +- Use a different concept, like a view or relative space, for example the child +div could have position `[0,0]` but in reality it is relative to the origin of the +parent div +- each div could have a view and a total area of the content, when drawing everything +is clipped to the view and scrollbars are shown +- individual elements accept dimensions and the x/y coordinates could be interpreted +as offset if the layout is row/column or absolute coordinates if the leayout is floating + +A div can be marked resizeable or fixed, and static or dynamic. The difference being +that resizeable adds a resize handle to the div and dynamic lets the content overflow +causing scrollbars to be drawn + +### Notes + +How elements determine if they have focus or not + +```C +// in begin_{container} code +calculate focus +set has_focus property + + + +// in the element code +if(PARENT_HAS_FOCUS()) { + update stuff +} else { + fast path to return +} +``` + +How to get ids: + 1. use a name for each element + 2. supply an id for each element + 3. use a macro and the line position as id and then hash it + 4. use a macro, get the code line and hash it diff --git a/LAYOUT b/LAYOUT deleted file mode 100644 index 700b43d..0000000 --- a/LAYOUT +++ /dev/null @@ -1,243 +0,0 @@ - Div Children Alignment -+------------------------------------------------+ -|TOP-LEFT TOP TOP-RIGHT| -| | -| | -| | -| | -| | -| | -|LEFT CENTER RIGHT| -| | -| | -| | -| | -| | -| | -|BOTTOM-LEFT BOTTOM BOTTOM-RIGHT| -+------------------------------------------------+ - -ALIGNMENT CHART: -+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ -| TOP-LEFT, ROW: | TOP-LEFT, COLUMN: | BOTTOM, ROW: | BOTTOM, COLUMN: | TOP-RIGHT, ROW: | TOP-RIGHT, COLUMN: | -| | | | | | | -| +------------------------- - | +----------- - | | +----+ | | - -----------+ | -| |+-------++-----++-----+ | |+-------+ | | | E1 | | | +-------+| | -| || E1 || E2 || | | || E1 | | | | | | - -----------------------+ | | E1 || | -| || |+-----+| E3 | | || | | | +----+ | +-------++-----++-----+| | | || | -| |+-------+ | | | |+-------+ | +-------+ +---+ | +------+ | | E1 || E2 || || | +-------+| | -| | +-----+ | |+----+ | | E1 |+------+|E3 | | | E2 | | | |+-----+| E3 || | +----+| | -| ' | || E2 | | | || E2 || | | | | | +-------+ | || | | E2 || | -| ' | |+----+ | +-------++------++---+ | +------+ | +-----+| | +----+| | -| | |+---------+ | - ------------------------ - | +--+ | ' | +---------+| | -| | || E3 | | | |E3| | ' | | E3 || | -| | |+---------+ | | +--+ | | +---------+| | -| | ' | | - ----------- - | | ' | -| | ' | | | | ' | -| | | | | | | -+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ -| LEFT, ROW: | LEFT, COLUMN: | BOTTOM-RIGHT, ROW: | BOTTOM-RIGHT, COLUMN: | TOP, ROW: | TOP, COLUMN: | -| | | | | | | -| | ' | | ' | | - -------------- - | -| ' | |+-------+ | | +-------+| | | +----------+ | -| | +----+ | || E1 | | ' | | E1 || | - ----------------------- - | | E1 | | -| |+------+ | | | || | | +-----+| | | || | +------++----++-----+ | | | | -| || |+-----+| | | |+-------+ | +-------+ | || | +-------+| | | E1 || E2 || E3 | | +----------+ | -| || E1 || E2 || E3 | | |+----+ | | E1 |+-----+| E3 || | +----+| | | |+----+| | | +--------+ | -| || |+-----+| | | || E2 | | | || E2 || || | | E2 || | +------+ | | | | E2 | | -| |+------+ | | | |+----+ | +-------++-----++-----+| | +----+| | +-----+ | +--------+ | -| | +----+ | |+---------+ | - -----------------------+ | +---------+| | | +------+ | -| ' | || E3 | | | | E3 || | | | E3 | | -| ' | |+---------+ | | +---------+| | | | | | -| | ' | | - -----------+ | | +------+ | -| | ' | | | | | -+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ -| BOTTOM-LEFT, ROW: | BOTTOM-LEFT, COLUMN: | RIGHT, ROW: | RIGHT, COLUMN: | CENTER, ROW: | CENTER, COLUMN: | -| | | | | | | -| | ' | | ' | | | | -| | |+-------+ | | +-------+| | | | +-----------+ | -| | || E1 | | ' | | E1 || | | | | E1 | | -| ' | || | | +----+| | | || | | +----+ | | | | | -| | +-----+ | |+-------+ | +------+ | || | +-------+| | +------+ | | | | +-----------+ | -| |+-------+ | | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ | -| || E1 |+-----+| E3 | | || E2 | | | E1 || E2 || E3 || | | E2 || | ---|--E1--||-E2-||-E3-|--- | ----|---E2----|---- | -| || || E2 || | | |+----+ | | |+-----+| || | +----+| | | |+----+| | | +---------+ | -| |+-------++-----++-----+ | |+---------+ | +------+ | || | +---------+| | +------+ | | | | +-------+ | -| +------------------------- - | || E3 | | +----+| | | E3 || | | +----+ | | E3 | | -| | |+---------+ | ' | +---------+| | | | | | | | -| | +----------- - | ' | ' | | | +-------+ | -| | | | ' | | | | -| | | | | | | -+------------------------------+----------------------+------------------------------+-----------------------+------------------------------+----------------------+ - -div ( - align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER - size_x/y: EXACT(x) | GROW() | FIT(min, max) - scroll_x/y: true | false - resize_x/y: true | false - layout: ROW | COLUMN -) - -align: alignment of the children elements -size: how the div should be sized -scroll: enables scrollbars -layout: the layout direction of the children - - COLUMN ROW - +--------------------+ +----------------------------------------------------+ - | +----------------+ | |+----------------+ | - | | | | || |+------------+ | - | | | | || || |+------------------+| - | | E1 | | || E1 || E2 || E3 || - | | | | || || |+------------------+| - | | | | || |+------------+ | - | +----------------+ | |+----------------+ | - | +------------+ | +----------------------------------------------------+ - | | | | - | | E2 | | - | | | | (both have center alignment) - | +------------+ | - |+------------------+| - || || - || E3 || - || || - || || - |+------------------+| - +--------------------+ - -Element { - id: uint - sizing: { min_w, min_h max_w, max_h } - bounds: { x, y, w, h } -} - -id: unique identifier of the element -sizing: the size that the element wants -bounds: the absoulte bounds that the element got assigned - -Rendering -========= - -Rendering happens when the element is called (immediately for leaf widgets like buttons and at the end -for root widgets like divs). The drawing is done on the bounds assigned to the widget, these bounds -have a one-frame delay on the current layout. - -The layout is calculated by each div at the end of their block and at frame end all the sizes and positions -are assigned at frame end by iterating the element tree. - -ElemDiv { - align: TOP-LEFT | LEFT | BOTTOM-LEFT | BOTTOM | BOTTOM-RIGHT | RIGHT | TOP-RIGHT | RIGHT | CENTER - size_x/y: { min, max } - scroll_x/y: true | false - layout: ROW | COLUMN - children_size_x/y: { min, max } -} - -size: - - min != max -> FIT sizing, fit to the content but respect the min and max size - - min == max == 0 -> GROW sizing, grow to the max amount of space possible - - min == max != 0 -> EXACT sizing -children_size: the size of the combined children sizes - - - -root(size_x: screen width, size_y: screen height, layout: ROW) { - - div1(layout: COLUMN, size_x: FIT, size_y GROW, resize_x: true) { - E1() - E2() - E3() - E4() - } <-(end div 1) - - div2(size_x: GROW, size_y: GROW) { - ... - } <-(end div 2) - - div3(layout: COLUMN, size_x: FIT, size_y: GROW) { - E5() - E6() - E7() - } <-(end div 3) - -} <-(end root) -(frame end) - - -+-Root-Div------------------------------------------------+ -|+-Div-1----------++-Div-2--------------------++-Div-3---+| -||+--------------+|| ||+-------+|| -||| E1 ||| ||| E5 ||| -||| ||| ||| ||| -||+--------------+|| ||+-------+|| [Root Div] -||+--------------+|| ||+-------+|| | -||| E2 ||| ||| E6 ||| +----------+----+-------+ -||| ||| ||| ||| v v v -||+--------------+|| ||+-------+|| [Div 1] [Div 2] [Div 3] -||+------+ || ||+-------+|| | | -||| | || ||| E7 ||| +----+----+----+ | -||| E3 | || ||| ||| v v v v | -||| | || ||+-------+|| [E1] [E2] [E3] [E4] +----+----+ -||+------+ || || || v v v -||+------+ || || || [E5] [E6] [E7] -||| | || || || -||| E4 | || || || -||| | || || || -||+------+ || || || -|| || || || -|+----------------++--------------------------++---------+| -+---------------------------------------------------------+ - -the call order is as follows - -E1() -> updates the children size of div1 -E2() -> " " -E3() -> " " -E4() -> " " -end div1() -> updates the children size of root -end div2() -> updates the children size of root -E5() -> updates the children size of div3 -E6() -> " " -E7() -> " " -end root() -> does nothing - -at frame end: - * Root: the root has a size constraint of fit so the bounds get assigned the whole window - * Div 1: the width has a size of fit, so it gets set to the children bounds, the height is set - to the root height since it has a height of GROW - - E1 to E4 get laid out - * Div 2: it has a width of GROW which is **along** the layout axis, so it gets added to grow list - the height gets set to the root height - * Div 3: the width is FIT, so it gets set to the content width, the height gets se to the root - height. - - E5 to E7 get laid out - * Div 2: is given a width (if there were other contending grow divs along the layout axis they - would also get sized). - - Now that div 2 has a size all it's children can be given a size - -Styling -======= - -The element bounds include the whole CSS box model: - -+---------------------------------------------+ -| MARGIN | -| +-----------------------------------+ | -| |xxxxxxxxxxx BORDER xxxxxxxxxxxx| | -| |x+-------------------------------+x| | -| |x| PADDING |x| | -| |x| +-----------------------+ |x| | -| |x| | | |x| | -| |x| | CONTENT | |x| | -| |x| | | |x| | -| |x| +-----------------------+ |x| | -| |x| |x| | -| |x+-------------------------------+x| | -| |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| | -| +-----------------------------------+ | -| | -+---------------------------------------------+ - -Styling happens via a .css file, the sizing strictly refers to the content, so if the the user -requests an exact size of 100px*100px the content box will have those dimensions, but the element -bounds will be larger. diff --git a/LICENSE b/LICENSE index 0a04128..e69de29 100644 --- a/LICENSE +++ b/LICENSE @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4f151b9 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +test_renderer: test_renderer.c3 src/renderer.c3 resources/shaders/source/* + scripts/compile_shaders.sh + c3c compile -g -O0 test_renderer.c3 src/renderer.c3 --libdir lib --lib sdl3 --lib ugui diff --git a/README.md b/README.md index 151118a..e69de29 100644 --- a/README.md +++ b/README.md @@ -1 +0,0 @@ -Welcome to the ugui library. diff --git a/TODO b/TODO index f2fa0fa..96b1ba4 100644 --- a/TODO +++ b/TODO @@ -4,22 +4,21 @@ [x] Implement div.view and scrollbars [x] Port font system from C to C3 (rewrite1) [ ] Update ARCHITECTURE.md -[x] Write a README.md -[x] Use an arena allocator for cache +[ ] Write a README.md +[ ] Use an arena allocator for cache [ ] Do not redraw if there was no update (no layout and no draw) [ ] Do command buffer damage tracking based on a context grid (see rxi writeup) -[x] Better handling of the active and focused widgets, try to maintain focus until mouse release (fix scroll bars) +[x] Better handling of the active and focused widgets, try +to maintain focus until mouse release (fix scroll bars) [x] Clip element bounds to parent div, specifically text [ ] Resizeable divs [x] Implement a z index and sort command buffer based on that [ ] Ctx.set_z_index() [x] Sort command buffer on insertion -[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot - of boiler plate and reuse it -[x] The id combination in gen_id() uses an intger division, which is costly, use another combination - function that is non-linear and doesn't use division +[x] Standardize element handling, for example all buttons do almost the same thing, so write a lot of boiler plate and reuse it +[x] The id combination in gen_id() uses an intger division, which is costly, use another combination function that is non-linear and doesn't use division [ ] Animations, somehow -[x] Maybe cache codepoint converted strings +[ ] Maybe cache codepoint converted strings [x] Fix scroll wheel when div is scrolled [ ] Be consistent with the initialization methods some are foo.new() and some are foo.init() [ ] Implement image loading (.bmp, .ff, .qoi and .png), in the future even lossy images like .jpg @@ -29,52 +28,31 @@ [ ] .png [ ] .jpg [ ] gif support? -[x] layout_set_max_rows() and layout_set_max_columns() +[ ] layout_set_max_rows() and layout_set_max_columns() [x] Maybe SDF sprites?? [x] Stylesheets and stylesheet import [x] use SDF to draw anti-aliased rounded rectangles https://zed.dev/blog/videogame [ ] Subdivide modules into ugui::ug for exported functions and ugui::core for internal use functions (used to create widgets) [x] The render loop RAPES the gpu, valve pls fix -[x] The way the element structures are implemented wastes a lot of memory since +[ ] The way the element structures are implemented wastes a lot of memory since each struct Elem, struct Cmd, etc. is as big as the largest element. It would be better to use a different allcation strategy. [ ] Add a way to handle time events like double clicks -[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the - content, the background color is applied starting from the border. Right now push_rect() offsets - the background rect by both border and padding -[x] Investigate why the debug pointer (cyan rectangle) disappears... -[ ] Selectable divs -[ ] Selectable text -[ ] Copy buffer +[ ] Border and padding do not go well together if the library issues two rect commands, the visible + border is effectively the border size plus the padding since there is a gap between the border + rect and the internal rect. A better solution is to leave it up to the renderer to draw the rect + correctly ## Layout [x] Flexbox -[x] Center elements to the row/column -[x] Text wrapping / reflow -[x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct -[x] Correct whitespace handling in text (\t \r etc) -[x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay) +[ ] For some reason padding is not correct, look at the sliders, they have 2px per side when the + theme specifies 4px per side +[ ] Center elements to the row/column +[ ] Text wrapping / reflow +[ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay) instead of the curren multi-frame approach. -[x] Implement column/row sizing (min, max) -[x] Implement a way to size the element as the current row/column size - * +-------------+ - * | | - * +-------------+ - * +--+ - * | | - * +--+ - * <-------------> - * column size - See the calculator example for why it is useful -[ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing - of elements -[x] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly) -[x] Use containing_rect() in position_element() to skip some computing and semplify the function -[x] Rename position_element() to layout_element() -[x] Make functions to mark rows/columns as full, to fix the calculator demo -[ ] Grids ## Input @@ -82,9 +60,7 @@ [x] Mouse scroll wheel [ ] Touch input [x] Do not set input event to true if the movement was zero (like no mouse movement) -[x] Use input event flags, for example to consume the input event -[x] Fix bug in text box: when spamming keys you can get multiple characters in the text input field - of the context, this causes a bug where only the first char is actually used +[ ] Use input event flags, for example to consume the input event ## Commands @@ -92,10 +68,9 @@ - border width - border radius [x] add a command to update an atlas -[x] New window command, useful for popups -[x] Text command returns the text bounds, this way we can avoid the pattern +[ ] New window command, useful for popups +[ ] Text command returns the text bounds, this way we can avoid the pattern draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ... -[ ] Rounded rectangle with different radius for each corner ## Atlas @@ -118,6 +93,11 @@ [x] Checkbox [ ] Selectable text box +## Main / exaple + +[ ] Create maps from ids to textures and images instead of hardcoding them + + ## API [ ] Introduce a Layout structure that specifies the positioning of elements inside diff --git a/linux-x64/.gitkeep b/docs/.gitkeep similarity index 100% rename from linux-x64/.gitkeep rename to docs/.gitkeep diff --git a/lib/.gitkeep b/lib/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/grapheme.c3l/Makefile b/lib/grapheme.c3l/Makefile new file mode 100644 index 0000000..09193f1 --- /dev/null +++ b/lib/grapheme.c3l/Makefile @@ -0,0 +1,4 @@ +all: + make -C thirdparty/libgrapheme + mkdir -p linux-x64 + cp thirdparty/libgrapheme/libgrapheme.a linux-x64/libgrapheme.a diff --git a/lib/grapheme.c3l/libgrapheme.c3i b/lib/grapheme.c3l/libgrapheme.c3i new file mode 100644 index 0000000..6b1c7fc --- /dev/null +++ b/lib/grapheme.c3l/libgrapheme.c3i @@ -0,0 +1,46 @@ +module grapheme; + +const uint GRAPHEME_INVALID_CODEPOINT = 0xFFFD; + +enum BidirectionalDirection { + GRAPHEME_BIDIRECTIONAL_DIRECTION_NEUTRAL, + GRAPHEME_BIDIRECTIONAL_DIRECTION_LTR, + GRAPHEME_BIDIRECTIONAL_DIRECTION_RTL, +} + +fn isz bidirectional_get_line_embedding_levels(uint *, isz, ichar *, isz) @extern("grapheme_bidirectional_get_line_embedding_levels"); + +fn isz bidirectional_preprocess_paragraph(uint *, isz, BidirectionalDirection, uint *, isz, BidirectionalDirection *) @extern("grapheme_bidirectional_preprocess_paragraph"); + +fn isz bidirectional_reorder_line(uint *, uint *, isz, uint *, isz) @extern("grapheme_bidirectional_reorder_line"); + +fn isz decode_utf8(char *, isz, uint *) @extern("grapheme_decode_utf8"); +fn isz encode_utf8(uint, char *, isz) @extern("grapheme_encode_utf8"); + +fn bool is_character_break(uint, uint, ushort *) @extern("grapheme_is_character_break"); + +fn bool is_lowercase(uint *, isz, isz *) @extern("grapheme_is_lowercase"); +fn bool is_titlecase(uint *, isz, isz *) @extern("grapheme_is_titlecase"); +fn bool is_uppercase(uint *, isz, isz *) @extern("grapheme_is_uppercase"); + +fn bool is_lowercase_utf8(char *, isz, isz *) @extern("grapheme_is_lowercase_utf8"); +fn bool is_titlecase_utf8(char *, isz, isz *) @extern("grapheme_is_titlecase_utf8"); +fn bool is_uppercase_utf8(char *, isz, isz *) @extern("grapheme_is_uppercase_utf8"); + +fn isz next_character_break(uint *, isz) @extern("grapheme_next_character_break"); +fn isz next_line_break(uint *, isz) @extern("grapheme_next_line_break"); +fn isz next_sentence_break(uint *, isz) @extern("grapheme_next_sentence_break"); +fn isz next_word_break(uint *, isz) @extern("grapheme_next_word_break"); + +fn isz next_character_break_utf8(char *, isz) @extern("grapheme_next_character_break_utf8"); +fn isz next_line_break_utf8(char *, isz) @extern("grapheme_next_line_break_utf8"); +fn isz next_sentence_break_utf8(char *, isz) @extern("grapheme_next_sentence_break_utf8"); +fn isz next_word_break_utf8(char *, isz) @extern("grapheme_next_word_break_utf8"); + +fn isz to_lowercase(uint *, isz, uint *, isz) @extern("grapheme_to_lowercase"); +fn isz to_titlecase(uint *, isz, uint *, isz) @extern("grapheme_to_titlecase"); +fn isz to_uppercase(uint *, isz, uint *, isz) @extern("grapheme_to_uppercase"); + +fn isz to_lowercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_lowercase_utf8"); +fn isz to_titlecase_utf8(char *, isz, char *, isz) @extern("grapheme_to_titlecase_utf8"); +fn isz to_uppercase_utf8(char *, isz, char *, isz) @extern("grapheme_to_uppercase_utf8"); diff --git a/lib/grapheme.c3l/manifest.json b/lib/grapheme.c3l/manifest.json new file mode 100644 index 0000000..97f04d0 --- /dev/null +++ b/lib/grapheme.c3l/manifest.json @@ -0,0 +1,9 @@ +{ + "provides": "grapheme", + "targets": { + "linux-x64": { + "dependencies": [], + "linked-libraries": ["grapheme", "c"] + } + } +} diff --git a/lib/grapheme.c3l/project.json b/lib/grapheme.c3l/project.json new file mode 100644 index 0000000..4b2d212 --- /dev/null +++ b/lib/grapheme.c3l/project.json @@ -0,0 +1,14 @@ +{ + "langrev": "1", + "warnings": ["no-unused"], + "dependency-search-paths": [".."], + "dependencies": ["grapheme"], + "authors": ["Alessandro Mauri ", "Laslo Hunhold "], + "version": "0.1.0", + "sources": [], + "output": "build", + "target": "linux-x64", + "features": [], + "cpu": "generic", + "opt": "O0" +} diff --git a/lib/grapheme.c3l/thirdparty/libgrapheme b/lib/grapheme.c3l/thirdparty/libgrapheme new file mode 160000 index 0000000..65b354f --- /dev/null +++ b/lib/grapheme.c3l/thirdparty/libgrapheme @@ -0,0 +1 @@ +Subproject commit 65b354f0fcb1d925f4340dbb4415ea06e8af2bec diff --git a/lib/schrift.c3l/Makefile b/lib/schrift.c3l/Makefile new file mode 100644 index 0000000..1de46d5 --- /dev/null +++ b/lib/schrift.c3l/Makefile @@ -0,0 +1,4 @@ +all: + make -C thirdparty/libschrift + mkdir -p linux-x64 + cp thirdparty/libschrift/libschrift.a linux-x64/libschrift.a diff --git a/lib/schrift.c3l/libschrift.c3 b/lib/schrift.c3l/libschrift.c3 new file mode 100644 index 0000000..699d296 --- /dev/null +++ b/lib/schrift.c3l/libschrift.c3 @@ -0,0 +1,58 @@ +module schrift; + +alias SftFont = void*; +alias SftUChar = uint; +alias SftGlyph = uint; + +const int SFT_DOWNWARD_Y = 0x01; + +struct Sft +{ + SftFont font; + double xScale; + double yScale; + double xOffset; + double yOffset; + int flags; +} + +struct SftLMetrics +{ + double ascender; + double descender; + double lineGap; +} + +struct SftGMetrics +{ + double advanceWidth; + double leftSideBearing; + int yOffset; + int minWidth; + int minHeight; +} + +struct SftKerning +{ + double xShift; + double yShift; +} + +struct SftImage +{ + void *pixels; + int width; + int height; +} + +extern fn ZString sft_version() @extern("sft_version"); + +extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem"); +extern fn SftFont loadfile(ZString filename) @extern("sft_loadfile"); +extern fn void freefont(SftFont font) @extern("sft_freefont"); + +extern fn int lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics"); +extern fn int lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup"); +extern fn int gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics"); +extern fn int kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning"); +extern fn int render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render"); diff --git a/lib/schrift.c3l/manifest.json b/lib/schrift.c3l/manifest.json new file mode 100644 index 0000000..efb7f23 --- /dev/null +++ b/lib/schrift.c3l/manifest.json @@ -0,0 +1,9 @@ +{ + "provides" : "schrift", + "targets" : { + "linux-x64" : { + "dependencies" : [], + "linked-libraries" : ["schrift", "c"] + } + } +} diff --git a/lib/schrift.c3l/project.json b/lib/schrift.c3l/project.json new file mode 100644 index 0000000..92b049c --- /dev/null +++ b/lib/schrift.c3l/project.json @@ -0,0 +1,14 @@ +{ + "langrev": "1", + "warnings": [ "no-unused" ], + "dependency-search-paths": [ ".." ], + "dependencies": [ "schrift" ], + "authors": [ "Alessandro Mauri ", "Thomas Oltmann " ], + "version": "0.1.0", + "sources": [ ], + "output": "build", + "target": "linux-x64", + "features": [], + "cpu": "generic", + "opt": "O0", +} diff --git a/lib/schrift.c3l/thirdparty/libschrift b/lib/schrift.c3l/thirdparty/libschrift new file mode 160000 index 0000000..24737d2 --- /dev/null +++ b/lib/schrift.c3l/thirdparty/libschrift @@ -0,0 +1 @@ +Subproject commit 24737d2922b23df4a5692014f5ba03da0c296112 diff --git a/lib/sdl3.c3l b/lib/sdl3.c3l new file mode 160000 index 0000000..076355e --- /dev/null +++ b/lib/sdl3.c3l @@ -0,0 +1 @@ +Subproject commit 076355e2d126e7546e53663b97e8dec22667d34d diff --git a/lib/ugui.c3l/LICENSE b/lib/ugui.c3l/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/lib/ugui.c3l/README.md b/lib/ugui.c3l/README.md new file mode 100644 index 0000000..151118a --- /dev/null +++ b/lib/ugui.c3l/README.md @@ -0,0 +1 @@ +Welcome to the ugui library. diff --git a/lib/ugui.c3l/linux-x64/.gitkeep b/lib/ugui.c3l/linux-x64/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/manifest.json b/lib/ugui.c3l/manifest.json similarity index 76% rename from manifest.json rename to lib/ugui.c3l/manifest.json index e34f342..813ed39 100644 --- a/manifest.json +++ b/lib/ugui.c3l/manifest.json @@ -4,7 +4,7 @@ "targets" : { "linux-x64" : { "link-args" : [], - "dependencies" : ["schrift"], + "dependencies" : ["schrift", "grapheme"], "linked-libraries" : [] } } diff --git a/lib/ugui.c3l/scripts/.gitkeep b/lib/ugui.c3l/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/cache.c3 b/lib/ugui.c3l/src/cache.c3 similarity index 74% rename from src/cache.c3 rename to lib/ugui.c3l/src/cache.c3 index 53141a4..130ffd1 100644 --- a/src/cache.c3 +++ b/lib/ugui.c3l/src/cache.c3 @@ -1,4 +1,4 @@ -module cache; +module cache{Key, Value, SIZE}; /* LRU Cache * The cache uses a pool (array) to store all the elements, each element has @@ -9,19 +9,22 @@ module cache; * the elements that were not recently used. */ +// FIXME: this module should really allocate all resources on an arena or temp +// allocator, since all memory allocations are connected and freeing +// happens at the same time + import std::core::mem; -import std::core::mem::alloc; +import std::core::mem::allocator; import std::collections::bitset; import std::collections::map; alias BitArr = bitset::BitSet{SIZE}; -alias IdTable = map::HashMap{Key, sz}; -alias IdTableEntry = map::Entry{Key, sz}; +alias IdTable = map::HashMap{Key, usz}; +alias IdTableEntry = map::Entry{Key, usz}; const usz CACHE_NCYCLES = (usz)(SIZE * 2.0/3.0); struct Cache { - Allocator allocator; BitArr present, used; IdTable table; Value[] pool; @@ -29,7 +32,7 @@ struct Cache { } // Every CACHE_CYCLES operations mark as not-present the unused elements -macro Cache.cycle(&cache) { +macro Cache.cycle(&cache) @private { cache.cycle_count++; if (cache.cycle_count > CACHE_NCYCLES) { for (usz i = 0; i < cache.present.data.len; i++) { @@ -40,20 +43,19 @@ macro Cache.cycle(&cache) { } } -fn void? Cache.init(&cache, Allocator allocator) +fn void? Cache.init(&cache) { - cache.allocator = allocator; - cache.table.init(allocator, capacity: SIZE); + cache.table.init(allocator::heap(), capacity: SIZE); // FIXME: this shit is SLOW foreach (idx, bit : cache.used) { cache.used[idx] = false; } foreach (idx, bit : cache.present) { cache.present[idx] = false; } - cache.pool = alloc::new_array(allocator, Value, SIZE); + cache.pool = mem::new_array(Value, SIZE); } fn void Cache.free(&cache) { (void)cache.table.free(); - (void)alloc::free(cache.allocator, cache.pool); + (void)mem::free(cache.pool); } fn Value*? Cache.search(&cache, Key id) @@ -64,18 +66,17 @@ fn Value*? Cache.search(&cache, Key id) /* MISS, wrong key */ if (entry.key != id) { cache.table.remove(id)!; - return NOT_FOUND~; + return NOT_FOUND?; } /* MISS, the data is not valid (not present) */ if (!cache.present[entry.value]) { // if the data is not present but it is still in the table, remove it cache.table.remove(id)!; - return NOT_FOUND~; + return NOT_FOUND?; } /* HIT, set as recently used */ - //io::printfn("HIT: %d [%d]", entry.value, entry.key); cache.used[entry.value] = true; return &(cache.pool[entry.value]); } @@ -94,22 +95,20 @@ fn void Cache.remove(&cache, Key id) /* Look for a free spot in the present bitmap and return its index */ /* If there is no free space left then just return the first position */ -fn sz Cache.get_free_spot(&cache) +fn usz Cache.get_free_spot(&cache) @private { - const BITS = @bitsizeof(cache.present.data[0]); + const BITS = $typeof(cache.present.data[0]).sizeof*8; foreach (idx, d: cache.present.data) { - if (d != $Typeof(d)::max) { - sz spot = idx*BITS + BITS-d.clz(); - if (cache.used[spot]) unreachable("free spot is not actually free: %d", spot); - return spot; + if (d.clz() != BITS) { + return idx*BITS + BITS-d.clz(); } } return 0; } -<* @require g != null *> -fn Value*? Cache.insert_at(&cache, Value* g, Key id, sz index) +fn Value*? Cache.insert_at(&cache, Value *g, Key id, usz index) @private { + // TODO: verify index, g and id Value* spot; /* Set used and present */ @@ -124,20 +123,18 @@ fn Value*? Cache.insert_at(&cache, Value* g, Key id, sz index) } // Insert an element in the cache, returns the index -<* @require g != null *> fn Value*? Cache.insert_new(&cache, Value* g, Key id) { - sz index = cache.get_free_spot(); + usz index = cache.get_free_spot(); return cache.insert_at(g, id, index); } -<* @require g != null *> fn Value*? Cache.get_or_insert(&cache, Value* g, Key id, bool *is_new = null) { Value*? c = cache.search(id); if (catch e = c) { if (e != NOT_FOUND) { - return e~; + return e?; } else { // if the element is new (inserted) set the is_new flag if (is_new) *is_new = true; diff --git a/lib/ugui.c3l/src/fifo.c3 b/lib/ugui.c3l/src/fifo.c3 new file mode 100644 index 0000000..d9fc52e --- /dev/null +++ b/lib/ugui.c3l/src/fifo.c3 @@ -0,0 +1,91 @@ +module fifo::faults; +faultdef FULL, EMPTY; + +module fifo{Type}; + +import std::core::mem; +import std::sort; + +// TODO: specify the allocator + +struct Fifo { + Type[] arr; + usz out; + usz count; +} + +fn void? Fifo.init(&fifo, usz size) +{ + fifo.arr = mem::new_array(Type, size); + fifo.out = 0; + fifo.count = 0; +} + +fn void Fifo.free(&fifo) +{ + (void)mem::free(fifo.arr); +} + +fn void? Fifo.enqueue(&fifo, Type *elem) +{ + if (fifo.count >= fifo.arr.len) { + return fifo::faults::FULL?; + } + usz in = (fifo.out + fifo.count) % fifo.arr.len; + fifo.arr[in] = *elem; + fifo.count++; +} + +fn Type*? Fifo.dequeue(&fifo) +{ + if (fifo.count == 0) { + return fifo::faults::EMPTY?; + } + Type *ret = &fifo.arr[fifo.out]; + fifo.count--; + fifo.out = (fifo.out + 1) % fifo.arr.len; + return ret; +} + +macro Type Fifo.get(&fifo, usz i) @operator([]) +{ + return fifo.arr[(fifo.out + i) % fifo.arr.len]; +} + +fn void Fifo.set(&fifo, usz i, Type val) @operator([]=) +{ + fifo.arr[(fifo.out + i) % fifo.arr.len] = val; +} + +macro Type* Fifo.get_ref(&fifo, usz i) @operator(&[]) +{ + return &fifo.arr[(fifo.out + i) % fifo.arr.len]; +} + +macro usz Fifo.len(&fifo) @operator(len) +{ + return fifo.count; +} + +fn void? Fifo.sort(&fifo) +{ + Type[] arr = mem::new_array(Type, fifo.count); + defer mem::free(arr); + + foreach(i, c: fifo) { + arr[i] = c; + } + + // doesn't keep ordering + //sort::quicksort(arr); + + // seems to keep the right order but we will never know... + // also since most things are already ordered the time is closer to O(n) than to O(n^2) + sort::insertionsort(arr); + + fifo.count = 0; + fifo.out = 0; + foreach (&c: arr) { + fifo.enqueue(c)!; + } +} \ No newline at end of file diff --git a/src/atlas.c3 b/lib/ugui.c3l/src/ugui_atlas.c3 similarity index 70% rename from src/atlas.c3 rename to lib/ugui.c3l/src/ugui_atlas.c3 index 3bc2dbf..7f3a40b 100644 --- a/src/atlas.c3 +++ b/lib/ugui.c3l/src/ugui_atlas.c3 @@ -4,9 +4,9 @@ import std::io; faultdef CANNOT_PLACE, INVALID_TYPE; -enum AtlasType : (sz bpp, typeid underlying) { - ATLAS_GRAYSCALE {1, char}, - ATLAS_R8G8B8A8 {4, uint}, +enum AtlasType { + ATLAS_GRAYSCALE, + ATLAS_R8G8B8A8, } // black and white atlas @@ -21,6 +21,23 @@ struct Atlas { ushort row_h; } +// bytes per pixel +macro usz AtlasType.bpp(type) +{ + switch (type) { + case ATLAS_GRAYSCALE: return 1; + case ATLAS_R8G8B8A8: return 4; + } +} + +macro typeid AtlasType.underlying(type) +{ + switch (type) { + case ATLAS_GRAYSCALE: return char; + case ATLAS_R8G8B8A8: return uint; + } +} + /* // FIXME: in and out types are not always known at compile time macro @pixel_convert(p, AtlasType $in, AtlasType $out) @@ -35,10 +52,10 @@ macro @pixel_convert(p, AtlasType $in, AtlasType $out) var b = ((p >> 16) & 0xff); var a = ((p >> 24) & 0xff); if (a == 0) return (char)0; - return (ATLAS_GRAYSCALE.underlying)(((float)r+g+b) / 3.0f); + return (ATLAS_GRAYSCALE.underlying())(((float)r+g+b) / 3.0f); $case $in == ATLAS_GRAYSCALE && $out == ATLAS_R8G8B8A8: var x = (char)(p/3.0); - return (ATLAS_R8G8B8A8.underlying)(x|(x<<8)|(x<<16)|(255<<24)); + return (ATLAS_R8G8B8A8.underlying())(x|(x<<8)|(x<<16)|(255<<24)); $default: $error "Unimplemented pixel format conversion"; $endswitch $endif @@ -52,7 +69,7 @@ fn void? Atlas.new(&atlas, Id id, AtlasType type, ushort width, ushort height) atlas.width = width; atlas.height = height; - atlas.buffer = mem::new_array(char, (sz)atlas.width*atlas.height*type.bpp); + atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height*type.bpp()); } fn void Atlas.free(&atlas) @@ -86,26 +103,26 @@ fn Point? Atlas.place(&atlas, char[] pixels, ushort w, ushort h, ushort stride) p = atlas.row; } else { atlas.row.x = 0; - atlas.row.y = (short)atlas.row.y + (short)atlas.row_h; + atlas.row.y = atlas.row.y + atlas.row_h; atlas.row_h = 0; if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) { p = atlas.row; } else { - return CANNOT_PLACE~; + return CANNOT_PLACE?; } } - sz bpp = atlas.type.bpp; - for (sz y = 0; y < h; y++) { - for (sz x = 0; x < w; x++) { - char[] buf = atlas.buffer[(sz)(p.y+y)*atlas.width*bpp + (p.x+x)*bpp ..]; - char[] pix = pixels[(sz)y*stride*bpp + x*bpp ..]; + usz bpp = atlas.type.bpp(); + for (usz y = 0; y < h; y++) { + for (usz x = 0; x < w; x++) { + char[] buf = atlas.buffer[(usz)(p.y+y)*atlas.width*bpp + (p.x+x)*bpp ..]; + char[] pix = pixels[(usz)y*stride*bpp + x*bpp ..]; buf[0..bpp-1] = pix[0..bpp-1]; } } - atlas.row.x += (short)w; + atlas.row.x += w; if (h > atlas.row_h) { atlas.row_h = h; } diff --git a/lib/ugui.c3l/src/ugui_button.c3 b/lib/ugui.c3l/src/ugui_button.c3 new file mode 100644 index 0000000..344cea5 --- /dev/null +++ b/lib/ugui.c3l/src/ugui_button.c3 @@ -0,0 +1,187 @@ +module ugui; + +import std::io; + +// button element +struct ElemButton { + int filler; +} + + +// draw a button, return the events on that button +macro Ctx.button(&ctx, Rect size, bool state = false, ...) + => ctx.button_id(@compute_id($vasplat), size, state); +fn ElemEvents? Ctx.button_id(&ctx, Id id, Rect size, bool active) +{ + id = ctx.gen_id(id)!; + + Elem *parent = ctx.get_parent()!; + Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; + Style* style_norm = ctx.styles.get_style(@str_hash("button")); + + elem.bounds = ctx.position_element(parent, size, style_norm); + + // if the bounds are null the element is outside the div view, + // no interaction should occur so just return + if (elem.bounds.is_null()) { return {}; } + + Style* style_active = ctx.styles.get_style(@str_hash("button-active")); + + elem.events = ctx.get_elem_events(elem); + bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover; + + // Draw the button + ctx.push_rect(elem.bounds, parent.div.z_index, is_active ? style_active : style_norm)!; + + return elem.events; +} + +macro Ctx.button_label(&ctx, String label, Rect size = {0,0,short.max,short.max}, bool active = false, ...) + => ctx.button_label_id(@compute_id($vasplat), label, size, active); +fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool active) +{ + id = ctx.gen_id(id)!; + + Elem *parent = ctx.get_parent()!; + Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; + + short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; + Rect text_size = ctx.get_text_bounds(label)!; + Rect btn_size = text_size.add({0,0,10,10}); + Style* style_norm = ctx.styles.get_style(@str_hash("button")); + + // 2. Layout + elem.bounds = ctx.position_element(parent, btn_size, style_norm); + if (elem.bounds.is_null()) { return {}; } + + Style* style_active = ctx.styles.get_style(@str_hash("button-active")); + + elem.events = ctx.get_elem_events(elem); + + bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover; + Style* style = is_active ? style_active : style_norm; + + // Draw the button + text_size.x = elem.bounds.x; + text_size.y = elem.bounds.y; + Point off = ctx.center_text(text_size, elem.bounds); + text_size.x += off.x; + text_size.y += off.y; + ctx.push_rect(elem.bounds, parent.div.z_index, style)!; + ctx.push_string(text_size, label, parent.div.z_index, style.fg)!; + + return elem.events; +} + +macro Ctx.button_icon(&ctx, String icon, String on_icon = "", bool active = false, ...) + => ctx.button_icon_id(@compute_id($vasplat), icon, on_icon, active); +fn ElemEvents? Ctx.button_icon_id(&ctx, Id id, String icon, String on_icon, bool active) +{ + id = ctx.gen_id(id)!; + + Elem *parent = ctx.get_parent()!; + Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; + + Sprite* def_sprite = ctx.sprite_atlas.get(icon)!; + Sprite* on_sprite = ctx.sprite_atlas.get(on_icon) ?? &&(Sprite){}; + Rect max_size = def_sprite.rect().max(on_sprite.rect()); + + Style* style_norm = ctx.styles.get_style(@str_hash("button")); + + elem.bounds = ctx.position_element(parent, max_size, style_norm); + + // if the bounds are null the element is outside the div view, + // no interaction should occur so just return + if (elem.bounds.is_null()) { return {}; } + + Style* style_active = ctx.styles.get_style(@str_hash("button-active")); + + elem.events = ctx.get_elem_events(elem); + + bool is_active = active || ctx.elem_focus(elem) || elem.events.mouse_hover; + Style* style = is_active ? style_active : style_norm; + + Id tex_id = ctx.sprite_atlas.id; + if (active && on_icon != "") { + ctx.push_sprite(elem.bounds, on_sprite.uv(), tex_id, parent.div.z_index, type: on_sprite.type)!; + } else { + ctx.push_sprite(elem.bounds, def_sprite.uv(), tex_id, parent.div.z_index, type: def_sprite.type)!; + } + + // Draw the button + ctx.push_rect(elem.bounds, parent.div.z_index, style)!; + + return elem.events; +} + +// FIXME: this should be inside the style +macro Ctx.checkbox(&ctx, String desc, Point off, bool* active, String tick_sprite = {}, ...) + => ctx.checkbox_id(@compute_id($vasplat), desc, off, active, tick_sprite); +fn void? Ctx.checkbox_id(&ctx, Id id, String description, Point off, bool* active, String tick_sprite) +{ + id = ctx.gen_id(id)!; + + Elem *parent = ctx.get_parent()!; + Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; + Style* style = ctx.styles.get_style(@str_hash("checkbox")); + + Rect size = {off.x, off.y, style.size, style.size}; + elem.bounds = ctx.position_element(parent, size, style); + + // if the bounds are null the element is outside the div view, + // no interaction should occur so just return + if (elem.bounds.is_null()) return; + + elem.events = ctx.get_elem_events(elem); + if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active); + + if (tick_sprite != {}) { + ctx.push_rect(elem.bounds, parent.div.z_index, style)!; + if (*active) { + ctx.draw_sprite_raw(tick_sprite, elem.bounds, center: true)!; + } + } else { + ctx.push_rect(elem.bounds, parent.div.z_index, style)!; + if (*active) { + ushort x = style.size / 4; + Rect check = elem.bounds.add({x, x, -x*2, -x*2}); + Style s = *style; + s.bg = s.primary; + s.margin = s.padding = {}; + s.border = 0; + ctx.push_rect(check, parent.div.z_index, &s)!; + } + } +} + +// FIXME: this should be inside the style +macro Ctx.toggle(&ctx, String desc, Point off, bool* active) + => ctx.toggle_id(@compute_id($vasplat), desc, off, active); +fn void? Ctx.toggle_id(&ctx, Id id, String description, Point off, bool* active) +{ + id = ctx.gen_id(id)!; + + Elem *parent = ctx.get_parent()!; + Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!; + Style* style = ctx.styles.get_style(@str_hash("toggle")); + + Rect size = {off.x, off.y, style.size*2, style.size}; + elem.bounds = ctx.position_element(parent, size, style); + + // if the bounds are null the element is outside the div view, + // no interaction should occur so just return + if (elem.bounds.is_null()) return; + + elem.events = ctx.get_elem_events(elem); + if (elem.events.mouse_hover && elem.events.mouse_release) *active = !(*active); + + // Draw the button + // FIXME: THIS IS SHIT + ctx.push_rect(elem.bounds, parent.div.z_index, style)!; + Rect t = elem.bounds.add({*active ? (style.size+3) : +3, +3, -style.size-6, -6}); + Style s = *style; + s.bg = s.primary; + s.margin = s.padding = {}; + s.border = 0; + ctx.push_rect(t, parent.div.z_index, &s)!; +} diff --git a/lib/ugui.c3l/src/ugui_cmd.c3 b/lib/ugui.c3l/src/ugui_cmd.c3 new file mode 100644 index 0000000..0247e7a --- /dev/null +++ b/lib/ugui.c3l/src/ugui_cmd.c3 @@ -0,0 +1,209 @@ +module ugui; + +import std::ascii; +import std::io; + +// command type +enum CmdType { + CMD_RECT, + CMD_UPDATE_ATLAS, + CMD_SPRITE, + CMD_SCISSOR, +} + +// command to draw a rect +struct CmdRect { + Rect rect; + ushort thickness; + ushort radius; + Color color; +} + +struct CmdUpdateAtlas { + Id id; + char* raw_buffer; + short width, height, bpp; +} + +struct CmdSprite { + Id texture_id; + SpriteType type; + Rect rect; + Rect texture_rect; + Color hue; +} + +// if rect is zero Rect{0} then reset the scissor +struct CmdScissor { + Rect rect; +} + +// command structure +struct Cmd (Printable) { + CmdType type; + int z_index; + union { + CmdRect rect; + CmdUpdateAtlas update_atlas; + CmdSprite sprite; + CmdScissor scissor; + } +} + +fn int Cmd.compare_to(Cmd a, Cmd b) +{ + if (a.z_index == b.z_index) return 0; + return a.z_index > b.z_index ? 1 : -1; +} + +// implement the Printable interface +fn usz? Cmd.to_format(Cmd* cmd, Formatter *f) @dynamic +{ + return f.printf("Cmd{ type: %s, z_index: %d }", cmd.type, cmd.z_index); +} + +macro bool cull_rect(Rect rect, Rect clip = {0,0,short.max,short.max}) +{ + bool no_area = rect.w <= 0 || rect.h <= 0; + return no_area || !rect.collides(clip); +} + +// FIXME: this whole thing could be done at compile time, maybe +macro Ctx.push_cmd(&ctx, Cmd *cmd, int z_index) +{ + cmd.z_index = z_index; + Rect rect; + switch (cmd.type) { + case CMD_RECT: rect = cmd.rect.rect; + case CMD_SPRITE: rect = cmd.sprite.rect; + default: return ctx.cmd_queue.enqueue(cmd); + } + if (cull_rect(rect, ctx.div_scissor)) return; + return ctx.cmd_queue.enqueue(cmd); +} + +fn void? Ctx.push_scissor(&ctx, Rect rect, int z_index) +{ + Cmd sc = { + .type = CMD_SCISSOR, + .scissor.rect = rect.intersection(ctx.div_scissor), + }; + ctx.push_cmd(&sc, z_index)!; +} + +fn void? Ctx.push_rect(&ctx, Rect rect, int z_index, Style* style) +{ + Rect padding = style.padding; + ushort border = style.border; + ushort radius = style.radius; + Color bg = style.bg; + Color border_color = style.secondary; + + if (border != 0) { + Cmd cmd = { + .type = CMD_RECT, + .rect.rect = rect, + .rect.color = border_color, + .rect.radius = radius+border, + .rect.thickness = border, + }; + ctx.push_cmd(&cmd, z_index)!; + } + + Cmd cmd = { + .type = CMD_RECT, + .rect.rect = { + .x = rect.x + border + padding.x, + .y = rect.y + border + padding.y, + .h = rect.h - (border*2) - (padding.y+padding.h), + .w = rect.w - (border*2) - (padding.x+padding.w), + }, + .rect.color = bg, + .rect.radius = radius, + .rect.thickness = max(rect.w, rect.h)/2+1, + }; + if (cull_rect(cmd.rect.rect, ctx.div_scissor)) return; + ctx.push_cmd(&cmd, z_index)!; +} + +fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_index, Color hue = 0xffffffffu.to_rgba(), SpriteType type = SPRITE_NORMAL) +{ + Cmd cmd = { + .type = CMD_SPRITE, + .sprite.type = type, + .sprite.rect = bounds, + .sprite.texture_rect = texture, + .sprite.texture_id = texture_id, + .sprite.hue = hue, + }; + ctx.push_cmd(&cmd, z_index)!; +} + +fn void? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue) +{ + if (text.len == 0) { + return; + } + + ctx.push_scissor(bounds, z_index)!; + + short baseline = (short)ctx.font.ascender; + short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; + short line_gap = (short)ctx.font.linegap; + Id texture_id = ctx.font.id; // or ctx.font.atlas.id + Point orig = { + .x = bounds.x, + .y = bounds.y, + }; + + short line_len; + Codepoint cp; + usz off, x; + while (off < text.len && (cp = str_to_codepoint(text[off..], &x)) != 0) { + off += x; + Glyph* gp; + if (!ascii::is_cntrl((char)cp)) { + gp = ctx.font.get_glyph(cp)!; + Rect gb = { + .x = orig.x + line_len + gp.ox, + .y = orig.y + gp.oy + baseline, + .w = gp.w, + .h = gp.h, + }; + Rect gt = { + .x = gp.u, + .y = gp.v, + .w = gp.w, + .h = gp.h, + }; + // push the sprite only if it collides with the bounds + if (!cull_rect(gb, bounds)) ctx.push_sprite(gb, gt, texture_id, z_index, hue)!; + line_len += gp.adv; + } else if (cp == '\n'){ + orig.y += line_height + line_gap; + line_len = 0; + } else { + continue; + } + } + + // FIXME: we never get here if an error was thrown before + ctx.push_scissor({}, z_index)!; +} + +fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas) +{ + Cmd up = { + .type = CMD_UPDATE_ATLAS, + .update_atlas = { + .id = atlas.id, + .raw_buffer = atlas.buffer, + .width = atlas.width, + .height = atlas.height, + .bpp = (ushort)atlas.type.bpp(), + }, + }; + // update the atlases before everything else + ctx.push_cmd(&up, -1)!; +} + diff --git a/lib/ugui.c3l/src/ugui_core.c3 b/lib/ugui.c3l/src/ugui_core.c3 new file mode 100644 index 0000000..d25385b --- /dev/null +++ b/lib/ugui.c3l/src/ugui_core.c3 @@ -0,0 +1,330 @@ +module ugui; + +import vtree; +import cache; +import fifo; + +import std::io; +import std::core::string; +import std::core::mem::allocator; + + +// element ids are just long ints +alias Id = uint; + +enum ElemType { + ETYPE_NONE, + ETYPE_DIV, + ETYPE_BUTTON, + ETYPE_SLIDER, + ETYPE_TEXT, + ETYPE_SPRITE, +} + +bitstruct ElemFlags : uint { + bool updated : 0; + bool is_new : 1; +} + +bitstruct ElemEvents : uint { + bool key_press : 0; + bool key_release : 1; + bool key_hold : 2; + bool mouse_hover : 3; + bool mouse_press : 4; + bool mouse_release : 5; + bool mouse_hold : 6; + bool update : 7; + bool text_input : 8; +} + +// element structure +struct Elem { + Id id; + isz tree_idx; + ElemFlags flags; + ElemEvents events; + Rect bounds; + ElemType type; + union { + ElemDiv div; + ElemButton button; + ElemSlider slider; + ElemText text; + ElemSprite sprite; + } +} + + +// relationships between elements are stored in a tree, it stores just the ids +alias IdTree = vtree::VTree{Id}; + +// elements themselves are kept in a cache +const uint MAX_ELEMENTS = 256; +alias ElemCache = cache::Cache{Id, Elem, MAX_ELEMENTS}; + +alias CmdQueue = fifo::Fifo{Cmd}; + +faultdef INVALID_SIZE, EVENT_UNSUPPORTED, UNEXPECTED_ELEMENT, WRONG_ELEMENT_TYPE, WRONG_ID; + +const Rect DIV_FILL = { .x = 0, .y = 0, .w = 0, .h = 0 }; + +const uint STACK_STEP = 10; +const uint MAX_ELEMS = 128; +const uint MAX_CMDS = 2048; +const uint ROOT_ID = 1; +const uint TEXT_MAX = 64; + +struct Ctx { + IdTree tree; + ElemCache cache; + CmdQueue cmd_queue; + StyleMap styles; + // total size in pixels of the context + ushort width, height; + Font font; + SpriteAtlas sprite_atlas; + + bool has_focus; + struct input { + InputEvents events; + struct mouse { + Point pos, delta; + // mouse_down: bitmap of mouse buttons that are held + // mouse_updated: bitmap of mouse buttons that have been updated + // mouse_released = mouse_updated & ~mouse_down + // mouse_pressed = mouse_updated & mouse_down + MouseButtons down; + MouseButtons updated; + // scroll wheel + Point scroll; + } + struct keyboard { + char[TEXT_MAX] text; + usz text_len; + ModKeys modkeys; + } + } + + Id hover_id; + Id focus_id; + + Rect div_scissor; // the current div bounds used for scissor test + isz active_div; // tree node indicating the current active div +} + +// return a pointer to the parent of the current active div +fn Elem*? Ctx.get_parent(&ctx) +{ + Id parent_id = ctx.tree.get(ctx.active_div)!; + return ctx.cache.search(parent_id); +} + +macro @bits(#a) => $typeof(#a).sizeof*8; +macro Id.rotate_left(id, uint $n) => (id << $n) | (id >> (@bits(id) - $n)); +const uint GOLDEN_RATIO = 0x9E3779B9; + +// generate an id combining the hashes of the parent id and the label +// with the Cantor pairing function +fn Id? Ctx.gen_id(&ctx, Id id2) +{ + Id id1 = ctx.tree.get(ctx.active_div)!; + // Mix the two IDs non-linearly + Id mixed = id1 ^ id2.rotate_left(13); + mixed ^= id1.rotate_left(7); + mixed += GOLDEN_RATIO; + return mixed; +} + +// compute the id from arguments and the line of the call +macro Id @compute_id(...) +{ + Id id = (Id)$$LINE.hash() ^ (Id)@str_hash($$FILE); + $for var $i = 0; $i < $vacount; $i++: + id ^= (Id)$vaconst[$i].hash(); + $endfor + return id; +} + +// get or push an element from the cache, return a pointer to it +// resets all flags except is_new which is set accordingly +fn Elem*? Ctx.get_elem(&ctx, Id id, ElemType type) +{ + Elem empty_elem; + bool is_new; + Elem* elem; + elem = ctx.cache.get_or_insert(&empty_elem, id, &is_new)!; + elem.flags = (ElemFlags)0; + elem.flags.is_new = is_new; + elem.id = id; + if (is_new == false && elem.type != type) { + return WRONG_ELEMENT_TYPE?; + } else { + elem.type = type; + } + elem.tree_idx = ctx.tree.add(id, ctx.active_div)!; + return elem; +} + +// find an element, does not allocate a new one in cache +// THIS HAS TO BE A MACRO SINCE IT RETURNS A POINTER TO A TEMPORARY VALUE +macro Elem* Ctx.find_elem(&ctx, Id id) +{ + Elem*? elem; + elem = ctx.cache.search(id); + if (catch elem) { + return &&(Elem){}; + } + return elem; +} + +fn Elem*? Ctx.get_active_div(&ctx) +{ + Id id = ctx.tree.get(ctx.active_div)!; + return ctx.cache.search(id); +} + +fn void? Ctx.init(&ctx) +{ + ctx.tree.init(MAX_ELEMENTS)!; + defer catch { (void)ctx.tree.free(); } + + ctx.cache.init()!; + defer catch { (void)ctx.cache.free(); } + + ctx.cmd_queue.init(MAX_CMDS)!; + defer catch { (void)ctx.cmd_queue.free(); } + + ctx.styles.init(allocator::heap()); + ctx.styles.register_style(&DEFAULT_STYLE, @str_hash("default")); + defer catch { ctx.styles.free(); } + + ctx.active_div = 0; +} + +fn void Ctx.free(&ctx) +{ + (void)ctx.tree.free(); + (void)ctx.cache.free(); + (void)ctx.cmd_queue.free(); + (void)ctx.font.free(); + (void)ctx.sprite_atlas.free(); + (void)ctx.styles.free(); +} + +fn void? Ctx.frame_begin(&ctx) +{ + // 1. Reset the active div + // 2. Get the root element from the cache and update it + ctx.active_div = 0; + Elem* elem = ctx.get_elem(ROOT_ID, ETYPE_DIV)!; + ctx.active_div = elem.tree_idx; + // The root should have the updated flag only if the size of the window + // was changed between frasmes, this propagates an element size recalculation + // down the element tree + elem.flags.updated = ctx.input.events.resize; + // if the window has focus then the root element also has focus, no other + // computation needed, child elements need to check the mouse positon and + // other stuff + //elem.flags.has_focus = ctx.has_focus; + + elem.bounds = {0, 0, ctx.width, ctx.height}; + elem.div.layout = LAYOUT_ROW; + elem.div.z_index = 0; + elem.div.children_bounds = elem.bounds; + elem.div.scroll_x.enabled = false; + elem.div.scroll_y.enabled = false; + elem.div.pcb = {}; + elem.div.origin_c = {}; + elem.div.origin_r = {}; + + ctx.div_scissor = {0, 0, ctx.width, ctx.height}; + + // The root element does not push anything to the stack + // TODO: add a background color taken from a theme or config +} + +const int DEBUG = 1; + +fn void? Ctx.frame_end(&ctx) +{ + // FIXME: this is not guaranteed to be root. the user might forget to close a div or some other element + Elem* root = ctx.get_active_div()!; + root.div.layout = LAYOUT_ROW; + + // 1. clear the tree + ctx.tree.nuke(); + + // 2. clear input fields + ctx.input.events = (InputEvents)0; + ctx.input.keyboard.text_len = 0; + + // send atlas updates + if (ctx.font.should_update) { + ctx.push_update_atlas(&ctx.font.atlas)!; + ctx.font.should_update = false; + } + if (ctx.sprite_atlas.should_update) { + ctx.push_update_atlas(&ctx.sprite_atlas.atlas)!; + ctx.sprite_atlas.should_update = false; + } + +// debug +$if DEBUG == 1: + // draw mouse position + Cmd cmd = { + .type = CMD_RECT, + .z_index = int.max-1, // hopefully over everything else + .rect.rect = { + .x = ctx.input.mouse.pos.x - 2, + .y = ctx.input.mouse.pos.y - 2, + .w = 4, + .h = 4, + }, + .rect.color = 0xff00ffffu.to_rgba() + }; + ctx.cmd_queue.enqueue(&cmd)!; +$endif + + // sort the command buffer by the z-index + ctx.cmd_queue.sort()!; +} + +<* + * @ensure elem != null +*> +macro bool Ctx.is_hovered(&ctx, Elem *elem) +{ + return ctx.input.mouse.pos.in_rect(elem.bounds); +} + +macro bool Ctx.elem_focus(&ctx, Elem *elem) +{ + return ctx.focus_id == elem.id; +} + +// TODO: add other events +// FIXME: this does not work with touch +// FIXME: hacked together, please do better +fn ElemEvents Ctx.get_elem_events(&ctx, Elem *elem) +{ + bool hover = ctx.is_hovered(elem); + bool focus = ctx.focus_id == elem.id || (hover && ctx.is_mouse_pressed(BTN_LEFT)); + + if (ctx.is_mouse_pressed(BTN_ANY) && !hover){ + focus = false; + if (ctx.focus_id == elem.id) ctx.focus_id = 0; + } + + if (hover) { ctx.hover_id = elem.id; } + if (focus) { ctx.focus_id = elem.id; } + + ElemEvents ev = { + .mouse_hover = hover, + .mouse_press = hover && focus && ctx.is_mouse_pressed(BTN_ANY), + .mouse_release = hover && focus && ctx.is_mouse_released(BTN_ANY), + .mouse_hold = hover && focus && ctx.is_mouse_down(BTN_ANY), + .text_input = focus && (ctx.input.keyboard.text_len || ctx.input.keyboard.modkeys & KMOD_TXT), + }; + return ev; +} diff --git a/lib/ugui.c3l/src/ugui_div.c3 b/lib/ugui.c3l/src/ugui_div.c3 new file mode 100644 index 0000000..8b9e292 --- /dev/null +++ b/lib/ugui.c3l/src/ugui_div.c3 @@ -0,0 +1,153 @@ +module ugui; + +import std::io; +import std::math; + +// div element +struct ElemDiv { + Layout layout; + struct scroll_x { + bool enabled; + bool on; + float value; + } + struct scroll_y { + bool enabled; + bool on; + float value; + } + ushort scroll_size; + int z_index; + Rect children_bounds; // current frame children bounds + Rect pcb; // previous frame children bounds + Point origin_r, origin_c; +} + +// begin a widget container, or div, the size determines the offset (x,y) width and height. +// if the width or height are zero the width or height are set to the maximum available. +// if the width or height are negative the width or height will be calculated based on the children size +// sort similar to a flexbox, and the minimum size is set by the negative of the width or height +// FIXME: there is a bug if the size.w or size.h == -0 +macro Ctx.div_begin(&ctx, Rect size, bool scroll_x = false, bool scroll_y = false, ...) + => ctx.div_begin_id(@compute_id($vasplat), size, scroll_x, scroll_y); +fn void? Ctx.div_begin_id(&ctx, Id id, Rect size, bool scroll_x, bool scroll_y) +{ + id = ctx.gen_id(id)!; + + Elem* parent = ctx.get_parent()!; + Elem* elem = ctx.get_elem(id, ETYPE_DIV)!; + ctx.active_div = elem.tree_idx; + + Style* style = ctx.styles.get_style(@str_hash("default")); + Style* slider_style = ctx.styles.get_style(@str_hash("slider")); + + elem.div.scroll_x.enabled = scroll_x; + elem.div.scroll_y.enabled = scroll_y; + elem.div.scroll_size = slider_style.size ? slider_style.size : (style.size ? style.size : DEFAULT_STYLE.size); + elem.div.z_index = parent.div.z_index + 1; + + // 2. layout the element + Rect wanted_size = { + .x = size.x, + .y = size.y, + .w = size.w < 0 ? max(elem.div.pcb.w, (short)-size.w) : size.w, + .h = size.h < 0 ? max(elem.div.pcb.h, (short)-size.h) : size.h, + }; + elem.bounds = ctx.position_element(parent, wanted_size, style); + elem.div.children_bounds = {}; + + // update the ctx scissor + ctx.div_scissor = elem.bounds; + ctx.push_scissor(elem.bounds, elem.div.z_index)!; + + // 4. Fill the div fields + elem.div.origin_c = { + .x = elem.bounds.x, + .y = elem.bounds.y + }; + elem.div.origin_r = elem.div.origin_c; + elem.div.layout = parent.div.layout; + + // Add the background to the draw stack + bool do_border = parent.div.layout == LAYOUT_FLOATING; + ctx.push_rect(elem.bounds, elem.div.z_index, style)!; + + elem.events = ctx.get_elem_events(elem); + + // TODO: check active + // TODO: check resizeable +} + +fn void? Ctx.div_end(&ctx) +{ + // swap the children bounds + Elem* parent = ctx.get_parent()!; + Elem* elem = ctx.get_active_div()!; + elem.div.pcb = elem.div.children_bounds; + + // FIXME: this causes all elements inside the div to loose focus since the mouse press happens + // both inside the element and inside the div bounds + //elem.events = ctx.get_elem_events(elem); + + Rect cb = elem.div.pcb; + // children bounds bottom-right corner + Point cbc = { + .x = cb.x + cb.w, + .y = cb.y + cb.h, + }; + // div bounds bottom-right corner + Point bc = { + .x = elem.bounds.x + elem.bounds.w, + .y = elem.bounds.y + elem.bounds.h, + }; + + // set the scrollbar flag, is used in layout + // horizontal overflow + elem.div.scroll_x.on = cbc.x > bc.x && elem.div.scroll_x.enabled; + // vertical overflow + elem.div.scroll_y.on = cbc.y > bc.y && elem.div.scroll_y.enabled; + + Id hsid_raw = @str_hash("div_scrollbar_horizontal"); + Id vsid_raw = @str_hash("div_scrollbar_vertical"); + Id hsid_real = ctx.gen_id(@str_hash("div_scrollbar_horizontal"))!; + Id vsid_real = ctx.gen_id(@str_hash("div_scrollbar_vertical"))!; + short wdim = elem.div.scroll_y.on ? (ctx.focus_id == vsid_real || ctx.is_hovered(ctx.find_elem(vsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0; + short hdim = elem.div.scroll_x.on ? (ctx.focus_id == hsid_real || ctx.is_hovered(ctx.find_elem(hsid_real)) ? elem.div.scroll_size*2 : elem.div.scroll_size) : 0; + + if (elem.div.scroll_y.on) { + if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) { + elem.div.scroll_y.value += ctx.input.mouse.scroll.y * 0.07f; + elem.div.scroll_y.value = math::clamp(elem.div.scroll_y.value, 0.0f, 1.0f); + } + Rect vslider = { + .x = elem.bounds.x + elem.bounds.w - wdim, + .y = elem.bounds.y, + .w = wdim, + .h = elem.bounds.h - hdim, + }; + Layout prev_l = elem.div.layout; + elem.div.layout = LAYOUT_ABSOLUTE; + ctx.slider_ver_id(vsid_raw, vslider, &elem.div.scroll_y.value, max((float)bc.y / cbc.y, (float)0.15))!; + elem.div.layout = prev_l; + } + + if (elem.div.scroll_x.on) { + if (ctx.input.events.mouse_scroll && ctx.hover_id == elem.id) { + elem.div.scroll_x.value += ctx.input.mouse.scroll.x * 0.07f; + elem.div.scroll_x.value = math::clamp(elem.div.scroll_x.value, 0.0f, 1.0f); + } + Rect hslider = { + .x = elem.bounds.x, + .y = elem.bounds.y + elem.bounds.h - hdim, + .w = elem.bounds.w - wdim, + .h = hdim, + }; + Layout prev_l = elem.div.layout; + elem.div.layout = LAYOUT_ABSOLUTE; + ctx.slider_hor_id(hsid_raw, hslider, &elem.div.scroll_x.value, max((float)bc.x / cbc.x, (float)0.15))!; + elem.div.layout = prev_l; + } + + // the active_div returns to the parent of the current one + ctx.active_div = ctx.tree.parentof(ctx.active_div)!; +} diff --git a/src/font.c3 b/lib/ugui.c3l/src/ugui_font.c3 similarity index 58% rename from src/font.c3 rename to lib/ugui.c3l/src/ugui_font.c3 index 0bc233e..020ed75 100644 --- a/src/font.c3 +++ b/lib/ugui.c3l/src/ugui_font.c3 @@ -1,26 +1,19 @@ -module ugui::font; +module ugui; import schrift; +import grapheme; import std::collections::map; import std::core::mem; import std::core::mem::allocator; import std::io; -import std::core::ascii; +import std::ascii; -// ---------------------------------------------------------------------------------- // -// CODEPOINT // -// ---------------------------------------------------------------------------------- // - // unicode code point, different type for a different hash alias Codepoint = uint; //macro uint Codepoint.hash(self) => ((uint)self).hash(); -// ---------------------------------------------------------------------------------- // -// FONT ATLAS // -// ---------------------------------------------------------------------------------- // - /* width and height of a glyph contain the kering advance * (u,v) * +-------------*---+ - @@ -65,18 +58,9 @@ struct Font { bool should_update; // should send update_atlas command, resets at frame_end() } -macro Rect Glyph.bounds(&g) => {.x = (short)g.ox, .y = (short)g.oy, .w = (short)g.w, .h = (short)g.h}; -macro Rect Glyph.uv(&g) => {.x = (short)g.u, .y = (short)g.v, .w = (short)g.w, .h = (short)g.h}; - -<* -@param [&inout] font -@param [in] name -@param [&in] path -@require height > 0, scale > 0: "height and scale must be positive non-zero" -*> -fn void? Font.load(&font, Allocator allocator, String name, ZString path, uint height, float scale) +fn void? Font.load(&font, String name, ZString path, uint height, float scale) { - font.table.init(allocator, capacity: FONT_CACHED); + font.table.init(allocator::heap(), capacity: FONT_CACHED); font.id = name.hash(); font.size = height*scale; @@ -90,7 +74,7 @@ fn void? Font.load(&font, Allocator allocator, String name, ZString path, uint h font.sft.font = schrift::loadfile(path); if (font.sft.font == null) { font.table.free(); - return TTF_LOAD_FAILED~; + return TTF_LOAD_FAILED?; } schrift::SftLMetrics lmetrics; @@ -100,19 +84,17 @@ fn void? Font.load(&font, Allocator allocator, String name, ZString path, uint h font.linegap = (float)lmetrics.lineGap; //io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap); + // TODO: allocate buffer based on FONT_CACHED and the size of a sample letter + // like the letter 'A' ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED)); font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!; // preallocate the ASCII range for (char c = ' '; c < '~'; c++) { - // FIXME: without @inline, this crashes with O1 or greater - font.get_glyph((Codepoint)c) @inline!; + font.get_glyph((Codepoint)c)!; } } -<* -@param [&inout] font -*> fn Glyph*? Font.get_glyph(&font, Codepoint code) { Glyph*? gp; @@ -120,13 +102,12 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code) if (catch excuse = gp) { if (excuse != NOT_FOUND) { - return excuse~; + return excuse?; } } else { return gp; } - // missing glyph, render and place into an atlas Glyph glyph; @@ -134,21 +115,21 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code) schrift::SftGMetrics gmtx; if (schrift::lookup(&font.sft, (SftUChar)code, &gid) < 0) { - return MISSING_GLYPH~; + return MISSING_GLYPH?; } if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) { - return BAD_GLYPH_METRICS~; + return BAD_GLYPH_METRICS?; } schrift::SftImage img = { .width = gmtx.minWidth, .height = gmtx.minHeight, }; - char[] pixels = mem::new_array(char, (usz)img.width * (usz)img.height); + char[] pixels = mem::new_array(char, (usz)img.width * img.height); img.pixels = pixels; if (schrift::render(&font.sft, gid, img) < 0) { - return RENDER_ERROR~; + return RENDER_ERROR?; } glyph.code = code; @@ -161,8 +142,8 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code) // glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv); Point uv = font.atlas.place(pixels, glyph.w, glyph.h, (ushort)img.width)!; - glyph.u = (ushort)uv.x; - glyph.v = (ushort)uv.y; + glyph.u = uv.x; + glyph.v = uv.y; mem::free(pixels); @@ -172,9 +153,6 @@ fn Glyph*? Font.get_glyph(&font, Codepoint code) return font.table.get_ref(code); } -<* -@param [&inout] font -*> fn void Font.free(&font) { font.atlas.free(); @@ -182,44 +160,107 @@ fn void Font.free(&font) schrift::freefont(font.sft.font); } - -// ---------------------------------------------------------------------------------- // -// FONT LOAD AND QUERY // -// ---------------------------------------------------------------------------------- // - -module ugui; - -<* -@param [&inout] ctx -@param [in] name -@param [&in] path -@require height > 0, scale > 0: "height and scale must be positive non-zero" -*> -fn void? Ctx.load_font(&ctx, Allocator allocator, String name, ZString path, uint height, float scale = 1.0) +fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0) { - return ctx.font.load(allocator, name, path, height, scale); + return ctx.font.load(name, path, height, scale); } <* -@param [&in] ctx -@param [in] label -@require ctx.font.id != 0 : "font not loaded" +@require off != null *> -// TODO: check if the font is present in the context -fn Id Ctx.get_font_id(&ctx, String label) => (Id)ctx.font.id; +fn Codepoint str_to_codepoint(char[] str, usz* off) +{ + Codepoint cp; + isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp); + if (b == 0 || b > str.len) { + return 0; + } + *off = b; + return cp; +} + +fn Rect? Ctx.get_text_bounds(&ctx, String text) +{ + Rect text_bounds; + short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; + short line_gap = (short)ctx.font.linegap; + text_bounds.h = line_height; + Glyph* gp; + + // TODO: account for unicode codepoints + short line_len; + Codepoint cp; + usz off, x; + while ((cp = str_to_codepoint(text[off..], &x)) != 0) { + off += x; + bool n; + if (!ascii::is_cntrl((char)cp)) { + gp = ctx.font.get_glyph(cp)!; + line_len += gp.adv; + } else if (cp == '\n'){ + text_bounds.h += line_height + line_gap; + line_len = 0; + } else { + continue; + } + if (line_len > text_bounds.w) { + text_bounds.w = line_len; + } + } + + return text_bounds; +} + +fn Point? Ctx.get_cursor_position(&ctx, String text) +{ + short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; + short line_gap = (short)ctx.font.linegap; + Glyph* gp; + + // TODO: account for unicode codepoints + Point line; + Codepoint cp; + usz off, x; + while ((cp = str_to_codepoint(text[off..], &x)) != 0) { + off += x; + bool n; + if (!ascii::is_cntrl((char)cp)) { + gp = ctx.font.get_glyph(cp)!; + line.x += gp.adv; + } else if (cp == '\n'){ + line.y += line_height + line_gap; + line.x = 0; + } else { + continue; + } + } + + return line; +} + + +fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds) +{ + short dw = bounds.w - text_bounds.w; + short dh = bounds.h - text_bounds.h; + + return {.x = dw/2, .y = dh/2}; +} + +// TODO: check if the font is present in the context +fn Id Ctx.get_font_id(&ctx, String label) +{ + return (Id)label.hash(); +} -<* -@param [&in] ctx -@param [in] name -*> fn Atlas*? Ctx.get_font_atlas(&ctx, String name) { + // TODO: use the font name, for now there is only one font if (name.hash() != ctx.font.id) { - return WRONG_ID~; + return WRONG_ID?; } return &ctx.font.atlas; } -<* @param [&in] font *> fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5); diff --git a/lib/ugui.c3l/src/ugui_input.c3 b/lib/ugui.c3l/src/ugui_input.c3 new file mode 100644 index 0000000..ac70110 --- /dev/null +++ b/lib/ugui.c3l/src/ugui_input.c3 @@ -0,0 +1,204 @@ +module ugui; + +import grapheme; +import std::io; +import std::math; +import std::core::string; + +bitstruct InputEvents : uint { + bool resize : 0; // window size was changed + bool change_focus : 1; // window focus changed + bool mouse_move : 2; // mouse was moved + bool mouse_btn : 3; // mouse button pressed or released + bool mouse_scroll : 4; // mouse scroll wheel. x or y + bool text_input : 5; + bool mod_key : 6; +} + +bitstruct MouseButtons : uint { + bool btn_left : 0; + bool btn_middle : 1; + bool btn_right : 2; + bool btn_4 : 3; + bool btn_5 : 4; +} + +// FIXME: all of these names were prefixed with key_ idk if this is better, +// if it is remove the prefix on MouseButtons as well +// Modifier Keys, intended as any key that is not text +bitstruct ModKeys : uint { + bool lshift : 0; + bool rshift : 1; + bool lctrl : 2; + bool rctrl : 3; + bool lalt : 4; + bool ralt : 5; + bool lgui : 6; + bool rgui : 7; + bool num : 8; + bool caps : 9; + bool mode : 10; + bool scroll : 11; + bool bkspc : 12; + bool del : 13; + // arrow keys + bool up : 14; + bool down : 15; + bool left : 16; + bool right : 17; +} + +const ModKeys KMOD_CTRL = {.lctrl = true, .rctrl = true}; +const ModKeys KMOD_SHIFT = {.lshift = true, .rshift = true}; +const ModKeys KMOD_ALT = {.lalt = true, .ralt = true}; +const ModKeys KMOD_GUI = {.lgui = true, .rgui = true}; +const ModKeys KMOD_TXT = {.bkspc = true, .del = true}; // modkeys that act like text input +const ModKeys KMOD_NONE = {}; +const ModKeys KMOD_ANY = (ModKeys)(ModKeys.inner.max); + +const MouseButtons BTN_NONE = {}; +const MouseButtons BTN_ANY = (MouseButtons)(MouseButtons.inner.max); +const MouseButtons BTN_LEFT = {.btn_left = true}; +const MouseButtons BTN_MIDDLE = {.btn_middle = true}; +const MouseButtons BTN_RIGHT = {.btn_right = true}; +const MouseButtons BTN_4 = {.btn_4 = true}; +const MouseButtons BTN_5 = {.btn_5 = true}; + +const ModKeys KEY_ANY = (ModKeys)(ModKeys.inner.max); + +fn bool Ctx.check_key_combo(&ctx, ModKeys mod, String keys) +{ + bool is_mod = (bool)(ctx.input.keyboard.modkeys & mod); + bool is_keys = true; + String haystack = (String)ctx.input.keyboard.text[0..ctx.input.keyboard.text_len]; + char[2] needle; + foreach (c: keys) { + needle[0] = c; + is_keys = is_keys && haystack.contains((String)needle[..]); + } + return is_mod && is_keys; +} + +// Window size was changed +fn void? Ctx.input_window_size(&ctx, short width, short height) +{ + if (width <= 0 || height <= 0) { + return INVALID_SIZE?; + } + ctx.input.events.resize = ctx.width != width || ctx.height != height; + ctx.width = width; + ctx.height = height; +} + +// Window gained/lost focus +fn void Ctx.input_changefocus(&ctx, bool has_focus) +{ + // FIXME: raylib only has an API to query the focus status so we have to + // update the input flag only if the focus changed + ctx.input.events.change_focus = ctx.has_focus != has_focus; + ctx.has_focus = has_focus; +} + +macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down; +macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down; +macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down; + +// FIXME: hthis compairson could be done with a cast using MouseButtons.inner +// property but I could not figure out how +macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn) => (ctx.mouse_pressed() & btn) != BTN_NONE; +macro Ctx.is_mouse_released(&ctx, MouseButtons btn) => (ctx.mouse_released() & btn) != BTN_NONE; +macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE; + +// Mouse Buttons down +fn void Ctx.input_mouse_button(&ctx, MouseButtons buttons) +{ + ctx.input.mouse.updated = ctx.input.mouse.down ^ buttons; + ctx.input.mouse.down = buttons; + ctx.input.events.mouse_btn = (uint)ctx.input.mouse.down != 0 || (uint)ctx.input.mouse.updated != 0; +} + +// Mouse was moved, report absolute position +fn void Ctx.input_mouse_abs(&ctx, short x, short y) +{ + ctx.input.mouse.pos.x = math::clamp(x, (short)0, ctx.width); + ctx.input.mouse.pos.y = math::clamp(y, (short)0, ctx.height); + + short dx, dy; + dx = x - ctx.input.mouse.pos.x; + dy = y - ctx.input.mouse.pos.y; + + ctx.input.mouse.delta.x = dx; + ctx.input.mouse.delta.y = dy; + + ctx.input.events.mouse_move = dx != 0 || dy != 0; +} + +// Mouse was moved, report relative motion +fn void Ctx.input_mouse_delta(&ctx, short dx, short dy) +{ + ctx.input.mouse.delta.x = dx; + ctx.input.mouse.delta.y = dy; + + short mx, my; + mx = ctx.input.mouse.pos.x + dx; + my = ctx.input.mouse.pos.y + dy; + + ctx.input.mouse.pos.x = math::clamp(mx, (short)0, ctx.width); + ctx.input.mouse.pos.y = math::clamp(my, (short)0, ctx.height); + + ctx.input.events.mouse_move = dx != 0 || dy != 0; +} + +fn void Ctx.input_mouse_wheel(&ctx, short x, short y, float scale = 1.0) +{ + ctx.input.mouse.scroll.x = (short)((float)-x*scale); + ctx.input.mouse.scroll.y = (short)((float)-y*scale); + ctx.input.events.mouse_scroll = x !=0 || y != 0; +} + +// append utf-8 encoded text to the context text input +fn void Ctx.input_text_utf8(&ctx, char[] text) +{ + if (text.len == 0) { return; } + + usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len; + usz len = text.len > remaining ? remaining : text.len; + char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..]; + s[..len-1] = text[..len-1]; + ctx.input.keyboard.text_len += len; + ctx.input.events.text_input = true; +} + +fn void Ctx.input_text_unicode(&ctx, char[] text) +{ + if (text.ptr == null || text.len == 0) { return; } + + char[32] tmp; + usz remaining = ctx.input.keyboard.text.len - ctx.input.keyboard.text_len; + char[] s = ctx.input.keyboard.text[ctx.input.keyboard.text_len ..]; + + usz off; + foreach (idx, cp: text) { + if (off >= remaining) { break; } + usz enc = grapheme::encode_utf8(cp, tmp[..], tmp.len); + s[off..off+enc] = tmp[..enc]; + off += enc; + } + ctx.input.keyboard.text_len += off; + + ctx.input.events.text_input = true; +} + +fn void Ctx.input_char(&ctx, char c) +{ + char[1] b = {c}; + ctx.input_text_utf8(b[..]); +} + +// Modifier keys, like control or backspace +// TODO: make this call repetible to input modkeys one by one +fn void Ctx.input_mod_keys(&ctx, ModKeys modkeys) +{ + ctx.input.keyboard.modkeys = modkeys; + ctx.input.events.mod_key = (uint)ctx.input.keyboard.modkeys != 0; +} diff --git a/lib/ugui.c3l/src/ugui_layout.c3 b/lib/ugui.c3l/src/ugui_layout.c3 new file mode 100644 index 0000000..84eaf8b --- /dev/null +++ b/lib/ugui.c3l/src/ugui_layout.c3 @@ -0,0 +1,196 @@ +module ugui; + +enum Layout { + LAYOUT_ROW, + LAYOUT_COLUMN, + LAYOUT_FLOATING, + LAYOUT_ABSOLUTE, +} + +fn void? Ctx.layout_set_row(&ctx) +{ + Id parent_id = ctx.tree.get(ctx.active_div)!; + Elem *parent = ctx.cache.search(parent_id)!; + + if (parent.type != ETYPE_DIV) { + // what? + return UNEXPECTED_ELEMENT?; + } + + parent.div.layout = LAYOUT_ROW; +} + +fn void? Ctx.layout_set_column(&ctx) +{ + Id parent_id = ctx.tree.get(ctx.active_div)!; + Elem *parent = ctx.cache.search(parent_id)!; + + if (parent.type != ETYPE_DIV) { + // what? + return UNEXPECTED_ELEMENT?; + } + + parent.div.layout = LAYOUT_COLUMN; +} + +fn void? Ctx.layout_set_floating(&ctx) +{ + Id parent_id = ctx.tree.get(ctx.active_div)!; + Elem *parent = ctx.cache.search(parent_id)!; + + if (parent.type != ETYPE_DIV) { + // what? + return UNEXPECTED_ELEMENT?; + } + + parent.div.layout = LAYOUT_FLOATING; +} + +fn void? Ctx.layout_next_row(&ctx) +{ + Id parent_id = ctx.tree.get(ctx.active_div)!; + Elem *parent = ctx.cache.search(parent_id)!; + + if (parent.type != ETYPE_DIV) { + return UNEXPECTED_ELEMENT?; + } + + parent.div.origin_r = { + .x = parent.bounds.x, + .y = parent.div.children_bounds.bottom_right().y, + }; + parent.div.origin_c = parent.div.origin_r; +} + +fn void? Ctx.layout_next_column(&ctx) +{ + Id parent_id = ctx.tree.get(ctx.active_div)!; + Elem *parent = ctx.cache.search(parent_id)!; + + if (parent.type != ETYPE_DIV) { + return UNEXPECTED_ELEMENT?; + } + + parent.div.origin_c = { + .x = parent.div.children_bounds.bottom_right().x, + .y = parent.bounds.y, + }; + parent.div.origin_r = parent.div.origin_c; +} + + +macro Rect Elem.get_view(&elem) +{ + Rect off; + if (elem.div.scroll_x.enabled && elem.div.scroll_x.on) { + off.x = (short)((float)(elem.div.pcb.w - elem.bounds.w) * elem.div.scroll_x.value); + off.w = -elem.div.scroll_size; + } + if (elem.div.scroll_y.enabled && elem.div.scroll_y.on) { + off.y = (short)((float)(elem.div.pcb.h - elem.bounds.h) * elem.div.scroll_y.value); + off.h = -elem.div.scroll_size; + } + return elem.bounds.add(off); +} + +macro Point Elem.get_view_off(&elem) +{ + return elem.get_view().sub(elem.bounds).position(); +} + +// position the rectangle inside the parent according to the layout +// parent: parent div +// rect: the requested size +// style: apply style +<* +@require ctx != null +@require parent.type == ETYPE_DIV +*> +fn Rect Ctx.position_element(&ctx, Elem *parent, Rect rect, Style* style) +{ + ElemDiv* div = &parent.div; + + Rect parent_bounds, parent_view; + Rect child_placement, child_occupied; + + // 1. Select the right origin + Point origin; + switch (div.layout) { + case LAYOUT_ROW: + origin = div.origin_r; + case LAYOUT_COLUMN: + origin = div.origin_c; + case LAYOUT_FLOATING: // none, relative to zero zero + case LAYOUT_ABSOLUTE: // absolute position, this is a no-op, return the rect + return rect; + default: // error + return {}; + } + + // 2. Compute the parent's view + parent_bounds = parent.bounds; + parent_view = parent.get_view(); + + // 3. Compute the placement and occupied area + + // grow rect (wanted size) when widht or height are less than zero + bool adapt_x = rect.w <= 0; + bool adapt_y = rect.h <= 0; + if (adapt_x) rect.w = parent_bounds.w - parent_bounds.x - origin.x; + if (adapt_y) rect.h = parent_bounds.h - parent_bounds.y - origin.y; + + // offset placement and area + child_placement = child_placement.off(origin.add(rect.position())); + child_occupied = child_occupied.off(origin.add(rect.position())); + + Rect margin = style.margin; + Rect padding = style.padding; + ushort border = style.border; + + // padding, grows both the placement and occupied area + child_placement = child_placement.grow(padding.position().add(padding.size())); + child_occupied = child_occupied.grow(padding.position().add(padding.size())); + // border, grows both the placement and occupied area + child_placement = child_placement.grow({border*2, border*2}); + child_occupied = child_occupied.grow({border*2, border*2}); + // margin, offsets the placement and grows the occupied area + child_placement = child_placement.off(margin.position()); + child_occupied = child_occupied.grow(margin.position().add(margin.size())); + + // oh yeah also adjust the rect if i was to grow + if (adapt_x) rect.w -= padding.x+padding.w + border*2 + margin.x+margin.w; + if (adapt_y) rect.h -= padding.y+padding.h + border*2 + margin.y+margin.h; + + // set the size + child_placement = child_placement.grow(rect.size()); + child_occupied = child_occupied.grow(rect.size()); + + // 4. Update the parent's origin + div.origin_r = { + .x = child_occupied.bottom_right().x, + .y = origin.y, + }; + div.origin_c = { + .x = origin.x, + .y = child_occupied.bottom_right().y, + }; + + // 5. Update the parent's children bounds + if (!child_occupied.bottom_right().in_rect(div.children_bounds)) { + // right overflow + if (child_occupied.bottom_right().x > div.children_bounds.bottom_right().x) { + div.children_bounds.w += child_occupied.bottom_right().x - div.children_bounds.bottom_right().x; + } + // bottom overflow + if (child_occupied.bottom_right().y > div.children_bounds.bottom_right().y) { + div.children_bounds.h += child_occupied.bottom_right().y - div.children_bounds.bottom_right().y; + } + } + + // 99. return the placement + if (child_placement.collides(parent_view)) { + return child_placement.off(parent.get_view_off().neg()); + } else { + return {}; + } +} diff --git a/src/shapes.c3 b/lib/ugui.c3l/src/ugui_shapes.c3 similarity index 56% rename from src/shapes.c3 rename to lib/ugui.c3l/src/ugui_shapes.c3 index 96d9710..9e778b7 100644 --- a/src/shapes.c3 +++ b/lib/ugui.c3l/src/ugui_shapes.c3 @@ -1,5 +1,5 @@ module ugui; -import std::math; + // ---------------------------------------------------------------------------------- // // RECTANGLE // @@ -10,9 +10,6 @@ struct Rect { short x, y, w, h; } -// TODO: find another name -const Rect RECT_MAX = {0, 0, short::max, short::max}; - // return true if rect a contains b macro bool Rect.contains(Rect a, Rect b) { @@ -36,26 +33,11 @@ macro bool Rect.collides(Rect a, Rect b) return !(a.x > b.x+b.w || a.x+a.w < b.x || a.y > b.y+b.h || a.y+a.h < b.y); } -// return a rect that contains both rects, a bounding box of both -macro Rect containing_rect(Rect a, Rect b) -{ - short min_x = (short)min(a.x, b.x); - short min_y = (short)min(a.y, b.y); - short max_x = (short)max(a.x + a.w, b.x + b.w); - short max_y = (short)max(a.y + a.h, b.y + b.h); - return { - .x = min_x, - .y = min_y, - .w = (short)(max_x - min_x), - .h = (short)(max_y - min_y) - }; -} - // check for empty rect macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0; // returns the element-wise addition of r1 and r2 -macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+) +macro Rect Rect.add(Rect r1, Rect r2) { return { .x = r1.x + r2.x, @@ -66,7 +48,7 @@ macro Rect Rect.add(Rect r1, Rect r2) @operator_s(+) } // returns the element-wise subtraction of r1 and r2 -macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-) +macro Rect Rect.sub(Rect r1, Rect r2) { return { .x = r1.x - r2.x, @@ -77,7 +59,7 @@ macro Rect Rect.sub(Rect r1, Rect r2) @operator_s(-) } // returns the element-wise multiplication of r1 and r2 -macro Rect Rect.mul(Rect r1, Rect r2) @operator_s(*) +macro Rect Rect.mul(Rect r1, Rect r2) { return { .x = r1.x * r2.x, @@ -124,7 +106,7 @@ macro Rect Rect.min(Rect a, Rect b) } // Offset a rect by a point -macro Rect Rect.off(Rect r, Point p) @operator_s(+) +macro Rect Rect.off(Rect r, Point p) { return { .x = r.x + p.x, @@ -154,35 +136,6 @@ macro Point Rect.bottom_right(Rect r) }; } -macro Rect Rect.center_to(Rect a, Rect b) -{ - return { - .x = b.x + (b.w - a.w)/2, - .y = b.y + (b.h - a.h)/2, - .w = a.w, - .h = a.h, - }; -} - -macro Rect Rect.pad(Rect a, Rect b) -{ - return { - .x = a.x + b.x, - .y = a.y + b.y, - .w = a.w - b.x - b.w, - .h = a.h - b.y - b.h, - }; -} - -macro Rect Rect.expand(Rect a, Rect b) -{ - return { - .x = a.x - b.x, - .y = a.y - b.y, - .w = a.w + b.x + b.w, - .h = a.h + b.y + b.h, - }; -} // ---------------------------------------------------------------------------------- // // POINT // @@ -198,16 +151,39 @@ macro bool Point.in_rect(Point p, Rect r) return (p.x >= r.x && p.x <= r.x + r.w) && (p.y >= r.y && p.y <= r.y + r.h); } -macro bool Point.outside(Point p, Rect r) => !p.in_rect(r); +macro Point Point.add(Point a, Point b) +{ + return { + .x = a.x + b.x, + .y = a.y + b.y, + }; +} -macro Point Point.add(Point a, Point b) @operator_s(+) => {.x = a.x+b.x, .y = a.y+b.y}; -macro Point Point.sub(Point a, Point b) @operator_s(-) => {.x = a.x-b.x, .y = a.y-b.y}; -macro Point Point.neg(Point p) @operator_s(-) => {-p.x, -p.y}; +macro Point Point.sub(Point a, Point b) +{ + return { + .x = a.x - b.x, + .y = a.y - b.y, + }; +} -macro Point Point.max(Point a, Point b) => {.x = max(a.x, b.x), .y = max(a.y, b.y)}; -macro Point Point.min(Point a, Point b) => {.x = min(a.x, b.x), .y = min(a.y, b.y)}; +macro Point Point.neg(Point p) => {-p.x, -p.y}; -macro bool Point.equals(Point a, Point b) @operator_s(==) => a.x == b.x && a.y == b.y; +macro Point Point.max(Point a, Point b) +{ + return { + .x = max(a.x, b.x), + .y = max(a.y, b.y), + }; +} + +macro Point Point.min(Point a, Point b) +{ + return { + .x = min(a.x, b.x), + .y = min(a.y, b.y), + }; +} // ---------------------------------------------------------------------------------- // // COLOR // @@ -238,29 +214,8 @@ macro Color uint.@to_rgba($u) }; } -macro uint Color.to_uint(c) => c.r | (c.g << 8) | (c.b << 16) | (c.a << 24); - -// ---------------------------------------------------------------------------------- // -// SIZE // -// ---------------------------------------------------------------------------------- // - -struct Size { - short min, max; +macro uint Color.to_uint(c) +{ + uint u = c.r | (c.g << 8) | (c.b << 16) | (c.a << 24); + return u; } - -macro Size @grow() => {.min = 0, .max = 0}; -macro Size @exact(short s) => {.min = s, .max = s}; -macro Size @fit(short min = 0, short max = short::max) => {.min = min, .max = max}; - -macro bool Size.@is_grow(s) => (s.min == 0 && s.max == 0); -macro bool Size.@is_exact(s) => (s.min == s.max && s.min != 0); -macro bool Size.@is_fit(s) => (s.min != s.max); - -macro Size Size.add(a, Size b) @operator_s(+) => {.min = a.min.sat_add(b.min), .max = a.max.sat_add(b.max)}; -macro Size Size.sub(a, Size b) @operator_s(-) => {.min = a.min.sat_sub(b.min), .max = a.max.sat_sub(b.max)}; - -macro Size Size.combine(a, Size b) => {.min = max(a.min, b.min), .max = min(a.max, b.max)}; -macro Size Size.comb_max(a, Size b) => {.min = max(a.min, b.min), .max = max(a.max, b.max)}; -macro Size Size.comb_min(a, Size b) => {.min = min(a.min, b.min), .max = min(a.max, b.max)}; - -macro short Size.greater(a) => a.min > a.max ? a.min : a.max; diff --git a/lib/ugui.c3l/src/ugui_slider.c3 b/lib/ugui.c3l/src/ugui_slider.c3 new file mode 100644 index 0000000..a631dee --- /dev/null +++ b/lib/ugui.c3l/src/ugui_slider.c3 @@ -0,0 +1,132 @@ +module ugui; + +import std::io; +import std::math; + +// slider element +struct ElemSlider { + Rect handle; +} + + +/* handle + * +----+-----+---------------------+ + * | |#####| | + * +----+-----+---------------------+ + */ +macro Ctx.slider_hor(&ctx, Rect size, float* value, float hpercent = 0.25, ...) + => ctx.slider_hor_id(@compute_id($vasplat), size, value, hpercent); +<* +@require value != null +*> +fn ElemEvents? Ctx.slider_hor_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25) +{ + id = ctx.gen_id(id)!; + + Elem* parent = ctx.get_parent()!; + Elem* elem = ctx.get_elem(id, ETYPE_SLIDER)!; + Style* style = ctx.styles.get_style(@str_hash("slider")); + + // 2. Layout + elem.bounds = ctx.position_element(parent, size, style); + + // handle width + short hw = (short)(elem.bounds.w * hpercent); + Rect handle = { + .x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value), + .y = elem.bounds.y, + .w = hw, + .h = elem.bounds.h, + }; + elem.slider.handle = handle; + + Point m = ctx.input.mouse.pos; + elem.events = ctx.get_elem_events(elem); + + if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) { + *value = calc_value(elem.bounds.x, m.x, elem.bounds.w, hw); + elem.slider.handle.x = calc_slider(elem.bounds.x, elem.bounds.w-hw, *value); + elem.events.update = true; + } + + // Draw the slider background and handle + Style s = *style; + Rect padding = s.padding; + s.padding = {}; + ctx.push_rect(elem.bounds, parent.div.z_index, &s)!; + s.bg = s.primary; + s.padding = padding; + s.border = {}; + ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!; + + return elem.events; +} + + +/* + * +--+ + * | | + * | | + * +--+ + * |##| handle + * |##| + * +--+ + * | | + * | | + * +--+ + */ +macro Ctx.slider_ver(&ctx, Rect size, float* value, float hpercent = 0.25, ...) + => ctx.slider_ver_id(@compute_id($vasplat), size, value, hpercent); +fn ElemEvents? Ctx.slider_ver_id(&ctx, Id id, Rect size, float* value, float hpercent = 0.25) +{ + id = ctx.gen_id(id)!; + + Elem *parent = ctx.get_parent()!; + Elem *elem = ctx.get_elem(id, ETYPE_SLIDER)!; + Style* style = ctx.styles.get_style(@str_hash("slider")); + + // 1. Fill the element fields + if (elem.flags.is_new) { + elem.type = ETYPE_SLIDER; + } else if (elem.type != ETYPE_SLIDER) { + return WRONG_ELEMENT_TYPE?; + } + + // 2. Layout + elem.bounds = ctx.position_element(parent, size, style); + + // handle height + short hh = (short)(elem.bounds.h * hpercent); + Rect handle = { + .x = elem.bounds.x, + .y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value), + .w = elem.bounds.w, + .h = hh, + }; + elem.slider.handle = handle; + + Point m = ctx.input.mouse.pos; + elem.events = ctx.get_elem_events(elem); + + if (ctx.elem_focus(elem) && ctx.is_mouse_down(BTN_LEFT)) { + *value = calc_value(elem.bounds.y, m.y, elem.bounds.h, hh); + elem.slider.handle.y = calc_slider(elem.bounds.y, elem.bounds.h-hh, *value); + elem.events.update = true; + } + + // Draw the slider background and handle + Style s = *style; + Rect padding = s.padding; + s.padding = {}; + ctx.push_rect(elem.bounds, parent.div.z_index, &s)!; + s.bg = s.primary; + s.padding = padding; + s.border = {}; + ctx.push_rect(elem.slider.handle, parent.div.z_index, &s)!; + + return elem.events; +} + +macro short calc_slider(short off, short dim, float value) => (short)off + (short)(dim * value); +macro float calc_value(short off, short mouse, short dim, short slider) + => math::clamp((float)(mouse-off-slider/2)/(float)(dim-slider), 0.0f, 1.0f); diff --git a/src/sprite.c3 b/lib/ugui.c3l/src/ugui_sprite.c3 similarity index 58% rename from src/sprite.c3 rename to lib/ugui.c3l/src/ugui_sprite.c3 index 2b01e65..724262d 100644 --- a/src/sprite.c3 +++ b/lib/ugui.c3l/src/ugui_sprite.c3 @@ -30,17 +30,21 @@ struct SpriteAtlas { bool should_update; } +struct ElemSprite { + Id id; +} + // name: some examples are "icons" or "images" fn void? SpriteAtlas.init(&this, String name, AtlasType type, ushort width, ushort height) { // FIXME: for now only R8G8B8A8 format is supported if (type != ATLAS_R8G8B8A8) { - return INVALID_TYPE~; + return INVALID_TYPE?; } this.id = name.hash(); this.atlas.new(this.id, AtlasType.ATLAS_R8G8B8A8, width, height)!; - this.sprites.init(mem, capacity: SRITES_PER_ATLAS); + this.sprites.init(allocator::heap(), capacity: SRITES_PER_ATLAS); this.should_update = false; } @@ -60,8 +64,8 @@ fn Sprite*? SpriteAtlas.insert(&this, String name, SpriteType type, char[] pixel Point uv = this.atlas.place(pixels, w, h, stride)!; s.w = w; s.h = h; - s.u = (ushort)uv.x; - s.v = (ushort)uv.y; + s.u = uv.x; + s.v = uv.y; this.sprites.set(s.id, s); this.should_update = true; return this.sprites.get_ref(s.id); @@ -78,8 +82,8 @@ fn Sprite*? SpriteAtlas.get_by_id(&this, Id id) return this.sprites.get_ref(id); } -macro Rect Sprite.rect(s) => {0, 0, (short)s.w, (short)s.h}; -macro Rect Sprite.uv(s) => {(short)s.u, (short)s.v, (short)s.w, (short)s.h}; +macro Rect Sprite.rect(s) => {0,0,s.w,s.h}; +macro Rect Sprite.uv(s) => {s.u,s.v,s.w,s.h}; fn void? Ctx.sprite_atlas_create(&ctx, String name, AtlasType type, ushort w, ushort h) { @@ -100,8 +104,50 @@ fn void? Ctx.import_sprite_file_qoi(&ctx, String name, String path, SpriteType t { QOIDesc desc; - char[] pixels = qoi::read(mem, path, &desc, QOIChannels.RGBA)!; + char[] pixels = qoi::read(allocator::heap(), path, &desc, QOIChannels.RGBA)!; defer mem::free(pixels); ctx.sprite_atlas.insert(name, type, pixels, (ushort)desc.width, (ushort)desc.height, (ushort)desc.width)!; } + + +macro Ctx.sprite(&ctx, String name, Point off = {0,0}, ...) + => ctx.sprite_id(@compute_id($vasplat), name, off); +fn void? Ctx.sprite_id(&ctx, Id id, String name, Point off) +{ + id = ctx.gen_id(id)!; + + Elem *parent = ctx.get_parent()!; + Elem *elem = ctx.get_elem(id, ETYPE_SPRITE)!; + + Style* style = ctx.styles.get_style(@str_hash("sprite")); + Sprite* sprite = ctx.sprite_atlas.get(name)!; + + Rect uv = { sprite.u, sprite.v, sprite.w, sprite.h }; + Rect bounds = { 0, 0, sprite.w, sprite.h }; + + elem.bounds = ctx.position_element(parent, bounds.off(off), style); + elem.sprite.id = ctx.get_sprite_atlas_id(name); + + // if the bounds are null the element is outside the div view, + // no interaction should occur so just return + if (elem.bounds.is_null()) return; + + Id tex_id = ctx.sprite_atlas.id; + + return ctx.push_sprite(elem.bounds, uv, tex_id, parent.div.z_index)!; +} + +fn void? Ctx.draw_sprite_raw(&ctx, String name, Rect bounds, bool center = false) +{ + Elem *parent = ctx.get_parent()!; + Sprite* sprite = ctx.sprite_atlas.get(name)!; + Id tex_id = ctx.sprite_atlas.id; + + if (center) { + Point off = {.x = (bounds.w - sprite.w) / 2, .y = (bounds.h - sprite.h) / 2}; + bounds = bounds.off(off); + } + + return ctx.push_sprite(bounds, sprite.uv(), tex_id, parent.div.z_index, type: sprite.type)!; +} diff --git a/src/style.c3 b/lib/ugui.c3l/src/ugui_style.c3 similarity index 71% rename from src/style.c3 rename to lib/ugui.c3l/src/ugui_style.c3 index 5957403..aa5d20b 100644 --- a/src/style.c3 +++ b/lib/ugui.c3l/src/ugui_style.c3 @@ -7,8 +7,10 @@ import std::io; // global style, similar to the css box model struct Style { // css box model Rect padding; - Rect border; Rect margin; + ushort border; + ushort radius; + ushort size; Color bg; // background color Color fg; // foreground color @@ -16,22 +18,20 @@ struct Style { // css box model Color secondary; // secondary color Color accent; // accent color - ushort radius; - short size; } const Style DEFAULT_STYLE = { .margin = {2, 2, 2, 2}, - .border = {2, 2, 2, 2}, - .padding = {1, 1, 1, 1}, - .radius = 0, + .padding = {}, + .border = 2, + .radius = 12, .size = 16, - - .bg = 0x282828ffu.@to_rgba(), - .fg = 0xfbf1c7ffu.@to_rgba(), - .primary = 0xcc241dffu.@to_rgba(), + + .bg = 0x282828ffu.@to_rgba(), + .fg = 0xfbf1c7ffu.@to_rgba(), + .primary = 0xcc241dffu.@to_rgba(), .secondary = 0x458588ffu.@to_rgba(), - .accent = 0xfabd2fffu.@to_rgba(), + .accent = 0xfabd2fffu.@to_rgba(), }; // style is stored in a hashmap, each style has an Id that can be generated by a string or whatever @@ -60,7 +60,7 @@ fn int StyleMap.import_style_string(&map, String text) Parser p; p.lex.text = text; int added; - + while (p.parse_style() == true) { added++; // set the default style correctly @@ -75,11 +75,11 @@ fn int Ctx.import_style_from_string(&ctx, String text) => ctx.styles.import_styl fn int Ctx.import_style_from_file(&ctx, String path) { char[] text; - sz size = file::get_size(path)!!; + usz size = file::get_size(path)!!; text = mem::new_array(char, size); file::load_buffer(path, text)!!; defer mem::free(text); - + int added = ctx.import_style_from_string((String)text); return added; } @@ -89,8 +89,8 @@ fn int Ctx.import_style_from_file(&ctx, String path) * Style can be serialized and deserialized with a subset of CSS *