Compare commits
	
		
			39 Commits
		
	
	
		
			6ec78cf752
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8859504fed | |||
| 237208d972 | |||
| 42a9de3ba3 | |||
| 6f45a3bc70 | |||
| f61710010d | |||
| d368760f78 | |||
| a67b787386 | |||
| 86ecd128f8 | |||
| 06418b4cf4 | |||
| 81584c643e | |||
| aaf5dbb3b7 | |||
| 9f7365cbb6 | |||
| 58c5c2c6c4 | |||
| 92dbad27ee | |||
| d5af4c9baf | |||
| c4e13985ed | |||
| 86024e2c03 | |||
| e43799d11b | |||
| aeeae20aff | |||
| 465de1d0ea | |||
| f728d61f23 | |||
| 0629283aa5 | |||
| bd82b7a25c | |||
| cabf8b23df | |||
| acf480832a | |||
| 7834724e53 | |||
| 18614ccee9 | |||
| 30b558f649 | |||
| 804c420744 | |||
| 52fe62c3b1 | |||
| 47ffde7996 | |||
| 2b8bc31fc7 | |||
| 64979c6e5c | |||
| 278f4c6df3 | |||
| c377baf3c9 | |||
| 596f24becd | |||
| 4cbdc572a9 | |||
| 9268380fd9 | |||
| 4d8940812a | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,5 @@ | ||||
| [Bb]uild* | ||||
| CMakeLists.txt.* | ||||
| result | ||||
| .cache | ||||
| .direnv | ||||
|   | ||||
| @@ -1,10 +1,25 @@ | ||||
| cmake_minimum_required(VERSION 3.16) | ||||
|  | ||||
| project(waylight LANGUAGES C CXX) | ||||
|  | ||||
| set(CMAKE_CXX_STANDARD 23) | ||||
| set(CMAKE_CXX_STANDARD_REQUIRED ON) | ||||
| set(CMAKE_POSITION_INDEPENDENT_CODE ON) | ||||
|  | ||||
| find_program(cppcheck_exe NAMES cppcheck) | ||||
| if (cppcheck_exe) | ||||
| 	set(cppcheck_opts --enable=all --inline-suppr --quiet --suppressions-list=${PROJECT_SOURCE_DIR}/cppcheck.supp) | ||||
| 	add_custom_target(run_cppcheck | ||||
| 		COMMAND ${cppcheck_exe} | ||||
| 			--std=c++20 --enable=all --inline-suppr --quiet | ||||
| 			--suppressions-list=${PROJECT_SOURCE_DIR}/cppcheck.supp | ||||
| 			--project=${CMAKE_BINARY_DIR}/compile_commands.json | ||||
| 			--check-level=exhaustive | ||||
| 			-i ${CMAKE_BINARY_DIR} | ||||
| 		WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} | ||||
| 	) | ||||
| endif() | ||||
|  | ||||
| find_package(PkgConfig REQUIRED) | ||||
| pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client) | ||||
| pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl) | ||||
| @@ -34,6 +49,7 @@ FetchContent_MakeAvailable(raylib) | ||||
| FetchContent_Declare( | ||||
| 	msdfgen | ||||
| 	GIT_REPOSITORY https://github.com/Chlumsky/msdfgen.git | ||||
| 	GIT_TAG "v1.12.1" | ||||
| 	GIT_SHALLOW 1 | ||||
| ) | ||||
| set(MSDFGEN_BUILD_STANDALONE OFF) | ||||
| @@ -43,6 +59,48 @@ set(MSDFGEN_DISABLE_SVG ON) | ||||
| set(MSDFGEN_DISABLE_PNG ON) | ||||
| FetchContent_MakeAvailable(msdfgen) | ||||
|  | ||||
| FetchContent_Declare( | ||||
| 	mINI | ||||
| 	GIT_REPOSITORY https://github.com/metayeti/mINI.git | ||||
| 	GIT_TAG "0.9.18" | ||||
| 	GIT_SHALLOW 1 | ||||
| ) | ||||
| FetchContent_MakeAvailable(mINI) | ||||
|  | ||||
| FetchContent_Declare( | ||||
| 	lunasvg | ||||
| 	GIT_REPOSITORY https://github.com/sammycage/lunasvg.git | ||||
| 	GIT_TAG "v3.5.0" | ||||
| 	GIT_SHALLOW 1 | ||||
| ) | ||||
| FetchContent_MakeAvailable(lunasvg) | ||||
|  | ||||
| FetchContent_Declare( | ||||
| 	SQLiteCpp | ||||
| 	GIT_REPOSITORY https://github.com/SRombauts/SQLiteCpp.git | ||||
| 	GIT_TAG "3.3.3" | ||||
| 	GIT_SHALLOW 1 | ||||
| ) | ||||
| FetchContent_MakeAvailable(SQLiteCpp) | ||||
|  | ||||
| FetchContent_Declare( | ||||
| 	cpptrace | ||||
| 	GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git | ||||
| 	GIT_TAG "v1.0.1" | ||||
| 	GIT_SHALLOW 1 | ||||
| ) | ||||
| FetchContent_MakeAvailable(cpptrace) | ||||
|  | ||||
| FetchContent_Declare( | ||||
| 	tomlplusplus | ||||
| 	GIT_REPOSITORY https://github.com/marzer/tomlplusplus | ||||
| 	GIT_TAG "v3.4.0" | ||||
| 	GIT_SHALLOW 1 | ||||
| ) | ||||
| FetchContent_MakeAvailable(tomlplusplus) | ||||
|  | ||||
| add_subdirectory(vendor) | ||||
|  | ||||
| find_program(WAYLAND_SCANNER wayland-scanner REQUIRED) | ||||
|  | ||||
| pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) | ||||
| @@ -92,17 +150,23 @@ set(WLR_LAYER_SHELL_XML | ||||
| 	"${WLR_PROTOCOLS_DIR}/unstable/wlr-layer-shell-unstable-v1.xml" | ||||
| ) | ||||
|  | ||||
| set(TEXT_INPUT_XML | ||||
| 	"${WAYLAND_PROTOCOLS_DIR}/unstable/text-input/text-input-unstable-v3.xml" | ||||
| ) | ||||
|  | ||||
| set(GEN_C_HEADERS | ||||
| 	"${GEN_DIR}/xdg-shell-client-protocol.h" | ||||
| 	"${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h" | ||||
| 	"${GEN_DIR}/ext-background-effect-v1-client-protocol.h" | ||||
| 	"${GEN_DIR}/blur-client-protocol.h" | ||||
| 	"${GEN_DIR}/text-input-unstable-v3-client-protocol.h" | ||||
| ) | ||||
| set(GEN_C_PRIVATES | ||||
| 	"${GEN_DIR}/xdg-shell-protocol.c" | ||||
| 	"${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c" | ||||
| 	"${GEN_DIR}/ext-background-effect-v1-protocol.c" | ||||
| 	"${GEN_DIR}/blur-protocol.c" | ||||
| 	"${GEN_DIR}/text-input-unstable-v3-protocol.c" | ||||
| ) | ||||
|  | ||||
| add_custom_command( | ||||
| @@ -115,10 +179,13 @@ add_custom_command( | ||||
| 	# wlr-layer-shell | ||||
| 	COMMAND "${WAYLAND_SCANNER}" client-header "${WLR_LAYER_SHELL_XML}" "${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h" | ||||
| 	COMMAND "${WAYLAND_SCANNER}" private-code  "${WLR_LAYER_SHELL_XML}" "${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c" | ||||
| 	# text-input-unstable-v3 | ||||
| 	COMMAND "${WAYLAND_SCANNER}" client-header "${TEXT_INPUT_XML}" "${GEN_DIR}/text-input-unstable-v3-client-protocol.h" | ||||
| 	COMMAND "${WAYLAND_SCANNER}" private-code  "${TEXT_INPUT_XML}" "${GEN_DIR}/text-input-unstable-v3-protocol.c" | ||||
| 	# org-kde-win-blur | ||||
| 	COMMAND "${WAYLAND_SCANNER}" client-header "${CMAKE_CURRENT_SOURCE_DIR}/blur.xml" "${GEN_DIR}/blur-client-protocol.h" | ||||
| 	COMMAND "${WAYLAND_SCANNER}" private-code  "${CMAKE_CURRENT_SOURCE_DIR}/blur.xml" "${GEN_DIR}/blur-protocol.c" | ||||
| 	DEPENDS "${XDG_SHELL_XML}" "${WLR_LAYER_SHELL_XML}" | ||||
| 	DEPENDS "${XDG_SHELL_XML}" "${WLR_LAYER_SHELL_XML}" "${TEXT_INPUT_XML}" | ||||
| 	COMMENT "Generating Wayland + wlr-layer-shell client headers and private code" | ||||
| 	VERBATIM | ||||
| ) | ||||
| @@ -130,8 +197,13 @@ add_custom_target(generate_protocols ALL | ||||
| add_executable(waylight | ||||
| 	${GEN_C_PRIVATES} | ||||
|  | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/App.cpp | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/Config.cpp | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/IconRegistry.cpp | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/Cache.cpp | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/InotifyWatcher.cpp | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/TextRenderer.cpp | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/ImGui.cpp | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/App.cpp | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/Tick.cpp | ||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp | ||||
| ) | ||||
| @@ -152,8 +224,15 @@ target_link_libraries(waylight PRIVATE | ||||
| 	PkgConfig::FONTCONFIG | ||||
| 	PkgConfig::HARFBUZZ | ||||
|  | ||||
| 	tomlplusplus::tomlplusplus | ||||
| 	cpptrace::cpptrace | ||||
| 	tinyfiledialogs | ||||
| 	mINI | ||||
| 	raylib | ||||
| 	msdfgen::msdfgen-core | ||||
| 	msdfgen::msdfgen-ext | ||||
| 	lunasvg::lunasvg | ||||
| 	SQLiteCpp | ||||
|  | ||||
| 	m | ||||
| 	dl | ||||
|   | ||||
							
								
								
									
										674
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										674
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,674 @@ | ||||
|                     GNU GENERAL PUBLIC LICENSE | ||||
|                        Version 3, 29 June 2007 | ||||
|  | ||||
|  Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | ||||
|  Everyone is permitted to copy and distribute verbatim copies | ||||
|  of this license document, but changing it is not allowed. | ||||
|  | ||||
|                             Preamble | ||||
|  | ||||
|   The GNU General Public License is a free, copyleft license for | ||||
| software and other kinds of works. | ||||
|  | ||||
|   The licenses for most software and other practical works are designed | ||||
| to take away your freedom to share and change the works.  By contrast, | ||||
| the GNU General Public License is intended to guarantee your freedom to | ||||
| share and change all versions of a program--to make sure it remains free | ||||
| software for all its users.  We, the Free Software Foundation, use the | ||||
| GNU General Public License for most of our software; it applies also to | ||||
| any other work released this way by its authors.  You can apply it to | ||||
| your programs, too. | ||||
|  | ||||
|   When we speak of free software, we are referring to freedom, not | ||||
| price.  Our General Public Licenses are designed to make sure that you | ||||
| have the freedom to distribute copies of free software (and charge for | ||||
| them if you wish), that you receive source code or can get it if you | ||||
| want it, that you can change the software or use pieces of it in new | ||||
| free programs, and that you know you can do these things. | ||||
|  | ||||
|   To protect your rights, we need to prevent others from denying you | ||||
| these rights or asking you to surrender the rights.  Therefore, you have | ||||
| certain responsibilities if you distribute copies of the software, or if | ||||
| you modify it: responsibilities to respect the freedom of others. | ||||
|  | ||||
|   For example, if you distribute copies of such a program, whether | ||||
| gratis or for a fee, you must pass on to the recipients the same | ||||
| freedoms that you received.  You must make sure that they, too, receive | ||||
| or can get the source code.  And you must show them these terms so they | ||||
| know their rights. | ||||
|  | ||||
|   Developers that use the GNU GPL protect your rights with two steps: | ||||
| (1) assert copyright on the software, and (2) offer you this License | ||||
| giving you legal permission to copy, distribute and/or modify it. | ||||
|  | ||||
|   For the developers' and authors' protection, the GPL clearly explains | ||||
| that there is no warranty for this free software.  For both users' and | ||||
| authors' sake, the GPL requires that modified versions be marked as | ||||
| changed, so that their problems will not be attributed erroneously to | ||||
| authors of previous versions. | ||||
|  | ||||
|   Some devices are designed to deny users access to install or run | ||||
| modified versions of the software inside them, although the manufacturer | ||||
| can do so.  This is fundamentally incompatible with the aim of | ||||
| protecting users' freedom to change the software.  The systematic | ||||
| pattern of such abuse occurs in the area of products for individuals to | ||||
| use, which is precisely where it is most unacceptable.  Therefore, we | ||||
| have designed this version of the GPL to prohibit the practice for those | ||||
| products.  If such problems arise substantially in other domains, we | ||||
| stand ready to extend this provision to those domains in future versions | ||||
| of the GPL, as needed to protect the freedom of users. | ||||
|  | ||||
|   Finally, every program is threatened constantly by software patents. | ||||
| States should not allow patents to restrict development and use of | ||||
| software on general-purpose computers, but in those that do, we wish to | ||||
| avoid the special danger that patents applied to a free program could | ||||
| make it effectively proprietary.  To prevent this, the GPL assures that | ||||
| patents cannot be used to render the program non-free. | ||||
|  | ||||
|   The precise terms and conditions for copying, distribution and | ||||
| modification follow. | ||||
|  | ||||
|                        TERMS AND CONDITIONS | ||||
|  | ||||
|   0. Definitions. | ||||
|  | ||||
|   "This License" refers to version 3 of the GNU General Public License. | ||||
|  | ||||
|   "Copyright" also means copyright-like laws that apply to other kinds of | ||||
| works, such as semiconductor masks. | ||||
|  | ||||
|   "The Program" refers to any copyrightable work licensed under this | ||||
| License.  Each licensee is addressed as "you".  "Licensees" and | ||||
| "recipients" may be individuals or organizations. | ||||
|  | ||||
|   To "modify" a work means to copy from or adapt all or part of the work | ||||
| in a fashion requiring copyright permission, other than the making of an | ||||
| exact copy.  The resulting work is called a "modified version" of the | ||||
| earlier work or a work "based on" the earlier work. | ||||
|  | ||||
|   A "covered work" means either the unmodified Program or a work based | ||||
| on the Program. | ||||
|  | ||||
|   To "propagate" a work means to do anything with it that, without | ||||
| permission, would make you directly or secondarily liable for | ||||
| infringement under applicable copyright law, except executing it on a | ||||
| computer or modifying a private copy.  Propagation includes copying, | ||||
| distribution (with or without modification), making available to the | ||||
| public, and in some countries other activities as well. | ||||
|  | ||||
|   To "convey" a work means any kind of propagation that enables other | ||||
| parties to make or receive copies.  Mere interaction with a user through | ||||
| a computer network, with no transfer of a copy, is not conveying. | ||||
|  | ||||
|   An interactive user interface displays "Appropriate Legal Notices" | ||||
| to the extent that it includes a convenient and prominently visible | ||||
| feature that (1) displays an appropriate copyright notice, and (2) | ||||
| tells the user that there is no warranty for the work (except to the | ||||
| extent that warranties are provided), that licensees may convey the | ||||
| work under this License, and how to view a copy of this License.  If | ||||
| the interface presents a list of user commands or options, such as a | ||||
| menu, a prominent item in the list meets this criterion. | ||||
|  | ||||
|   1. Source Code. | ||||
|  | ||||
|   The "source code" for a work means the preferred form of the work | ||||
| for making modifications to it.  "Object code" means any non-source | ||||
| form of a work. | ||||
|  | ||||
|   A "Standard Interface" means an interface that either is an official | ||||
| standard defined by a recognized standards body, or, in the case of | ||||
| interfaces specified for a particular programming language, one that | ||||
| is widely used among developers working in that language. | ||||
|  | ||||
|   The "System Libraries" of an executable work include anything, other | ||||
| than the work as a whole, that (a) is included in the normal form of | ||||
| packaging a Major Component, but which is not part of that Major | ||||
| Component, and (b) serves only to enable use of the work with that | ||||
| Major Component, or to implement a Standard Interface for which an | ||||
| implementation is available to the public in source code form.  A | ||||
| "Major Component", in this context, means a major essential component | ||||
| (kernel, window system, and so on) of the specific operating system | ||||
| (if any) on which the executable work runs, or a compiler used to | ||||
| produce the work, or an object code interpreter used to run it. | ||||
|  | ||||
|   The "Corresponding Source" for a work in object code form means all | ||||
| the source code needed to generate, install, and (for an executable | ||||
| work) run the object code and to modify the work, including scripts to | ||||
| control those activities.  However, it does not include the work's | ||||
| System Libraries, or general-purpose tools or generally available free | ||||
| programs which are used unmodified in performing those activities but | ||||
| which are not part of the work.  For example, Corresponding Source | ||||
| includes interface definition files associated with source files for | ||||
| the work, and the source code for shared libraries and dynamically | ||||
| linked subprograms that the work is specifically designed to require, | ||||
| such as by intimate data communication or control flow between those | ||||
| subprograms and other parts of the work. | ||||
|  | ||||
|   The Corresponding Source need not include anything that users | ||||
| can regenerate automatically from other parts of the Corresponding | ||||
| Source. | ||||
|  | ||||
|   The Corresponding Source for a work in source code form is that | ||||
| same work. | ||||
|  | ||||
|   2. Basic Permissions. | ||||
|  | ||||
|   All rights granted under this License are granted for the term of | ||||
| copyright on the Program, and are irrevocable provided the stated | ||||
| conditions are met.  This License explicitly affirms your unlimited | ||||
| permission to run the unmodified Program.  The output from running a | ||||
| covered work is covered by this License only if the output, given its | ||||
| content, constitutes a covered work.  This License acknowledges your | ||||
| rights of fair use or other equivalent, as provided by copyright law. | ||||
|  | ||||
|   You may make, run and propagate covered works that you do not | ||||
| convey, without conditions so long as your license otherwise remains | ||||
| in force.  You may convey covered works to others for the sole purpose | ||||
| of having them make modifications exclusively for you, or provide you | ||||
| with facilities for running those works, provided that you comply with | ||||
| the terms of this License in conveying all material for which you do | ||||
| not control copyright.  Those thus making or running the covered works | ||||
| for you must do so exclusively on your behalf, under your direction | ||||
| and control, on terms that prohibit them from making any copies of | ||||
| your copyrighted material outside their relationship with you. | ||||
|  | ||||
|   Conveying under any other circumstances is permitted solely under | ||||
| the conditions stated below.  Sublicensing is not allowed; section 10 | ||||
| makes it unnecessary. | ||||
|  | ||||
|   3. Protecting Users' Legal Rights From Anti-Circumvention Law. | ||||
|  | ||||
|   No covered work shall be deemed part of an effective technological | ||||
| measure under any applicable law fulfilling obligations under article | ||||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or | ||||
| similar laws prohibiting or restricting circumvention of such | ||||
| measures. | ||||
|  | ||||
|   When you convey a covered work, you waive any legal power to forbid | ||||
| circumvention of technological measures to the extent such circumvention | ||||
| is effected by exercising rights under this License with respect to | ||||
| the covered work, and you disclaim any intention to limit operation or | ||||
| modification of the work as a means of enforcing, against the work's | ||||
| users, your or third parties' legal rights to forbid circumvention of | ||||
| technological measures. | ||||
|  | ||||
|   4. Conveying Verbatim Copies. | ||||
|  | ||||
|   You may convey verbatim copies of the Program's source code as you | ||||
| receive it, in any medium, provided that you conspicuously and | ||||
| appropriately publish on each copy an appropriate copyright notice; | ||||
| keep intact all notices stating that this License and any | ||||
| non-permissive terms added in accord with section 7 apply to the code; | ||||
| keep intact all notices of the absence of any warranty; and give all | ||||
| recipients a copy of this License along with the Program. | ||||
|  | ||||
|   You may charge any price or no price for each copy that you convey, | ||||
| and you may offer support or warranty protection for a fee. | ||||
|  | ||||
|   5. Conveying Modified Source Versions. | ||||
|  | ||||
|   You may convey a work based on the Program, or the modifications to | ||||
| produce it from the Program, in the form of source code under the | ||||
| terms of section 4, provided that you also meet all of these conditions: | ||||
|  | ||||
|     a) The work must carry prominent notices stating that you modified | ||||
|     it, and giving a relevant date. | ||||
|  | ||||
|     b) The work must carry prominent notices stating that it is | ||||
|     released under this License and any conditions added under section | ||||
|     7.  This requirement modifies the requirement in section 4 to | ||||
|     "keep intact all notices". | ||||
|  | ||||
|     c) You must license the entire work, as a whole, under this | ||||
|     License to anyone who comes into possession of a copy.  This | ||||
|     License will therefore apply, along with any applicable section 7 | ||||
|     additional terms, to the whole of the work, and all its parts, | ||||
|     regardless of how they are packaged.  This License gives no | ||||
|     permission to license the work in any other way, but it does not | ||||
|     invalidate such permission if you have separately received it. | ||||
|  | ||||
|     d) If the work has interactive user interfaces, each must display | ||||
|     Appropriate Legal Notices; however, if the Program has interactive | ||||
|     interfaces that do not display Appropriate Legal Notices, your | ||||
|     work need not make them do so. | ||||
|  | ||||
|   A compilation of a covered work with other separate and independent | ||||
| works, which are not by their nature extensions of the covered work, | ||||
| and which are not combined with it such as to form a larger program, | ||||
| in or on a volume of a storage or distribution medium, is called an | ||||
| "aggregate" if the compilation and its resulting copyright are not | ||||
| used to limit the access or legal rights of the compilation's users | ||||
| beyond what the individual works permit.  Inclusion of a covered work | ||||
| in an aggregate does not cause this License to apply to the other | ||||
| parts of the aggregate. | ||||
|  | ||||
|   6. Conveying Non-Source Forms. | ||||
|  | ||||
|   You may convey a covered work in object code form under the terms | ||||
| of sections 4 and 5, provided that you also convey the | ||||
| machine-readable Corresponding Source under the terms of this License, | ||||
| in one of these ways: | ||||
|  | ||||
|     a) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by the | ||||
|     Corresponding Source fixed on a durable physical medium | ||||
|     customarily used for software interchange. | ||||
|  | ||||
|     b) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by a | ||||
|     written offer, valid for at least three years and valid for as | ||||
|     long as you offer spare parts or customer support for that product | ||||
|     model, to give anyone who possesses the object code either (1) a | ||||
|     copy of the Corresponding Source for all the software in the | ||||
|     product that is covered by this License, on a durable physical | ||||
|     medium customarily used for software interchange, for a price no | ||||
|     more than your reasonable cost of physically performing this | ||||
|     conveying of source, or (2) access to copy the | ||||
|     Corresponding Source from a network server at no charge. | ||||
|  | ||||
|     c) Convey individual copies of the object code with a copy of the | ||||
|     written offer to provide the Corresponding Source.  This | ||||
|     alternative is allowed only occasionally and noncommercially, and | ||||
|     only if you received the object code with such an offer, in accord | ||||
|     with subsection 6b. | ||||
|  | ||||
|     d) Convey the object code by offering access from a designated | ||||
|     place (gratis or for a charge), and offer equivalent access to the | ||||
|     Corresponding Source in the same way through the same place at no | ||||
|     further charge.  You need not require recipients to copy the | ||||
|     Corresponding Source along with the object code.  If the place to | ||||
|     copy the object code is a network server, the Corresponding Source | ||||
|     may be on a different server (operated by you or a third party) | ||||
|     that supports equivalent copying facilities, provided you maintain | ||||
|     clear directions next to the object code saying where to find the | ||||
|     Corresponding Source.  Regardless of what server hosts the | ||||
|     Corresponding Source, you remain obligated to ensure that it is | ||||
|     available for as long as needed to satisfy these requirements. | ||||
|  | ||||
|     e) Convey the object code using peer-to-peer transmission, provided | ||||
|     you inform other peers where the object code and Corresponding | ||||
|     Source of the work are being offered to the general public at no | ||||
|     charge under subsection 6d. | ||||
|  | ||||
|   A separable portion of the object code, whose source code is excluded | ||||
| from the Corresponding Source as a System Library, need not be | ||||
| included in conveying the object code work. | ||||
|  | ||||
|   A "User Product" is either (1) a "consumer product", which means any | ||||
| tangible personal property which is normally used for personal, family, | ||||
| or household purposes, or (2) anything designed or sold for incorporation | ||||
| into a dwelling.  In determining whether a product is a consumer product, | ||||
| doubtful cases shall be resolved in favor of coverage.  For a particular | ||||
| product received by a particular user, "normally used" refers to a | ||||
| typical or common use of that class of product, regardless of the status | ||||
| of the particular user or of the way in which the particular user | ||||
| actually uses, or expects or is expected to use, the product.  A product | ||||
| is a consumer product regardless of whether the product has substantial | ||||
| commercial, industrial or non-consumer uses, unless such uses represent | ||||
| the only significant mode of use of the product. | ||||
|  | ||||
|   "Installation Information" for a User Product means any methods, | ||||
| procedures, authorization keys, or other information required to install | ||||
| and execute modified versions of a covered work in that User Product from | ||||
| a modified version of its Corresponding Source.  The information must | ||||
| suffice to ensure that the continued functioning of the modified object | ||||
| code is in no case prevented or interfered with solely because | ||||
| modification has been made. | ||||
|  | ||||
|   If you convey an object code work under this section in, or with, or | ||||
| specifically for use in, a User Product, and the conveying occurs as | ||||
| part of a transaction in which the right of possession and use of the | ||||
| User Product is transferred to the recipient in perpetuity or for a | ||||
| fixed term (regardless of how the transaction is characterized), the | ||||
| Corresponding Source conveyed under this section must be accompanied | ||||
| by the Installation Information.  But this requirement does not apply | ||||
| if neither you nor any third party retains the ability to install | ||||
| modified object code on the User Product (for example, the work has | ||||
| been installed in ROM). | ||||
|  | ||||
|   The requirement to provide Installation Information does not include a | ||||
| requirement to continue to provide support service, warranty, or updates | ||||
| for a work that has been modified or installed by the recipient, or for | ||||
| the User Product in which it has been modified or installed.  Access to a | ||||
| network may be denied when the modification itself materially and | ||||
| adversely affects the operation of the network or violates the rules and | ||||
| protocols for communication across the network. | ||||
|  | ||||
|   Corresponding Source conveyed, and Installation Information provided, | ||||
| in accord with this section must be in a format that is publicly | ||||
| documented (and with an implementation available to the public in | ||||
| source code form), and must require no special password or key for | ||||
| unpacking, reading or copying. | ||||
|  | ||||
|   7. Additional Terms. | ||||
|  | ||||
|   "Additional permissions" are terms that supplement the terms of this | ||||
| License by making exceptions from one or more of its conditions. | ||||
| Additional permissions that are applicable to the entire Program shall | ||||
| be treated as though they were included in this License, to the extent | ||||
| that they are valid under applicable law.  If additional permissions | ||||
| apply only to part of the Program, that part may be used separately | ||||
| under those permissions, but the entire Program remains governed by | ||||
| this License without regard to the additional permissions. | ||||
|  | ||||
|   When you convey a copy of a covered work, you may at your option | ||||
| remove any additional permissions from that copy, or from any part of | ||||
| it.  (Additional permissions may be written to require their own | ||||
| removal in certain cases when you modify the work.)  You may place | ||||
| additional permissions on material, added by you to a covered work, | ||||
| for which you have or can give appropriate copyright permission. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, for material you | ||||
| add to a covered work, you may (if authorized by the copyright holders of | ||||
| that material) supplement the terms of this License with terms: | ||||
|  | ||||
|     a) Disclaiming warranty or limiting liability differently from the | ||||
|     terms of sections 15 and 16 of this License; or | ||||
|  | ||||
|     b) Requiring preservation of specified reasonable legal notices or | ||||
|     author attributions in that material or in the Appropriate Legal | ||||
|     Notices displayed by works containing it; or | ||||
|  | ||||
|     c) Prohibiting misrepresentation of the origin of that material, or | ||||
|     requiring that modified versions of such material be marked in | ||||
|     reasonable ways as different from the original version; or | ||||
|  | ||||
|     d) Limiting the use for publicity purposes of names of licensors or | ||||
|     authors of the material; or | ||||
|  | ||||
|     e) Declining to grant rights under trademark law for use of some | ||||
|     trade names, trademarks, or service marks; or | ||||
|  | ||||
|     f) Requiring indemnification of licensors and authors of that | ||||
|     material by anyone who conveys the material (or modified versions of | ||||
|     it) with contractual assumptions of liability to the recipient, for | ||||
|     any liability that these contractual assumptions directly impose on | ||||
|     those licensors and authors. | ||||
|  | ||||
|   All other non-permissive additional terms are considered "further | ||||
| restrictions" within the meaning of section 10.  If the Program as you | ||||
| received it, or any part of it, contains a notice stating that it is | ||||
| governed by this License along with a term that is a further | ||||
| restriction, you may remove that term.  If a license document contains | ||||
| a further restriction but permits relicensing or conveying under this | ||||
| License, you may add to a covered work material governed by the terms | ||||
| of that license document, provided that the further restriction does | ||||
| not survive such relicensing or conveying. | ||||
|  | ||||
|   If you add terms to a covered work in accord with this section, you | ||||
| must place, in the relevant source files, a statement of the | ||||
| additional terms that apply to those files, or a notice indicating | ||||
| where to find the applicable terms. | ||||
|  | ||||
|   Additional terms, permissive or non-permissive, may be stated in the | ||||
| form of a separately written license, or stated as exceptions; | ||||
| the above requirements apply either way. | ||||
|  | ||||
|   8. Termination. | ||||
|  | ||||
|   You may not propagate or modify a covered work except as expressly | ||||
| provided under this License.  Any attempt otherwise to propagate or | ||||
| modify it is void, and will automatically terminate your rights under | ||||
| this License (including any patent licenses granted under the third | ||||
| paragraph of section 11). | ||||
|  | ||||
|   However, if you cease all violation of this License, then your | ||||
| license from a particular copyright holder is reinstated (a) | ||||
| provisionally, unless and until the copyright holder explicitly and | ||||
| finally terminates your license, and (b) permanently, if the copyright | ||||
| holder fails to notify you of the violation by some reasonable means | ||||
| prior to 60 days after the cessation. | ||||
|  | ||||
|   Moreover, your license from a particular copyright holder is | ||||
| reinstated permanently if the copyright holder notifies you of the | ||||
| violation by some reasonable means, this is the first time you have | ||||
| received notice of violation of this License (for any work) from that | ||||
| copyright holder, and you cure the violation prior to 30 days after | ||||
| your receipt of the notice. | ||||
|  | ||||
|   Termination of your rights under this section does not terminate the | ||||
| licenses of parties who have received copies or rights from you under | ||||
| this License.  If your rights have been terminated and not permanently | ||||
| reinstated, you do not qualify to receive new licenses for the same | ||||
| material under section 10. | ||||
|  | ||||
|   9. Acceptance Not Required for Having Copies. | ||||
|  | ||||
|   You are not required to accept this License in order to receive or | ||||
| run a copy of the Program.  Ancillary propagation of a covered work | ||||
| occurring solely as a consequence of using peer-to-peer transmission | ||||
| to receive a copy likewise does not require acceptance.  However, | ||||
| nothing other than this License grants you permission to propagate or | ||||
| modify any covered work.  These actions infringe copyright if you do | ||||
| not accept this License.  Therefore, by modifying or propagating a | ||||
| covered work, you indicate your acceptance of this License to do so. | ||||
|  | ||||
|   10. Automatic Licensing of Downstream Recipients. | ||||
|  | ||||
|   Each time you convey a covered work, the recipient automatically | ||||
| receives a license from the original licensors, to run, modify and | ||||
| propagate that work, subject to this License.  You are not responsible | ||||
| for enforcing compliance by third parties with this License. | ||||
|  | ||||
|   An "entity transaction" is a transaction transferring control of an | ||||
| organization, or substantially all assets of one, or subdividing an | ||||
| organization, or merging organizations.  If propagation of a covered | ||||
| work results from an entity transaction, each party to that | ||||
| transaction who receives a copy of the work also receives whatever | ||||
| licenses to the work the party's predecessor in interest had or could | ||||
| give under the previous paragraph, plus a right to possession of the | ||||
| Corresponding Source of the work from the predecessor in interest, if | ||||
| the predecessor has it or can get it with reasonable efforts. | ||||
|  | ||||
|   You may not impose any further restrictions on the exercise of the | ||||
| rights granted or affirmed under this License.  For example, you may | ||||
| not impose a license fee, royalty, or other charge for exercise of | ||||
| rights granted under this License, and you may not initiate litigation | ||||
| (including a cross-claim or counterclaim in a lawsuit) alleging that | ||||
| any patent claim is infringed by making, using, selling, offering for | ||||
| sale, or importing the Program or any portion of it. | ||||
|  | ||||
|   11. Patents. | ||||
|  | ||||
|   A "contributor" is a copyright holder who authorizes use under this | ||||
| License of the Program or a work on which the Program is based.  The | ||||
| work thus licensed is called the contributor's "contributor version". | ||||
|  | ||||
|   A contributor's "essential patent claims" are all patent claims | ||||
| owned or controlled by the contributor, whether already acquired or | ||||
| hereafter acquired, that would be infringed by some manner, permitted | ||||
| by this License, of making, using, or selling its contributor version, | ||||
| but do not include claims that would be infringed only as a | ||||
| consequence of further modification of the contributor version.  For | ||||
| purposes of this definition, "control" includes the right to grant | ||||
| patent sublicenses in a manner consistent with the requirements of | ||||
| this License. | ||||
|  | ||||
|   Each contributor grants you a non-exclusive, worldwide, royalty-free | ||||
| patent license under the contributor's essential patent claims, to | ||||
| make, use, sell, offer for sale, import and otherwise run, modify and | ||||
| propagate the contents of its contributor version. | ||||
|  | ||||
|   In the following three paragraphs, a "patent license" is any express | ||||
| agreement or commitment, however denominated, not to enforce a patent | ||||
| (such as an express permission to practice a patent or covenant not to | ||||
| sue for patent infringement).  To "grant" such a patent license to a | ||||
| party means to make such an agreement or commitment not to enforce a | ||||
| patent against the party. | ||||
|  | ||||
|   If you convey a covered work, knowingly relying on a patent license, | ||||
| and the Corresponding Source of the work is not available for anyone | ||||
| to copy, free of charge and under the terms of this License, through a | ||||
| publicly available network server or other readily accessible means, | ||||
| then you must either (1) cause the Corresponding Source to be so | ||||
| available, or (2) arrange to deprive yourself of the benefit of the | ||||
| patent license for this particular work, or (3) arrange, in a manner | ||||
| consistent with the requirements of this License, to extend the patent | ||||
| license to downstream recipients.  "Knowingly relying" means you have | ||||
| actual knowledge that, but for the patent license, your conveying the | ||||
| covered work in a country, or your recipient's use of the covered work | ||||
| in a country, would infringe one or more identifiable patents in that | ||||
| country that you have reason to believe are valid. | ||||
|  | ||||
|   If, pursuant to or in connection with a single transaction or | ||||
| arrangement, you convey, or propagate by procuring conveyance of, a | ||||
| covered work, and grant a patent license to some of the parties | ||||
| receiving the covered work authorizing them to use, propagate, modify | ||||
| or convey a specific copy of the covered work, then the patent license | ||||
| you grant is automatically extended to all recipients of the covered | ||||
| work and works based on it. | ||||
|  | ||||
|   A patent license is "discriminatory" if it does not include within | ||||
| the scope of its coverage, prohibits the exercise of, or is | ||||
| conditioned on the non-exercise of one or more of the rights that are | ||||
| specifically granted under this License.  You may not convey a covered | ||||
| work if you are a party to an arrangement with a third party that is | ||||
| in the business of distributing software, under which you make payment | ||||
| to the third party based on the extent of your activity of conveying | ||||
| the work, and under which the third party grants, to any of the | ||||
| parties who would receive the covered work from you, a discriminatory | ||||
| patent license (a) in connection with copies of the covered work | ||||
| conveyed by you (or copies made from those copies), or (b) primarily | ||||
| for and in connection with specific products or compilations that | ||||
| contain the covered work, unless you entered into that arrangement, | ||||
| or that patent license was granted, prior to 28 March 2007. | ||||
|  | ||||
|   Nothing in this License shall be construed as excluding or limiting | ||||
| any implied license or other defenses to infringement that may | ||||
| otherwise be available to you under applicable patent law. | ||||
|  | ||||
|   12. No Surrender of Others' Freedom. | ||||
|  | ||||
|   If conditions are imposed on you (whether by court order, agreement or | ||||
| otherwise) that contradict the conditions of this License, they do not | ||||
| excuse you from the conditions of this License.  If you cannot convey a | ||||
| covered work so as to satisfy simultaneously your obligations under this | ||||
| License and any other pertinent obligations, then as a consequence you may | ||||
| not convey it at all.  For example, if you agree to terms that obligate you | ||||
| to collect a royalty for further conveying from those to whom you convey | ||||
| the Program, the only way you could satisfy both those terms and this | ||||
| License would be to refrain entirely from conveying the Program. | ||||
|  | ||||
|   13. Use with the GNU Affero General Public License. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, you have | ||||
| permission to link or combine any covered work with a work licensed | ||||
| under version 3 of the GNU Affero General Public License into a single | ||||
| combined work, and to convey the resulting work.  The terms of this | ||||
| License will continue to apply to the part which is the covered work, | ||||
| but the special requirements of the GNU Affero General Public License, | ||||
| section 13, concerning interaction through a network will apply to the | ||||
| combination as such. | ||||
|  | ||||
|   14. Revised Versions of this License. | ||||
|  | ||||
|   The Free Software Foundation may publish revised and/or new versions of | ||||
| the GNU 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 | ||||
| Program specifies that a certain numbered version of the GNU General | ||||
| Public License "or any later version" applies to it, you have the | ||||
| option of following the terms and conditions either of that numbered | ||||
| version or of any later version published by the Free Software | ||||
| Foundation.  If the Program does not specify a version number of the | ||||
| GNU General Public License, you may choose any version ever published | ||||
| by the Free Software Foundation. | ||||
|  | ||||
|   If the Program specifies that a proxy can decide which future | ||||
| versions of the GNU General Public License can be used, that proxy's | ||||
| public statement of acceptance of a version permanently authorizes you | ||||
| to choose that version for the Program. | ||||
|  | ||||
|   Later license versions may give you additional or different | ||||
| permissions.  However, no additional obligations are imposed on any | ||||
| author or copyright holder as a result of your choosing to follow a | ||||
| later version. | ||||
|  | ||||
|   15. Disclaimer of Warranty. | ||||
|  | ||||
|   THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | ||||
| APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | ||||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | ||||
| OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | ||||
| THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||||
| PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | ||||
| IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | ||||
| ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | ||||
|  | ||||
|   16. Limitation of Liability. | ||||
|  | ||||
|   IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | ||||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | ||||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | ||||
| USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | ||||
| DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | ||||
| PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | ||||
| EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | ||||
| SUCH DAMAGES. | ||||
|  | ||||
|   17. Interpretation of Sections 15 and 16. | ||||
|  | ||||
|   If the disclaimer of warranty and limitation of liability provided | ||||
| above cannot be given local legal effect according to their terms, | ||||
| reviewing courts shall apply local law that most closely approximates | ||||
| an absolute waiver of all civil liability in connection with the | ||||
| Program, unless a warranty or assumption of liability accompanies a | ||||
| copy of the Program in return for a fee. | ||||
|  | ||||
|                      END OF TERMS AND CONDITIONS | ||||
|  | ||||
|             How to Apply These Terms to Your New Programs | ||||
|  | ||||
|   If you develop a new program, and you want it to be of the greatest | ||||
| possible use to the public, the best way to achieve this is to make it | ||||
| free software which everyone can redistribute and change under these terms. | ||||
|  | ||||
|   To do so, attach the following notices to the program.  It is safest | ||||
| to attach them to the start of each source file to most effectively | ||||
| state the exclusion of warranty; and each file should have at least | ||||
| the "copyright" line and a pointer to where the full notice is found. | ||||
|  | ||||
|     <one line to give the program's name and a brief idea of what it does.> | ||||
|     Copyright (C) <year>  <name of author> | ||||
|  | ||||
|     This program is free software: you can redistribute it and/or modify | ||||
|     it under the terms of the GNU General Public License as published by | ||||
|     the Free Software Foundation, either version 3 of the License, or | ||||
|     (at your option) any later version. | ||||
|  | ||||
|     This program is distributed in the hope that it will be useful, | ||||
|     but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|     GNU General Public License for more details. | ||||
|  | ||||
|     You should have received a copy of the GNU General Public License | ||||
|     along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| Also add information on how to contact you by electronic and paper mail. | ||||
|  | ||||
|   If the program does terminal interaction, make it output a short | ||||
| notice like this when it starts in an interactive mode: | ||||
|  | ||||
|     <program>  Copyright (C) <year>  <name of author> | ||||
|     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | ||||
|     This is free software, and you are welcome to redistribute it | ||||
|     under certain conditions; type `show c' for details. | ||||
|  | ||||
| The hypothetical commands `show w' and `show c' should show the appropriate | ||||
| parts of the General Public License.  Of course, your program's commands | ||||
| might be different; for a GUI interface, you would use an "about box". | ||||
|  | ||||
|   You should also get your employer (if you work as a programmer) or school, | ||||
| if any, to sign a "copyright disclaimer" for the program, if necessary. | ||||
| For more information on this, and how to apply and follow the GNU GPL, see | ||||
| <https://www.gnu.org/licenses/>. | ||||
|  | ||||
|   The GNU General Public License does not permit incorporating your program | ||||
| into proprietary programs.  If your program is a subroutine library, you | ||||
| may consider it more useful to permit linking proprietary applications with | ||||
| the library.  If this is what you want to do, use the GNU Lesser General | ||||
| Public License instead of this License.  But first, please read | ||||
| <https://www.gnu.org/licenses/why-not-lgpl.html>. | ||||
							
								
								
									
										8
									
								
								cppcheck.supp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								cppcheck.supp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| unusedFunction | ||||
| shadowFunction | ||||
| missingIncludeSystem | ||||
| ignoredReturnValue | ||||
|  | ||||
| *:build/generated/* | ||||
| *:build/_deps/* | ||||
| *:vendor/* | ||||
							
								
								
									
										30
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								flake.nix
									
									
									
									
									
								
							| @@ -16,7 +16,22 @@ | ||||
|       system: | ||||
|       let | ||||
|         pkgs = import nixpkgs { inherit system; }; | ||||
|         nativeBuildInputs = [ ]; | ||||
|         nativeBuildInputs = with pkgs; [ | ||||
|           pkg-config | ||||
|           wayland | ||||
|           wayland-protocols | ||||
|           wlr-protocols | ||||
|           wayland-scanner | ||||
|           libGL | ||||
|           libportal | ||||
|           glib | ||||
|           libxkbcommon | ||||
|           fontconfig | ||||
|           harfbuzz | ||||
|           sqlite | ||||
| 	  zenity | ||||
| 	  boost | ||||
|         ]; | ||||
|         buildInputs = with pkgs; [ | ||||
|           cmake | ||||
|           ninja | ||||
| @@ -31,23 +46,12 @@ | ||||
|                 [ | ||||
|                   llvmPackages_21.clang-tools | ||||
|                   lldb | ||||
|                   gdb | ||||
|                   codespell | ||||
|                   doxygen | ||||
|                   gtest | ||||
|                   cppcheck | ||||
|                   inotify-tools | ||||
|  | ||||
|                   pkg-config | ||||
|                   wayland | ||||
|                   wayland-protocols | ||||
|                   wlr-protocols | ||||
|                   wayland-scanner | ||||
|                   libGL | ||||
|                   libportal | ||||
|                   glib | ||||
|                   libxkbcommon | ||||
|                   fontconfig | ||||
|                   harfbuzz | ||||
|                 ] | ||||
|                 ++ buildInputs | ||||
|                 ++ nativeBuildInputs | ||||
|   | ||||
							
								
								
									
										811
									
								
								src/App.cpp
									
									
									
									
									
								
							
							
						
						
									
										811
									
								
								src/App.cpp
									
									
									
									
									
								
							| @@ -1,17 +1,24 @@ | ||||
| #include "App.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cassert> | ||||
| #include <chrono> | ||||
| #include <cmath> | ||||
| #include <cstdio> | ||||
| #include <cstdlib> | ||||
| #include <cstring> | ||||
| #include <poll.h> | ||||
| #include <print> | ||||
| #include <pthread.h> | ||||
| #include <ranges> | ||||
| #include <signal.h> | ||||
| #include <span> | ||||
| #include <sys/mman.h> | ||||
| #include <sys/signalfd.h> | ||||
| #include <thread> | ||||
| #include <unistd.h> | ||||
| #include <unordered_set> | ||||
| #include <vector> | ||||
|  | ||||
| #include <GLES3/gl3.h> | ||||
| #include <fontconfig/fontconfig.h> | ||||
| @@ -27,6 +34,84 @@ | ||||
| #include "blur-client-protocol.h" | ||||
| #include "ext-background-effect-v1-client-protocol.h" | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| constexpr usize MAX_SURROUNDING_BYTES = 4000; | ||||
|  | ||||
| inline auto is_utf8_continuation(char c) -> bool | ||||
| { | ||||
| 	return (static_cast<unsigned char>(c) & 0xC0) == 0x80; | ||||
| } | ||||
|  | ||||
| inline auto adjust_utf8_backward(std::string const &text, int index) -> int | ||||
| { | ||||
| 	index = std::clamp(index, 0, static_cast<int>(text.size())); | ||||
| 	while ( | ||||
| 	    index > 0 && is_utf8_continuation(text[static_cast<usize>(index - 1)])) | ||||
| 		--index; | ||||
| 	return index; | ||||
| } | ||||
|  | ||||
| inline auto adjust_utf8_forward(std::string const &text, int index) -> int | ||||
| { | ||||
| 	int const size = static_cast<int>(text.size()); | ||||
| 	index = std::clamp(index, 0, size); | ||||
| 	while ( | ||||
| 	    index < size && is_utf8_continuation(text[static_cast<usize>(index)])) | ||||
| 		++index; | ||||
| 	return index; | ||||
| } | ||||
|  | ||||
| struct SurroundingSlice { | ||||
| 	std::string text; | ||||
| 	int cursor { 0 }; | ||||
| 	int anchor { 0 }; | ||||
| }; | ||||
|  | ||||
| auto clamp_surrounding_text(std::string const &text, int cursor, int anchor) | ||||
|     -> SurroundingSlice | ||||
| { | ||||
| 	int const size = static_cast<int>(text.size()); | ||||
| 	cursor = std::clamp(cursor, 0, size); | ||||
| 	anchor = std::clamp(anchor, 0, size); | ||||
|  | ||||
| 	if (text.size() <= MAX_SURROUNDING_BYTES) { | ||||
| 		return SurroundingSlice { text, cursor, anchor }; | ||||
| 	} | ||||
|  | ||||
| 	int window_start = std::max(0, | ||||
| 	    std::min(cursor, anchor) - static_cast<int>(MAX_SURROUNDING_BYTES / 2)); | ||||
| 	int window_end = window_start + static_cast<int>(MAX_SURROUNDING_BYTES); | ||||
| 	int const max_pos = std::max(cursor, anchor); | ||||
| 	if (window_end < max_pos) { | ||||
| 		window_end = max_pos; | ||||
| 		window_start | ||||
| 		    = std::max(0, window_end - static_cast<int>(MAX_SURROUNDING_BYTES)); | ||||
| 	} | ||||
| 	if (window_end > size) | ||||
| 		window_end = size; | ||||
| 	if (window_end - window_start > static_cast<int>(MAX_SURROUNDING_BYTES)) { | ||||
| 		window_start = window_end - static_cast<int>(MAX_SURROUNDING_BYTES); | ||||
| 	} | ||||
|  | ||||
| 	window_start = adjust_utf8_backward(text, window_start); | ||||
| 	window_end = adjust_utf8_forward(text, window_end); | ||||
| 	if (window_end < window_start) | ||||
| 		window_end = window_start; | ||||
|  | ||||
| 	std::string slice(text.begin() + window_start, text.begin() + window_end); | ||||
| 	int const new_cursor | ||||
| 	    = std::clamp(cursor - window_start, 0, static_cast<int>(slice.size())); | ||||
| 	int const new_anchor | ||||
| 	    = std::clamp(anchor - window_start, 0, static_cast<int>(slice.size())); | ||||
|  | ||||
| 	return SurroundingSlice { std::move(slice), new_cursor, new_anchor }; | ||||
| } | ||||
|  | ||||
| } // namespace | ||||
|  | ||||
| auto TypingBuffer::push_utf8(char const *s) -> void | ||||
| { | ||||
| 	for (unsigned char const *p = reinterpret_cast<unsigned char const *>(s); | ||||
| @@ -58,6 +143,73 @@ App::App() | ||||
| 	init_egl(); | ||||
| 	init_signal(); | ||||
| 	init_theme_portal(); | ||||
|  | ||||
| 	{ | ||||
| 		auto const env = getenv("XDG_DATA_HOME"); | ||||
| 		if (env && *env) { | ||||
| 			if (std::filesystem::exists(env)) { | ||||
| 				m_data_home_dir = env; | ||||
| 			} | ||||
| 		} | ||||
| 		if (m_data_home_dir.empty()) { | ||||
| 			auto const home = getenv("HOME"); | ||||
| 			assert(home && *home); | ||||
| 			m_data_home_dir = std::filesystem::path(home) / ".local" / "share"; | ||||
| 			std::filesystem::create_directories(m_data_home_dir); | ||||
| 		} | ||||
| 		m_data_home_dir /= "waylight"; | ||||
| 		std::filesystem::create_directories(m_data_home_dir); | ||||
| 	} | ||||
|  | ||||
| 	{ | ||||
| 		auto const env = getenv("XDG_CONFIG_HOME"); | ||||
| 		if (env && *env) { | ||||
| 			if (std::filesystem::exists(env)) { | ||||
| 				m_config_home_dir = env; | ||||
| 			} | ||||
| 		} | ||||
| 		if (m_config_home_dir.empty()) { | ||||
| 			auto const home = getenv("HOME"); | ||||
| 			assert(home && *home); | ||||
| 			m_config_home_dir = std::filesystem::path(home) / ".config"; | ||||
| 			std::filesystem::create_directories(m_config_home_dir); | ||||
| 		} | ||||
| 		m_config_home_dir /= "waylight"; | ||||
| 		std::filesystem::create_directories(m_config_home_dir); | ||||
|  | ||||
| 		m_config = Config::load(m_config_home_dir); | ||||
| 	} | ||||
|  | ||||
| 	m_db = std::make_shared<SQLite::Database>(m_data_home_dir / "data.db", | ||||
| 	    SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); | ||||
|  | ||||
| 	SQLite::Statement(*m_db, R"( | ||||
| 		CREATE TABLE IF NOT EXISTS ApplicationCache ( | ||||
| 			id INTEGER PRIMARY KEY NOT NULL, | ||||
| 			type INTEGER NOT NULL, | ||||
| 			desktop_entry_path TEXT NOT NULL, | ||||
| 			terminal BOOL NOT NULL, | ||||
| 			no_display BOOL NOT NULL, | ||||
| 			path TEXT, | ||||
| 			comment TEXT, | ||||
| 			dbus_activatable BOOL NOT NULL | ||||
| 		) | ||||
| 	)") | ||||
| 	    .exec(); | ||||
|  | ||||
| 	SQLite::Statement(*m_db, R"( | ||||
| 		CREATE TABLE IF NOT EXISTS ApplicationActionCache ( | ||||
| 			id INTEGER PRIMARY KEY NOT NULL, | ||||
| 			id_app INTEGER NOT NULL, | ||||
| 			name TEXT NOT NULL, | ||||
| 			exec TEXT, | ||||
| 			icon TEXT, | ||||
| 			FOREIGN KEY (id_app) REFERENCES ApplicationCache(id) | ||||
| 		) | ||||
| 	)") | ||||
| 	    .exec(); | ||||
|  | ||||
| 	m_cache.emplace(m_db); | ||||
| } | ||||
|  | ||||
| App::~App() | ||||
| @@ -65,6 +217,9 @@ App::~App() | ||||
| 	if (m_sfd != -1) | ||||
| 		close(m_sfd); | ||||
|  | ||||
| 	for (auto &[_, tex] : m_textures) | ||||
| 		UnloadTexture(tex); | ||||
|  | ||||
| 	destroy_layer_surface(); | ||||
|  | ||||
| 	if (m_gl.edpy != EGL_NO_DISPLAY) { | ||||
| @@ -79,6 +234,14 @@ App::~App() | ||||
| 		xkb_keymap_unref(m_kbd.xkb_keymap_v); | ||||
| 	if (m_kbd.xkb_ctx_v) | ||||
| 		xkb_context_unref(m_kbd.xkb_ctx_v); | ||||
| 	if (m_wayland.text_input) { | ||||
| 		zwp_text_input_v3_destroy(m_wayland.text_input); | ||||
| 		m_wayland.text_input = nullptr; | ||||
| 	} | ||||
| 	if (m_wayland.text_input_mgr) { | ||||
| 		zwp_text_input_manager_v3_destroy(m_wayland.text_input_mgr); | ||||
| 		m_wayland.text_input_mgr = nullptr; | ||||
| 	} | ||||
| 	if (m_wayland.kbd) | ||||
| 		wl_keyboard_destroy(m_wayland.kbd); | ||||
| 	if (m_wayland.seat) | ||||
| @@ -101,7 +264,10 @@ auto App::run() -> void | ||||
| 	SetWindowSize(m_win_w, m_win_h); | ||||
| 	while (m_running) { | ||||
| 		pump_events(); | ||||
| 		m_ir.color(m_accent_color); | ||||
| 		tick(); | ||||
| 		m_kbd.typing.clear(); | ||||
| 		m_kbd.clear_transients(); | ||||
| 		std::this_thread::sleep_for(std::chrono::milliseconds(16)); | ||||
| 	} | ||||
| } | ||||
| @@ -132,9 +298,9 @@ auto App::init_wayland() -> void | ||||
|  | ||||
| 	static wl_keyboard_listener keyboard_listener {}; | ||||
| 	{ | ||||
| 		auto kb_keymap = [](void *data, wl_keyboard *, u32 format, i32 fd, | ||||
| 		auto kb_keymap { [](void *data, wl_keyboard *, u32 format, i32 fd, | ||||
| 			                 u32 size) -> void { | ||||
| 			auto *app = static_cast<App *>(data); | ||||
| 			auto *app { static_cast<App *>(data) }; | ||||
| 			if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { | ||||
| 				close(fd); | ||||
| 				return; | ||||
| @@ -158,21 +324,25 @@ auto App::init_wayland() -> void | ||||
| 			    : nullptr; | ||||
| 			munmap(map, size); | ||||
| 			close(fd); | ||||
| 		}; | ||||
| 		} }; | ||||
|  | ||||
| 		auto kb_enter = [](void *, wl_keyboard *, u32, wl_surface *, | ||||
| 		                    wl_array *) -> void { }; | ||||
| 		auto kb_enter { [](void *data, wl_keyboard *, u32 serial, wl_surface *, | ||||
| 			                wl_array *) -> void { | ||||
| 			static_cast<App *>(data)->m_last_serial = serial; | ||||
| 		} }; | ||||
| 		auto kb_leave | ||||
| 		    = [](void *data, wl_keyboard *, u32, wl_surface *) -> void { | ||||
| 			static_cast<App *>(data)->m_kbd.held.clear(); | ||||
| 		}; | ||||
|  | ||||
| 		auto kb_key = [](void *data, wl_keyboard *, u32, u32, u32 key, | ||||
| 		auto kb_key { [](void *data, wl_keyboard *, u32 serial, u32, u32 key, | ||||
| 			              u32 state) -> void { | ||||
| 			auto *app = static_cast<App *>(data); | ||||
| 			auto *app { static_cast<App *>(data) }; | ||||
| 			if (!app->m_kbd.xkb_state_v) | ||||
| 				return; | ||||
|  | ||||
| 			app->m_last_serial = serial; | ||||
|  | ||||
| 			xkb_keycode_t kc = key + 8; | ||||
|  | ||||
| 			if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { | ||||
| @@ -186,7 +356,68 @@ auto App::init_wayland() -> void | ||||
| 				bool ctrl = app->m_kbd.mod_active("Control"); | ||||
| 				bool alt = app->m_kbd.mod_active("Mod1"); | ||||
| 				bool meta = app->m_kbd.mod_active("Mod4"); | ||||
| 				if (!(ctrl || alt || meta)) { | ||||
|  | ||||
| 				bool handled = false; | ||||
| 				switch (sym) { | ||||
| 				case XKB_KEY_Left: | ||||
| 					app->m_kbd.typing.push_back(1); | ||||
| 					handled = true; | ||||
| 					break; | ||||
| 				case XKB_KEY_Down: | ||||
| 					app->m_kbd.typing.push_back(2); | ||||
| 					handled = true; | ||||
| 					break; | ||||
| 				case XKB_KEY_Up: | ||||
| 					app->m_kbd.typing.push_back(3); | ||||
| 					handled = true; | ||||
| 					break; | ||||
| 				case XKB_KEY_Right: | ||||
| 					app->m_kbd.typing.push_back(4); | ||||
| 					handled = true; | ||||
| 					break; | ||||
| 				case XKB_KEY_BackSpace: | ||||
| 					app->m_kbd.typing.push_back(8); | ||||
| 					handled = true; | ||||
| 					break; | ||||
| 				case XKB_KEY_Delete: | ||||
| 				case XKB_KEY_KP_Delete: | ||||
| 					app->m_kbd.typing.push_back(0x7F); | ||||
| 					handled = true; | ||||
| 					break; | ||||
| 				case XKB_KEY_Return: | ||||
| 				case XKB_KEY_KP_Enter: | ||||
| 					app->m_kbd.typing.push_back('\n'); | ||||
| 					handled = true; | ||||
| 					break; | ||||
| 				case XKB_KEY_v: | ||||
| 				case XKB_KEY_V: | ||||
| 					app->m_kbd.typing.push_back('v'); | ||||
| 					handled = true; | ||||
| 					break; | ||||
| 				case XKB_KEY_c: | ||||
| 				case XKB_KEY_C: | ||||
| 					app->m_kbd.typing.push_back('c'); | ||||
| 					handled = true; | ||||
| 					break; | ||||
| 				case XKB_KEY_w: | ||||
| 				case XKB_KEY_W: | ||||
| 					if (ctrl) { | ||||
| 						app->m_kbd.typing.push_back(8); | ||||
| 						handled = true; | ||||
| 					} | ||||
| 					break; | ||||
| 				case XKB_KEY_a: | ||||
| 				case XKB_KEY_A: | ||||
| 					if (ctrl) { | ||||
| 						app->m_kbd.typing.push_back('a'); | ||||
| 						handled = true; | ||||
| 					} | ||||
| 					break; | ||||
| 				default: | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				if (!handled && !(ctrl || alt || meta)) { | ||||
| 					u32 cp = xkb_keysym_to_utf32(sym); | ||||
| 					if (cp >= 0x20) { | ||||
| 						char buf[8]; | ||||
| @@ -203,27 +434,117 @@ auto App::init_wayland() -> void | ||||
| 				app->m_kbd.held.erase(key); | ||||
| 				xkb_state_update_key(app->m_kbd.xkb_state_v, kc, XKB_KEY_UP); | ||||
| 			} | ||||
| 		}; | ||||
| 		} }; | ||||
|  | ||||
| 		auto kb_mods = [](void *data, wl_keyboard *, u32, u32 depressed, | ||||
| 		auto kb_mods { [](void *data, wl_keyboard *, u32, u32 depressed, | ||||
| 			               u32 latched, u32 locked, u32 group) -> void { | ||||
| 			auto *app = static_cast<App *>(data); | ||||
| 			auto *app { static_cast<App *>(data) }; | ||||
| 			if (!app->m_kbd.xkb_state_v) | ||||
| 				return; | ||||
| 			xkb_state_update_mask(app->m_kbd.xkb_state_v, depressed, latched, | ||||
| 			    locked, 0, 0, group); | ||||
| 		}; | ||||
| 		} }; | ||||
|  | ||||
| 		auto kb_repeat_info = [](void *, wl_keyboard *, i32, i32) -> void { }; | ||||
| 		auto kb_repeat_info { [](void *, wl_keyboard *, i32, i32) -> void { } }; | ||||
|  | ||||
| 		keyboard_listener = { kb_keymap, kb_enter, kb_leave, kb_key, kb_mods, | ||||
| 			kb_repeat_info }; | ||||
| 	} | ||||
|  | ||||
| 	static zwp_text_input_v3_listener text_input_listener {}; | ||||
| 	{ | ||||
| 		auto ti_enter = | ||||
| 		    [](void *data, zwp_text_input_v3 *, | ||||
| 		        wl_surface *surface) // cppcheck-suppress constParameterPointer | ||||
| 		    -> void { | ||||
| 			auto *app { static_cast<App *>(data) }; | ||||
| 			bool const focused_surface | ||||
| 			    = surface && surface == app->m_wayland.surface; | ||||
| 			app->m_ime.seat_focus = focused_surface; | ||||
| 			app->m_ime.pending = {}; | ||||
| 			app->m_ime.pending_done = false; | ||||
| 			if (!focused_surface) { | ||||
| 				app->m_ime.enabled = false; | ||||
| 				app->m_ime.last_surrounding.clear(); | ||||
| 				if (app->m_gui) | ||||
| 					app->m_gui->ime_clear_preedit(); | ||||
| 			} else { | ||||
| 				app->m_ime.surrounding_dirty = true; | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		auto ti_leave { [](void *data, zwp_text_input_v3 *, | ||||
| 			                wl_surface *) -> void { | ||||
| 			auto *app { static_cast<App *>(data) }; | ||||
| 			app->m_ime.seat_focus = false; | ||||
| 			app->m_ime.enabled = false; | ||||
| 			app->m_ime.pending = {}; | ||||
| 			app->m_ime.pending_done = false; | ||||
| 			app->m_ime.last_surrounding.clear(); | ||||
| 			if (app->m_gui) | ||||
| 				app->m_gui->ime_clear_preedit(); | ||||
| 		} }; | ||||
|  | ||||
| 		auto ti_preedit { [](void *data, zwp_text_input_v3 *, char const *text, | ||||
| 			                  int32_t cursor_begin, | ||||
| 			                  int32_t cursor_end) -> void { | ||||
| 			auto *app { static_cast<App *>(data) }; | ||||
| 			auto &pending { app->m_ime.pending }; | ||||
| 			pending.has_preedit = true; | ||||
| 			pending.preedit_text = text ? text : ""; | ||||
| 			pending.cursor_begin = cursor_begin; | ||||
| 			pending.cursor_end = cursor_end; | ||||
| 		} }; | ||||
|  | ||||
| 		auto ti_commit { [](void *data, zwp_text_input_v3 *, | ||||
| 			                 char const *text) -> void { | ||||
| 			auto *app { static_cast<App *>(data) }; | ||||
| 			auto &pending { app->m_ime.pending }; | ||||
| 			pending.has_commit = true; | ||||
| 			pending.commit_text = text ? text : ""; | ||||
| 		} }; | ||||
|  | ||||
| 		auto ti_delete { [](void *data, zwp_text_input_v3 *, uint32_t before, | ||||
| 			                 uint32_t after) -> void { | ||||
| 			auto *app { static_cast<App *>(data) }; | ||||
| 			auto &pending { app->m_ime.pending }; | ||||
| 			pending.has_delete = true; | ||||
| 			pending.before = before; | ||||
| 			pending.after = after; | ||||
| 		} }; | ||||
|  | ||||
| 		auto ti_done | ||||
| 		    = [](void *data, zwp_text_input_v3 *, uint32_t serial) -> void { | ||||
| 			auto *app { static_cast<App *>(data) }; | ||||
| 			app->m_ime.pending_done = true; | ||||
| 			app->m_ime.pending_serial = serial; | ||||
| 			app->m_ime.surrounding_dirty = true; | ||||
| 		}; | ||||
|  | ||||
| 		text_input_listener | ||||
| 		    = { ti_enter, ti_leave, ti_preedit, ti_commit, ti_delete, ti_done }; | ||||
| 	} | ||||
|  | ||||
| 	static auto ensure_text_input { +[](App *app) -> void { | ||||
| 		if (!app->m_wayland.text_input_mgr || !app->m_wayland.seat | ||||
| 		    || app->m_wayland.text_input) | ||||
| 			return; | ||||
| 		app->m_wayland.text_input = zwp_text_input_manager_v3_get_text_input( | ||||
| 		    app->m_wayland.text_input_mgr, app->m_wayland.seat); | ||||
| 		if (!app->m_wayland.text_input) | ||||
| 			return; | ||||
| 		zwp_text_input_v3_add_listener( | ||||
| 		    app->m_wayland.text_input, &text_input_listener, app); | ||||
| 		app->m_ime.supported = true; | ||||
| 		app->m_ime.enabled = false; | ||||
| 		app->m_ime.last_surrounding.clear(); | ||||
| 		app->m_ime.sent_serial = 0; | ||||
| 	} }; | ||||
|  | ||||
| 	auto handle_registry_global | ||||
| 	    = [](void *data, wl_registry *registry, u32 name, char const *interface, | ||||
| 	          u32 version) -> void { | ||||
| 		auto *app = static_cast<App *>(data); | ||||
| 		auto *app { static_cast<App *>(data) }; | ||||
| 		if (std::strcmp(interface, wl_compositor_interface.name) == 0) { | ||||
| 			app->m_wayland.compositor = static_cast<wl_compositor *>( | ||||
| 			    wl_registry_bind(registry, name, &wl_compositor_interface, 4)); | ||||
| @@ -233,12 +554,14 @@ auto App::init_wayland() -> void | ||||
| 			static struct wl_seat_listener const seat_listener = { | ||||
| 				.capabilities = | ||||
| 				    [](void *data, struct wl_seat *seat, u32 caps) { | ||||
| 				        auto *app = static_cast<App *>(data); | ||||
| 				        auto *app { static_cast<App *>(data) }; | ||||
| 				        if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { | ||||
| 					        app->m_wayland.kbd = wl_seat_get_keyboard(seat); | ||||
| 					        wl_keyboard_add_listener( | ||||
| 					            app->m_wayland.kbd, &keyboard_listener, data); | ||||
| 					        app->m_ime.seat_focus = false; | ||||
| 				        } | ||||
| 				        ensure_text_input(app); | ||||
| 				    }, | ||||
| 				.name = [](void *, struct wl_seat *, char const *) {}, | ||||
| 			}; | ||||
| @@ -259,6 +582,120 @@ auto App::init_wayland() -> void | ||||
| 			app->m_wayland.kde_blur_mgr | ||||
| 			    = static_cast<org_kde_kwin_blur_manager *>(wl_registry_bind( | ||||
| 			        registry, name, &org_kde_kwin_blur_manager_interface, 1)); | ||||
| 		} else if (std::strcmp( | ||||
| 		               interface, zwp_text_input_manager_v3_interface.name) | ||||
| 		    == 0) { | ||||
| 			app->m_wayland.text_input_mgr | ||||
| 			    = static_cast<zwp_text_input_manager_v3 *>(wl_registry_bind( | ||||
| 			        registry, name, &zwp_text_input_manager_v3_interface, 1)); | ||||
| 			app->m_ime.supported = true; | ||||
| 			ensure_text_input(app); | ||||
| 		} else if (std::strcmp(interface, wl_data_device_manager_interface.name) | ||||
| 		    == 0) { | ||||
| 			app->m_wayland.ddm | ||||
| 			    = static_cast<wl_data_device_manager *>(wl_registry_bind( | ||||
| 			        registry, name, &wl_data_device_manager_interface, | ||||
| 			        std::min<uint32_t>(version, 3))); | ||||
| 			if (app->m_wayland.ddm && !app->m_wayland.ddev) { | ||||
| 				app->m_wayland.ddev = wl_data_device_manager_get_data_device( | ||||
| 				    app->m_wayland.ddm, app->m_wayland.seat); | ||||
| 				static wl_data_device_listener const ddev_l = { | ||||
| 					.data_offer = | ||||
| 					    [](void *data, wl_data_device *, wl_data_offer *offer) { | ||||
| 					        auto *app = static_cast<App *>(data); | ||||
| 					        static wl_data_offer_listener const offer_l = { | ||||
| 						        .offer = | ||||
| 						            [](void *data, wl_data_offer *, | ||||
| 						                char const *mime) { | ||||
| 						                auto *app = static_cast<App *>(data); | ||||
| 						                (void)app; | ||||
| 						                (void)mime; | ||||
| 						            }, | ||||
| #if WL_DATA_OFFER_SOURCE_ACTIONS_SINCE_VERSION | ||||
| 						        .source_actions | ||||
| 						        = [](void *, wl_data_offer *, uint32_t) {}, | ||||
| 						        .action | ||||
| 						        = [](void *, wl_data_offer *, uint32_t) {} | ||||
| #endif | ||||
| 					        }; | ||||
| 					        wl_data_offer_add_listener(offer, &offer_l, app); | ||||
| 					        if (app->m_wayland.curr_offer | ||||
| 					            && app->m_wayland.curr_offer != offer) | ||||
| 						        wl_data_offer_destroy( | ||||
| 						            app->m_wayland.curr_offer); | ||||
| 					        app->m_wayland.curr_offer = offer; | ||||
| 					    }, | ||||
| 					.enter | ||||
| 					= [](void *, wl_data_device *, uint32_t, wl_surface *, | ||||
| 					      wl_fixed_t, wl_fixed_t, wl_data_offer *) {}, | ||||
| 					.leave = [](void *, wl_data_device *) {}, | ||||
| 					.motion = [](void *, wl_data_device *, uint32_t, wl_fixed_t, | ||||
| 					              wl_fixed_t) {}, | ||||
| 					.drop = [](void *, wl_data_device *) {}, | ||||
| 					.selection = | ||||
| 					    [](void *data, wl_data_device *, wl_data_offer *offer) { | ||||
| 					        auto *app = static_cast<App *>(data); | ||||
| 					        if (!offer) { | ||||
| 						        app->m_clipboard_cache.clear(); | ||||
| 						        return; | ||||
| 					        } | ||||
|  | ||||
| 					        char const *mime = "text/plain;charset=utf-8"; | ||||
| 					        int fds[2]; | ||||
| 					        if (pipe(fds) != 0) | ||||
| 						        return; | ||||
|  | ||||
| 					        wl_data_offer_receive(offer, mime, fds[1]); | ||||
| 					        wl_display_flush(app->m_wayland.display); | ||||
| 					        close(fds[1]); | ||||
|  | ||||
| 					        int rfd = fds[0]; | ||||
|  | ||||
| 					        std::thread([app, rfd, offer]() { | ||||
| 						        std::string data; | ||||
| 						        char buf[4096]; | ||||
| 						        for (;;) { | ||||
| 							        ssize_t n = read(rfd, buf, sizeof buf); | ||||
| 							        if (n > 0) { | ||||
| 								        data.append(buf, buf + n); | ||||
| 								        continue; | ||||
| 							        } | ||||
| 							        if (n < 0 && errno == EINTR) | ||||
| 								        continue; | ||||
| 							        break; | ||||
| 						        } | ||||
| 						        close(rfd); | ||||
|  | ||||
| 						        struct Ctx { | ||||
| 							        App *app; | ||||
| 							        wl_data_offer *offer; | ||||
| 							        std::string data; | ||||
| 						        }; | ||||
| 						        auto *ctx | ||||
| 						            = new Ctx { app, offer, std::move(data) }; | ||||
|  | ||||
| 						        g_main_context_invoke( | ||||
| 						            nullptr, | ||||
| 						            +[](gpointer p) -> gboolean { | ||||
| 							            auto *ctx = static_cast<Ctx *>(p); | ||||
| 							            if (!ctx->data.empty()) | ||||
| 								            ctx->app->m_clipboard_cache | ||||
| 								                = std::move(ctx->data); | ||||
| 							            if (ctx->offer | ||||
| 							                == ctx->app->m_wayland.curr_offer) { | ||||
| 								            wl_data_offer_destroy(ctx->offer); | ||||
| 								            ctx->app->m_wayland.curr_offer | ||||
| 								                = nullptr; | ||||
| 							            } | ||||
| 							            delete ctx; | ||||
| 							            return G_SOURCE_REMOVE; | ||||
| 						            }, | ||||
| 						            ctx); | ||||
| 					        }).detach(); | ||||
| 					    }, | ||||
| 				}; | ||||
| 				wl_data_device_add_listener(app->m_wayland.ddev, &ddev_l, app); | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| @@ -295,14 +732,54 @@ auto App::init_egl() -> void | ||||
|  | ||||
| 	ensure_egl_surface(); | ||||
|  | ||||
| 	{ | ||||
| 		auto const *env = getenv("WAYLIGHT_DEBUG"); | ||||
| 		if (env && *env) { | ||||
| 			SetTraceLogLevel(LOG_DEBUG); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	InitWindow(m_win_w, m_win_h, ""); | ||||
|  | ||||
| 	m_tr = TextRenderer(); | ||||
| 	auto const font = find_font_path(); | ||||
| 	m_tr = std::make_shared<TextRenderer>(); | ||||
| 	m_gui = std::make_shared<ImGui>(m_tr); | ||||
| 	auto const font { find_font_path() }; | ||||
| 	assert(font && "Could not find font"); | ||||
| 	auto const font_handle = m_tr->load_font(*font); | ||||
| 	std::vector<std::filesystem::path> fallback_paths; | ||||
| 	std::unordered_set<std::string> seen_paths; | ||||
| 	auto const primary_path_str { font->string() }; | ||||
|  | ||||
| 	constexpr char const *fallback_candidates[] = { | ||||
| 		"Noto Sans CJK JP:style=Regular", | ||||
| 		"Noto Sans CJK SC:style=Regular", | ||||
| 		"Noto Sans CJK KR:style=Regular", | ||||
| 		"Noto Sans CJK TC:style=Regular", | ||||
| 		"sans-serif:lang=ja", | ||||
| 		"sans-serif:lang=ko", | ||||
| 		"sans-serif:lang=zh-cn", | ||||
| 		"sans-serif:lang=zh-tw", | ||||
| 		"sans-serif:lang=zh-hk", | ||||
| 		"Noto Color Emoji:style=Regular", | ||||
| 	}; | ||||
| 	for (auto const *name : fallback_candidates) { | ||||
| 		if (auto fallback { find_font_path(name) }) { | ||||
| 			auto const path_str { fallback->string() }; | ||||
| 			if (path_str == primary_path_str) | ||||
| 				continue; | ||||
| 			if (!seen_paths.emplace(path_str).second) | ||||
| 				continue; | ||||
| 			fallback_paths.push_back(*fallback); | ||||
| 		} | ||||
| 	} | ||||
| 	if (fallback_paths.empty()) { | ||||
| 		TraceLog(LOG_WARNING, | ||||
| 		    "No fallback fonts found; some glyphs may render as missing"); | ||||
| 	} | ||||
| 	auto const font_handle { m_tr->load_font( | ||||
| 		*font, std::span(fallback_paths)) }; | ||||
| 	assert(font_handle && "Could not load font"); | ||||
| 	m_font = *font_handle; | ||||
| 	m_gui->set_font(m_font); | ||||
| } | ||||
|  | ||||
| auto App::init_signal() -> void | ||||
| @@ -326,9 +803,9 @@ auto App::init_signal() -> void | ||||
| void App::on_settings_changed(XdpSettings * /*self*/, char const *ns, | ||||
|     char const *key, GVariant * /*value*/, gpointer data) | ||||
| { | ||||
| 	auto *app = static_cast<App *>(data); | ||||
| 	if (g_strcmp0(ns, "org.freedesktop.appearance") == 0 | ||||
| 	    && g_strcmp0(key, "color-scheme") == 0) { | ||||
| 	auto *app { static_cast<App *>(data) }; | ||||
| 	if (g_strcmp0(ns, "org.freedesktop.appearance") == 0) { | ||||
| 		if (g_strcmp0(key, "color-scheme") == 0) { | ||||
| 			guint v = xdp_settings_read_uint(app->m_xdp.settings, | ||||
| 			    "org.freedesktop.appearance", "color-scheme", NULL, NULL); | ||||
|  | ||||
| @@ -336,6 +813,18 @@ void App::on_settings_changed(XdpSettings * /*self*/, char const *ns, | ||||
| 				app->m_active_theme = Theme::Dark; | ||||
| 			else | ||||
| 				app->m_active_theme = Theme::Light; | ||||
| 		} else if (g_strcmp0(key, "accent-color") == 0) { | ||||
| 			auto val { xdp_settings_read_value(app->m_xdp.settings, | ||||
| 				"org.freedesktop.appearance", "accent-color", NULL, NULL) }; | ||||
| 			if (val) { | ||||
| 				gdouble r, g, b; | ||||
| 				g_variant_get(val, "(ddd)", &r, &g, &b); | ||||
| 				app->m_accent_color.r = static_cast<u8>(r * 255); | ||||
| 				app->m_accent_color.g = static_cast<u8>(g * 255); | ||||
| 				app->m_accent_color.b = static_cast<u8>(b * 255); | ||||
| 				g_variant_unref(val); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -351,6 +840,17 @@ auto App::init_theme_portal() -> void | ||||
| 	else | ||||
| 		m_active_theme = Theme::Light; | ||||
|  | ||||
| 	auto val { xdp_settings_read_value(m_xdp.settings, | ||||
| 		"org.freedesktop.appearance", "accent-color", NULL, NULL) }; | ||||
| 	if (val) { | ||||
| 		gdouble r, g, b; | ||||
| 		g_variant_get(val, "(ddd)", &r, &g, &b); | ||||
| 		m_accent_color.r = static_cast<u8>(r * 255); | ||||
| 		m_accent_color.g = static_cast<u8>(g * 255); | ||||
| 		m_accent_color.b = static_cast<u8>(b * 255); | ||||
| 		g_variant_unref(val); | ||||
| 	} | ||||
|  | ||||
| 	g_signal_connect( | ||||
| 	    m_xdp.settings, "changed", G_CALLBACK(on_settings_changed), this); | ||||
| } | ||||
| @@ -404,9 +904,9 @@ auto App::create_layer_surface() -> void | ||||
| 		    ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND); | ||||
| 	} | ||||
|  | ||||
| 	auto handle_layer_configure = [](void *data, zwlr_layer_surface_v1 *ls, | ||||
| 	auto handle_layer_configure { [](void *data, zwlr_layer_surface_v1 *ls, | ||||
| 		                              u32 serial, u32 w, u32 h) -> void { | ||||
| 		auto *app = static_cast<App *>(data); | ||||
| 		auto *app { static_cast<App *>(data) }; | ||||
| 		if (w) | ||||
| 			app->m_win_w = static_cast<int>(w); | ||||
| 		if (h) | ||||
| @@ -429,11 +929,11 @@ auto App::create_layer_surface() -> void | ||||
|  | ||||
| 		if (app->m_wayland.surface) | ||||
| 			wl_surface_commit(app->m_wayland.surface); | ||||
| 	}; | ||||
| 	} }; | ||||
|  | ||||
| 	auto handle_layer_closed = [](void *data, zwlr_layer_surface_v1 *) -> void { | ||||
| 	auto handle_layer_closed { [](void *data, zwlr_layer_surface_v1 *) -> void { | ||||
| 		static_cast<App *>(data)->m_running = false; | ||||
| 	}; | ||||
| 	} }; | ||||
|  | ||||
| 	static zwlr_layer_surface_v1_listener const lsl = { | ||||
| 		.configure = handle_layer_configure, | ||||
| @@ -541,6 +1041,127 @@ auto App::update_blur_region() -> void | ||||
| 	wl_region_destroy(region); | ||||
| } | ||||
|  | ||||
| auto App::process_pending_text_input() -> void | ||||
| { | ||||
| 	if (!m_ime.pending_done) | ||||
| 		return; | ||||
| 	if (!m_gui) | ||||
| 		return; | ||||
| 	if (!m_ime.bound_text) | ||||
| 		return; | ||||
| 	if (!m_wayland.text_input) | ||||
| 		return; | ||||
|  | ||||
| 	auto focused { m_gui->focused_text_input() }; | ||||
| 	if (!focused || *focused != m_ime.bound_id) { | ||||
| 		m_ime.pending = {}; | ||||
| 		m_ime.pending_done = false; | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	m_gui->ime_clear_preedit(); | ||||
|  | ||||
| 	if (m_ime.pending.has_delete) { | ||||
| 		m_gui->ime_delete_surrounding( | ||||
| 		    *m_ime.bound_text, m_ime.pending.before, m_ime.pending.after); | ||||
| 	} | ||||
|  | ||||
| 	if (m_ime.pending.has_commit) { | ||||
| 		m_gui->ime_commit_text(*m_ime.bound_text, m_ime.pending.commit_text); | ||||
| 	} | ||||
|  | ||||
| 	if (m_ime.pending.has_preedit) { | ||||
| 		m_gui->ime_set_preedit(m_ime.pending.preedit_text, | ||||
| 		    m_ime.pending.cursor_begin, m_ime.pending.cursor_end); | ||||
| 	} else { | ||||
| 		m_gui->ime_clear_preedit(); | ||||
| 	} | ||||
|  | ||||
| 	m_ime.pending = {}; | ||||
| 	m_ime.pending_done = false; | ||||
| 	m_ime.surrounding_dirty = true; | ||||
| } | ||||
|  | ||||
| auto App::update_text_input_state( | ||||
|     std::pmr::string const &text, usize id, Rectangle field_rect) -> void | ||||
| { | ||||
| 	if (!m_wayland.text_input || !m_ime.supported || !m_gui) | ||||
| 		return; | ||||
|  | ||||
| 	m_ime.bound_rect = field_rect; | ||||
|  | ||||
| 	auto focused { m_gui->focused_text_input() }; | ||||
| 	bool const has_focus { focused && (*focused == id) }; | ||||
| 	bool const should_enable { has_focus && m_ime.seat_focus }; | ||||
|  | ||||
| 	if (!should_enable) { | ||||
| 		if (m_ime.enabled) { | ||||
| 			zwp_text_input_v3_disable(m_wayland.text_input); | ||||
| 			zwp_text_input_v3_commit(m_wayland.text_input); | ||||
| 			m_ime.sent_serial++; | ||||
| 			m_ime.enabled = false; | ||||
| 			m_ime.last_surrounding.clear(); | ||||
| 		} | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	bool state_dirty = false; | ||||
|  | ||||
| 	if (!m_ime.enabled) { | ||||
| 		zwp_text_input_v3_enable(m_wayland.text_input); | ||||
| 		zwp_text_input_v3_set_content_type(m_wayland.text_input, | ||||
| 		    ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, | ||||
| 		    ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL); | ||||
| 		m_ime.enabled = true; | ||||
| 		m_ime.surrounding_dirty = true; | ||||
| 		state_dirty = true; | ||||
| 	} | ||||
|  | ||||
| 	if (auto info { m_gui->text_input_surrounding(id, text) }) { | ||||
| 		auto slice | ||||
| 		    = clamp_surrounding_text(info->text, info->cursor, info->anchor); | ||||
| 		if (m_ime.surrounding_dirty || slice.text != m_ime.last_surrounding | ||||
| 		    || slice.cursor != m_ime.last_cursor | ||||
| 		    || slice.anchor != m_ime.last_anchor) { | ||||
| 			zwp_text_input_v3_set_surrounding_text(m_wayland.text_input, | ||||
| 			    slice.text.c_str(), slice.cursor, slice.anchor); | ||||
| 			m_ime.last_surrounding = std::move(slice.text); | ||||
| 			m_ime.last_cursor = slice.cursor; | ||||
| 			m_ime.last_anchor = slice.anchor; | ||||
| 			state_dirty = true; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (auto cursor_info { m_gui->text_input_cursor(id) }) { | ||||
| 		Rectangle rect = cursor_info->rect; | ||||
| 		int32_t const x = static_cast<int32_t>(std::round(rect.x)); | ||||
| 		int32_t const y = static_cast<int32_t>(std::round(rect.y)); | ||||
| 		int32_t const width | ||||
| 		    = std::max(1, static_cast<int32_t>(std::round(rect.width))); | ||||
| 		int32_t const height | ||||
| 		    = std::max(1, static_cast<int32_t>(std::round(rect.height))); | ||||
| 		bool const cur_visible = cursor_info->visible; | ||||
|  | ||||
| 		if (rect.x != m_ime.last_cursor_rect.x | ||||
| 		    || rect.y != m_ime.last_cursor_rect.y | ||||
| 		    || rect.width != m_ime.last_cursor_rect.width | ||||
| 		    || rect.height != m_ime.last_cursor_rect.height | ||||
| 		    || cur_visible != m_ime.last_cursor_visible) { | ||||
| 			zwp_text_input_v3_set_cursor_rectangle( | ||||
| 			    m_wayland.text_input, x, y, width, height); | ||||
| 			m_ime.last_cursor_rect = rect; | ||||
| 			m_ime.last_cursor_visible = cur_visible; | ||||
| 			state_dirty = true; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (state_dirty) { | ||||
| 		zwp_text_input_v3_commit(m_wayland.text_input); | ||||
| 		m_ime.sent_serial++; | ||||
| 		m_ime.surrounding_dirty = false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| auto App::pump_events() -> void | ||||
| { | ||||
| 	while (g_main_context_iteration(nullptr, false)) | ||||
| @@ -552,13 +1173,13 @@ auto App::pump_events() -> void | ||||
| 	pollfd fds[2] { { wl_display_get_fd(m_wayland.display), POLLIN, 0 }, | ||||
| 		{ m_sfd, POLLIN, 0 } }; | ||||
|  | ||||
| 	auto prepared = (wl_display_prepare_read(m_wayland.display) == 0); | ||||
| 	auto ret = poll(fds, 2, 0); | ||||
| 	auto prepared { (wl_display_prepare_read(m_wayland.display) == 0) }; | ||||
| 	auto ret { poll(fds, 2, 0) }; | ||||
|  | ||||
| 	if (ret > 0 && (fds[0].revents & POLLIN)) { | ||||
| 		if (prepared) { | ||||
| 			wl_display_read_events(m_wayland.display); | ||||
| 			prepared = false; | ||||
| 			prepared = false; // cppcheck-suppress unreadVariable | ||||
| 		} | ||||
| 	} else if (prepared) { | ||||
| 		wl_display_cancel_read(m_wayland.display); | ||||
| @@ -573,3 +1194,135 @@ auto App::pump_events() -> void | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| auto App::clipboard(std::string_view const &str) -> void | ||||
| { | ||||
| 	if (!m_wayland.ddm || !m_wayland.ddev || !m_wayland.seat) | ||||
| 		return; | ||||
| 	if (m_last_serial == 0) | ||||
| 		return; | ||||
|  | ||||
| 	if (m_wayland.curr_source) { | ||||
| 		wl_data_source_destroy(m_wayland.curr_source); | ||||
| 		m_wayland.curr_source = nullptr; | ||||
| 	} | ||||
| 	m_wayland.curr_source | ||||
| 	    = wl_data_device_manager_create_data_source(m_wayland.ddm); | ||||
|  | ||||
| 	static wl_data_source_listener const src_l = { | ||||
| 		.target = [](void *, wl_data_source *, char const *) {}, | ||||
| 		.send = | ||||
| 		    [](void *data, wl_data_source *, char const *, int32_t fd) { | ||||
| 		        auto *app = static_cast<App *>(data); | ||||
|  | ||||
| 		        int wfd = dup(fd); | ||||
| 		        close(fd); | ||||
|  | ||||
| 		        std::pmr::string payload = app->m_clipboard_cache; | ||||
| 		        std::thread([wfd, payload = std::move(payload)]() { | ||||
| 			        size_t off = 0; | ||||
| 			        while (off < payload.size()) { | ||||
| 				        ssize_t n = write(wfd, payload.data() + off, | ||||
| 				            std::min<size_t>(64 * 1024, payload.size() - off)); | ||||
| 				        if (n > 0) { | ||||
| 					        off += (size_t)n; | ||||
| 					        continue; | ||||
| 				        } | ||||
| 				        if (n < 0 && (errno == EINTR)) | ||||
| 					        continue; | ||||
| 				        if (n < 0 && (errno == EAGAIN)) { | ||||
| 					        std::this_thread::sleep_for( | ||||
| 					            std::chrono::milliseconds(1)); | ||||
| 					        continue; | ||||
| 				        } | ||||
| 				        break; | ||||
| 			        } | ||||
| 			        close(wfd); | ||||
| 		        }).detach(); | ||||
| 		    }, | ||||
| 		.cancelled = | ||||
| 		    [](void *data, wl_data_source *src) { | ||||
| 		        auto *app = static_cast<App *>(data); | ||||
| 		        if (app->m_wayland.curr_source == src) | ||||
| 			        app->m_wayland.curr_source = nullptr; | ||||
| 		        wl_data_source_destroy(src); | ||||
| 		    }, | ||||
| #if WL_DATA_SOURCE_DND_DROP_PERFORMED_SINCE_VERSION | ||||
| 		.dnd_drop_performed = [](void *, wl_data_source *) {}, | ||||
| 		.dnd_finished = [](void *, wl_data_source *) {}, | ||||
| 		.action = [](void *, wl_data_source *, uint32_t) {} | ||||
| #endif | ||||
| 	}; | ||||
| 	wl_data_source_add_listener(m_wayland.curr_source, &src_l, this); | ||||
| 	wl_data_source_offer(m_wayland.curr_source, "text/plain;charset=utf-8"); | ||||
| 	wl_data_source_offer(m_wayland.curr_source, "text/plain"); | ||||
|  | ||||
| 	m_clipboard_cache.assign(str.begin(), str.end()); | ||||
|  | ||||
| 	wl_data_device_set_selection( | ||||
| 	    m_wayland.ddev, m_wayland.curr_source, m_last_serial); | ||||
| 	wl_display_flush(m_wayland.display); | ||||
| } | ||||
|  | ||||
| void App::execute_command(bool terminal, std::string_view const command) | ||||
| { | ||||
| 	constexpr auto resolve_cmdline { [](std::string_view const cmdline, | ||||
| 		                                 std::vector<std::string> &out) { | ||||
| 		std::ranges::copy(cmdline | std::views::split(' ') | ||||
| 		        | std::views::transform( | ||||
| 		            [](auto &&s) { return std::string(s.begin(), s.end()); }), | ||||
| 		    std::back_inserter(out)); | ||||
| 	} }; | ||||
|  | ||||
| 	std::vector<std::string> args; | ||||
| 	if (terminal) { | ||||
| 		resolve_cmdline(m_config.terminal_cmdline, args); | ||||
| 	} else { | ||||
| 		args.push_back("/bin/sh"); | ||||
| 		args.push_back("-c"); | ||||
| 	} | ||||
|  | ||||
| 	args.push_back(std::string(command)); | ||||
| 	if (auto const &exe { args.at(0) }; exe.at(0) != '/') { | ||||
| 		auto const *path_env { getenv("PATH") }; | ||||
| 		if (!(path_env && *path_env)) { | ||||
| 			path_env = "/bin"; | ||||
| 		} | ||||
| 		auto const path { std::string(path_env) }; | ||||
|  | ||||
| 		for (auto const &dir : | ||||
| 		    path | std::views::split(':') | std::views::transform([](auto &&s) { | ||||
| 			    return std::filesystem::path(s.begin(), s.end()); | ||||
| 		    }) | std::views::filter([](auto &&p) { | ||||
| 			    return std::filesystem::is_directory(p); | ||||
| 		    })) { | ||||
| 			auto const path = dir / exe; | ||||
| 			if (std::filesystem::is_regular_file(path)) { | ||||
| 				args[0] = path.string(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	std::print("Final args: "); | ||||
| 	for (auto const &arg : args) { | ||||
| 		std::print("{} ", arg); | ||||
| 	} | ||||
| 	std::println(""); | ||||
|  | ||||
| 	std::vector<char const *> cargs; | ||||
| 	std::transform(args.begin(), args.end(), std::back_inserter(cargs), | ||||
| 	    [](auto &&s) { return s.c_str(); }); | ||||
| 	cargs.push_back(nullptr); | ||||
| 	auto cargsc { const_cast<char *const *>(cargs.data()) }; | ||||
|  | ||||
| 	auto const pid = fork(); | ||||
| 	if (pid == 0) { | ||||
| 		setsid(); | ||||
|  | ||||
| 		execv(args.at(0).c_str(), cargsc); | ||||
| 	} else if (pid < 0) { | ||||
| 		throw std::runtime_error("Failed to fork process"); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
|   | ||||
							
								
								
									
										103
									
								
								src/App.hpp
									
									
									
									
									
								
							
							
						
						
									
										103
									
								
								src/App.hpp
									
									
									
									
									
								
							| @@ -1,26 +1,38 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <cassert> | ||||
| #include <filesystem> | ||||
| #include <string> | ||||
| #include <unordered_set> | ||||
| #include <vector> | ||||
|  | ||||
| #include <EGL/egl.h> | ||||
| #include <libportal/portal.h> | ||||
| #include <wayland-client-protocol.h> | ||||
| extern "C" { | ||||
| #include "blur-client-protocol.h" | ||||
| #define namespace namespace_ | ||||
| #include "ext-background-effect-v1-client-protocol.h" | ||||
| #include "text-input-unstable-v3-client-protocol.h" | ||||
| #include "wlr-layer-shell-unstable-v1-client-protocol.h" | ||||
| #include <libportal/settings.h> | ||||
| #undef namespace | ||||
| } | ||||
| #include <SQLiteCpp/SQLiteCpp.h> | ||||
| #include <wayland-client.h> | ||||
| #include <wayland-egl.h> | ||||
| #include <xkbcommon/xkbcommon.h> | ||||
|  | ||||
| #include "Cache.hpp" | ||||
| #include "Config.hpp" | ||||
| #include "IconRegistry.hpp" | ||||
| #include "ImGui.hpp" | ||||
| #include "TextRenderer.hpp" | ||||
| #include "Theme.hpp" | ||||
| #include "common.hpp" | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| struct TypingBuffer : std::pmr::vector<u32> { | ||||
| 	void push_utf8(char const *s); | ||||
| }; | ||||
| @@ -46,11 +58,20 @@ private: | ||||
| 	auto destroy_layer_surface() -> void; | ||||
| 	auto ensure_egl_surface() -> void; | ||||
| 	auto update_blur_region() -> void; | ||||
| 	auto process_pending_text_input() -> void; | ||||
| 	auto update_text_input_state( | ||||
| 	    std::pmr::string const &text, usize id, Rectangle field_rect) -> void; | ||||
| 	auto theme() const -> ColorScheme const & | ||||
| 	{ | ||||
| 		return m_themes[m_active_theme]; | ||||
| 	} | ||||
|  | ||||
| 	auto clipboard() const -> std::pmr::string const & | ||||
| 	{ | ||||
| 		return m_clipboard_cache; | ||||
| 	} | ||||
| 	auto clipboard(std::string_view const &str) -> void; | ||||
|  | ||||
| 	static void on_settings_changed(XdpSettings * /*self*/, char const *ns, | ||||
| 	    char const *key, GVariant * /*value*/, gpointer data); | ||||
|  | ||||
| @@ -67,7 +88,15 @@ private: | ||||
| 		ext_background_effect_surface_v1 *eff {}; | ||||
| 		org_kde_kwin_blur_manager *kde_blur_mgr {}; | ||||
| 		org_kde_kwin_blur *kde_blur {}; | ||||
| 		zwp_text_input_manager_v3 *text_input_mgr {}; | ||||
| 		zwp_text_input_v3 *text_input {}; | ||||
| 		wl_data_device_manager *ddm {}; | ||||
| 		wl_data_device *ddev {}; | ||||
| 		wl_data_offer *curr_offer {}; | ||||
| 		wl_data_source *curr_source {}; | ||||
| 	} m_wayland; | ||||
| 	std::pmr::string m_clipboard_cache; | ||||
| 	u32 m_last_serial { 0 }; | ||||
|  | ||||
| 	struct { | ||||
| 		EGLDisplay edpy { EGL_NO_DISPLAY }; | ||||
| @@ -102,13 +131,11 @@ private: | ||||
| 		{ | ||||
| 			if (!xkb_state_v) | ||||
| 				return false; | ||||
| 			for (auto k : held) { | ||||
| 				if (xkb_state_key_get_one_sym( | ||||
| 			return std::any_of(held.begin(), held.end(), [&](u32 const k) { | ||||
| 				return (xkb_state_key_get_one_sym( | ||||
| 				            xkb_state_v, static_cast<xkb_keycode_t>(k + 8)) | ||||
| 				    == sym) | ||||
| 					return true; | ||||
| 			} | ||||
| 			return false; | ||||
| 				    == sym); | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		auto is_sym_pressed(xkb_keysym_t sym) const -> bool | ||||
| @@ -139,16 +166,78 @@ private: | ||||
| 		} | ||||
| 	} m_kbd; | ||||
|  | ||||
| 	std::optional<TextRenderer> m_tr { std::nullopt }; | ||||
| 	std::shared_ptr<TextRenderer> m_tr { nullptr }; | ||||
| 	FontHandle m_font; | ||||
| 	std::shared_ptr<ImGui> m_gui { nullptr }; | ||||
|  | ||||
| 	struct { | ||||
| 		bool supported { false }; | ||||
| 		bool seat_focus { false }; | ||||
| 		bool enabled { false }; | ||||
| 		bool pending_done { false }; | ||||
| 		uint32_t pending_serial { 0 }; | ||||
| 		uint32_t sent_serial { 0 }; | ||||
|  | ||||
| 		std::pmr::string *bound_text { nullptr }; | ||||
| 		usize bound_id { 0 }; | ||||
| 		Rectangle bound_rect {}; | ||||
|  | ||||
| 		struct { | ||||
| 			bool has_preedit { false }; | ||||
| 			std::string preedit_text; | ||||
| 			int cursor_begin { 0 }; | ||||
| 			int cursor_end { 0 }; | ||||
| 			bool has_commit { false }; | ||||
| 			std::string commit_text; | ||||
| 			bool has_delete { false }; | ||||
| 			uint32_t before { 0 }; | ||||
| 			uint32_t after { 0 }; | ||||
| 		} pending; | ||||
|  | ||||
| 		std::string last_surrounding; | ||||
| 		int last_cursor { 0 }; | ||||
| 		int last_anchor { 0 }; | ||||
| 		Rectangle last_cursor_rect {}; | ||||
| 		bool last_cursor_visible { false }; | ||||
| 		bool surrounding_dirty { false }; | ||||
| 	} m_ime; | ||||
|  | ||||
| 	auto get_texture(std::filesystem::path const &path) -> Texture2D const & | ||||
| 	{ | ||||
| 		if (m_textures.contains(path)) { | ||||
| 			return m_textures[path]; | ||||
| 		} | ||||
| 		auto fname = path.c_str(); | ||||
| 		assert(fname); | ||||
| 		TraceLog(LOG_INFO, std::format("loading texture at {}", fname).c_str()); | ||||
| 		auto const tex = LoadTexture(fname); | ||||
| 		assert(IsTextureValid(tex)); | ||||
| 		m_textures[path] = tex; | ||||
| 		return m_textures[path]; | ||||
| 	} | ||||
|  | ||||
| 	void execute_command(bool terminal, std::string_view const command); | ||||
|  | ||||
| 	// NOTE: Canonicalize first! | ||||
| 	std::unordered_map<std::filesystem::path, Texture2D> m_textures; | ||||
|  | ||||
| 	enum_array<Theme, ColorScheme> m_themes { make_default_themes() }; | ||||
| 	Theme m_active_theme { Theme::Light }; | ||||
| 	IconRegistry m_ir; | ||||
| 	std::optional<Cache> m_cache; | ||||
| 	Config m_config; | ||||
|  | ||||
| 	int m_win_w { 800 }; | ||||
| 	int m_win_h { 600 }; | ||||
| 	bool m_running { true }; | ||||
| 	bool m_visible { true }; | ||||
|  | ||||
| 	Color m_accent_color { 127, 127, 255, 255 }; | ||||
|  | ||||
| 	std::filesystem::path m_data_home_dir {}; | ||||
| 	std::filesystem::path m_config_home_dir {}; | ||||
| 	std::shared_ptr<SQLite::Database> m_db {}; | ||||
| 	int m_sfd { -1 }; | ||||
| }; | ||||
|  | ||||
| } // namespace Waylight | ||||
|   | ||||
							
								
								
									
										435
									
								
								src/Cache.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										435
									
								
								src/Cache.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,435 @@ | ||||
| #include "Cache.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <numeric> | ||||
| #include <ranges> | ||||
| #include <unordered_map> | ||||
|  | ||||
| #include <SQLiteCpp/Statement.h> | ||||
| #include <SQLiteCpp/Transaction.h> | ||||
| #include <mini/ini.h> | ||||
| #include <raylib.h> | ||||
|  | ||||
| #include "common.hpp" | ||||
| #include <sys/inotify.h> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| void replace_all( | ||||
|     std::string &str, std::string const &from, std::string const &to) | ||||
| { | ||||
| 	if (from.empty()) | ||||
| 		return; | ||||
| 	size_t pos = 0; | ||||
| 	while ((pos = str.find(from, pos)) != std::string::npos) { | ||||
| 		str.replace(pos, from.size(), to); | ||||
| 		pos += to.size(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| Cache::Cache(std::shared_ptr<SQLite::Database> db) | ||||
|     : m_db(db) | ||||
| { | ||||
| 	{ | ||||
| 		auto const *env { getenv("XDG_DATA_DIRS") }; | ||||
| 		if (env && *env) { | ||||
| 			std::ranges::copy(std::string_view(env) | std::views::split(':') | ||||
| 			        | std::views::transform([](auto &&s) { | ||||
| 				          return std::filesystem::path(s.begin(), s.end()) | ||||
| 				              / "applications"; | ||||
| 			          }) | ||||
| 			        | std::views::filter([](auto &&p) { | ||||
| 				          if (!std::filesystem::is_directory(p)) | ||||
| 					          return false; | ||||
| 				          if (std::filesystem::directory_iterator(p) | ||||
| 				              == std::filesystem::directory_iterator {}) | ||||
| 					          return false; | ||||
| 				          return true; | ||||
| 			          }), | ||||
| 			    std::back_inserter(m_app_dirs)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	load(); | ||||
|  | ||||
| 	auto total = std::accumulate(m_app_dirs.begin(), m_app_dirs.end(), | ||||
| 	    static_cast<usize>(0), [](usize acc, auto &&dir) { | ||||
| 		    return acc | ||||
| 		        + std::count_if(std::filesystem::directory_iterator(dir), | ||||
| 		            std::filesystem::directory_iterator {}, [](auto &&entry) { | ||||
| 			            return entry.is_regular_file() | ||||
| 			                && entry.path().extension() == ".desktop"; | ||||
| 		            }); | ||||
| 	    }); | ||||
| 	if (total != m_apps.size()) { | ||||
| 		rescan(); | ||||
| 	} | ||||
|  | ||||
| 	TraceLog(LOG_DEBUG, std::format("Applications in cache:").c_str()); | ||||
| 	for (auto const &app : m_apps) { | ||||
| 		TraceLog(LOG_DEBUG, | ||||
| 		    std::format("{}:", app.desktop_entry_path.string()).c_str()); | ||||
| 		if (app.comment) | ||||
| 			TraceLog( | ||||
| 			    LOG_DEBUG, std::format(" - Comment: {}", *app.comment).c_str()); | ||||
| 		if (app.path) | ||||
| 			TraceLog(LOG_DEBUG, std::format(" - Path: {}", *app.path).c_str()); | ||||
| 		TraceLog( | ||||
| 		    LOG_DEBUG, std::format(" - Terminal: {}", app.terminal).c_str()); | ||||
| 		TraceLog( | ||||
| 		    LOG_DEBUG, std::format(" - NoDisplay: {}", app.no_display).c_str()); | ||||
| 		TraceLog(LOG_DEBUG, std::format(" - Actions:").c_str()); | ||||
| 		for (auto const &action : app.actions) { | ||||
| 			TraceLog( | ||||
| 			    LOG_DEBUG, std::format("   - Name: {}", action.name).c_str()); | ||||
| 			if (action.exec) | ||||
| 				TraceLog(LOG_DEBUG, | ||||
| 				    std::format("     Exec: {}", *action.exec).c_str()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for (auto const &dir : m_app_dirs) { | ||||
| 		m_inotify.watch_path_recursively(dir); | ||||
| 	} | ||||
|  | ||||
| 	m_inotify.set_callback([this](FileWatchEvent const &event) { | ||||
| 		auto const mask = event.mask; | ||||
| 		if (mask & IN_Q_OVERFLOW) { | ||||
| 			rescan(); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (mask & IN_ISDIR) { | ||||
| 			rescan(); | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		if (mask & (IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM | ||||
| 		        | IN_MOVED_TO | IN_CLOSE_WRITE)) { | ||||
| 			if (event.path.extension() == ".desktop") { | ||||
| 				rescan(); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	m_inotify_thread = std::thread([this]() { m_inotify.run(); }); | ||||
| } | ||||
|  | ||||
| Cache::~Cache() | ||||
| { | ||||
| 	m_inotify.stop(); | ||||
| 	if (m_inotify_thread.joinable()) | ||||
| 		m_inotify_thread.join(); | ||||
| } | ||||
|  | ||||
| void Cache::rescan() | ||||
| { | ||||
| 	m_apps.clear(); | ||||
|  | ||||
| 	int id = 0; | ||||
| 	for (auto const &dir : m_app_dirs) { | ||||
| 		for (auto const &file : std::filesystem::directory_iterator(dir)) { | ||||
| 			if (!file.is_regular_file()) | ||||
| 				continue; | ||||
|  | ||||
| 			if (file.path().extension() != ".desktop") | ||||
| 				continue; | ||||
|  | ||||
| 			mINI::INIFile ini_file(file.path()); | ||||
| 			mINI::INIStructure ini; | ||||
| 			ini_file.read(ini); | ||||
|  | ||||
| 			constexpr auto read_action = [&](std::string const | ||||
| 			                                     &desktop_file_uri, | ||||
| 			                                 mINI::INIMap<std::string> const | ||||
| 			                                     §ion) { | ||||
| 				auto const name = section.get("Name"); | ||||
| 				auto const icon = [&]() | ||||
| 				    -> std::optional< | ||||
| 				        std::variant<std::filesystem::path, std::string>> { | ||||
| 					if (section.has("Icon")) { | ||||
| 						auto const icon_name = section.get("Icon"); | ||||
| 						if (!icon_name.empty()) { | ||||
| 							if (icon_name[0] == '/') { | ||||
| 								return std::filesystem::path(icon_name); | ||||
| 							} else { | ||||
| 								return icon_name; | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 					return std::nullopt; | ||||
| 				}(); | ||||
|  | ||||
| 				return ApplicationCache::Action { | ||||
| 					.name = name, | ||||
| 					.exec = [&]() -> std::optional<std::string> { | ||||
| 					    if (section.has("Exec")) { | ||||
| 						    auto s = section.get("Exec"); | ||||
| 						    if (!s.empty()) { | ||||
| 							    // Either deprecated or not used... | ||||
| 							    replace_all(s, "%f", ""); | ||||
| 							    replace_all(s, "%F", ""); | ||||
| 							    replace_all(s, "%u", ""); | ||||
| 							    replace_all(s, "%U", ""); | ||||
| 							    replace_all(s, "%d", ""); | ||||
| 							    replace_all(s, "%D", ""); | ||||
| 							    replace_all(s, "%n", ""); | ||||
| 							    replace_all(s, "%N", ""); | ||||
| 							    replace_all(s, "%v", ""); | ||||
| 							    replace_all(s, "%M", ""); | ||||
|  | ||||
| 							    replace_all(s, "%c", name); | ||||
| 							    if (icon) { | ||||
| 								    if (auto const p | ||||
| 								        = std::get_if<std::filesystem::path>( | ||||
| 								            &*icon)) { | ||||
| 									    replace_all(s, "%i", | ||||
| 									        "--icon '" + p->string() + "'"); | ||||
| 								    } else if (auto const n | ||||
| 								        = std::get_if<std::string>(&*icon)) { | ||||
| 									    replace_all( | ||||
| 									        s, "%i", "--icon '" + *n + "'"); | ||||
| 								    } else { | ||||
| 									    replace_all(s, "%i", ""); | ||||
| 								    } | ||||
| 							    } else { | ||||
| 								    replace_all(s, "%i", ""); | ||||
| 							    } | ||||
| 							    replace_all(s, "%k", "'" + desktop_file_uri); | ||||
| 							    return s; | ||||
| 						    } | ||||
| 					    } | ||||
| 					    return std::nullopt; | ||||
| 					}(), | ||||
| 					.icon = icon, | ||||
| 				}; | ||||
| 			}; | ||||
|  | ||||
| 			m_apps.push_back({ | ||||
| 			    .id = id++, | ||||
| 			    .desktop_entry_path = file.path(), | ||||
| 			    .type = | ||||
| 			        [&]() { | ||||
| 				        auto const type_str { ini["Desktop Entry"].get( | ||||
| 					        "Type") }; | ||||
| 				        auto type { ApplicationCache::Type::Application }; | ||||
| 				        if (type_str == "Application") | ||||
| 					        type = ApplicationCache::Type::Application; | ||||
| 				        else if (type_str == "Link") | ||||
| 					        type = ApplicationCache::Type::Link; | ||||
| 				        else if (type_str == "Directory") | ||||
| 					        type = ApplicationCache::Type::Directory; | ||||
| 				        return type; | ||||
| 			        }(), | ||||
| 			    .terminal = | ||||
| 			        [&]() { | ||||
| 				        if (ini["Desktop Entry"].has("Terminal")) { | ||||
| 					        return ini["Desktop Entry"]["Terminal"] == "true" | ||||
| 					            ? true | ||||
| 					            : false; | ||||
| 				        } | ||||
| 				        return false; | ||||
| 			        }(), | ||||
| 			    .no_display = | ||||
| 			        [&]() { | ||||
| 				        if (ini["Desktop Entry"].has("NoDisplay")) { | ||||
| 					        return ini["Desktop Entry"]["NoDisplay"] == "true" | ||||
| 					            ? true | ||||
| 					            : false; | ||||
| 				        } | ||||
| 				        return false; | ||||
| 			        }(), | ||||
| 			    .path = [&]() -> std::optional<std::string> { | ||||
| 				    if (ini["Desktop Entry"].has("Path")) { | ||||
| 					    return ini["Desktop Entry"]["Path"]; | ||||
| 				    } | ||||
| 				    return std::nullopt; | ||||
| 			    }(), | ||||
| 			    .comment = [&]() -> std::optional<std::string> { | ||||
| 				    if (ini["Desktop Entry"].has("Comment")) { | ||||
| 					    return ini["Desktop Entry"]["Comment"]; | ||||
| 				    } | ||||
| 				    return std::nullopt; | ||||
| 			    }(), | ||||
| 			    .actions = | ||||
| 			        [&]() { | ||||
| 				        std::vector<ApplicationCache::Action> actions; | ||||
| 				        for (auto const &[_, v] : ini) { | ||||
| 					        try { | ||||
| 						        auto const action = read_action( | ||||
| 						            std::filesystem::canonical(file.path()) | ||||
| 						                .string(), | ||||
| 						            v); | ||||
| 						        actions.push_back(action); | ||||
| 					        } catch (...) { | ||||
| 					        } | ||||
| 				        } | ||||
| 				        return actions; | ||||
| 			        }(), | ||||
| 			    .dbus_activatable = | ||||
| 			        [&]() { | ||||
| 				        if (ini["Desktop Entry"].has("DBusActivatable")) { | ||||
| 					        return ini["Desktop Entry"]["DBusActivatable"] | ||||
| 					                == "true" | ||||
| 					            ? true | ||||
| 					            : false; | ||||
| 				        } | ||||
| 				        return false; | ||||
| 			        }(), | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	dump(); | ||||
| } | ||||
|  | ||||
| void Cache::dump() | ||||
| { | ||||
| 	SQLite::Transaction tx(*m_db); | ||||
|  | ||||
| 	SQLite::Statement(*m_db, "DELETE FROM ApplicationCache").exec(); | ||||
| 	SQLite::Statement(*m_db, "DELETE FROM ApplicationActionCache").exec(); | ||||
|  | ||||
| 	try { | ||||
| 		SQLite::Statement( | ||||
| 		    *m_db, "DELETE FROM sqlite_sequence WHERE name='ApplicationCache'") | ||||
| 		    .exec(); | ||||
| 		SQLite::Statement(*m_db, | ||||
| 		    "DELETE FROM sqlite_sequence WHERE name='ApplicationActionCache'") | ||||
| 		    .exec(); | ||||
| 	} catch (std::exception const &) { | ||||
| 	} | ||||
|  | ||||
| 	SQLite::Statement ins_app(*m_db, | ||||
| 	    "INSERT INTO ApplicationCache(type, desktop_entry_path, terminal, " | ||||
| 	    "no_display, path, comment, dbus_activatable) VALUES (?,?,?,?,?,?,?)"); | ||||
|  | ||||
| 	SQLite::Statement ins_act(*m_db, | ||||
| 	    "INSERT INTO ApplicationActionCache(id_app, name, exec, icon) VALUES " | ||||
| 	    "(?,?,?,?)"); | ||||
|  | ||||
| 	for (auto &app : m_apps) { | ||||
| 		ins_app.reset(); | ||||
| 		ins_app.clearBindings(); | ||||
| 		ins_app.bind(1, static_cast<int>(app.type)); | ||||
| 		ins_app.bind(2, app.desktop_entry_path.string()); | ||||
| 		ins_app.bind(3, app.terminal ? 1 : 0); | ||||
| 		ins_app.bind(4, app.no_display ? 1 : 0); | ||||
| 		if (app.path) | ||||
| 			ins_app.bind(5, *app.path); | ||||
| 		else | ||||
| 			ins_app.bind(5); | ||||
| 		if (app.comment) | ||||
| 			ins_app.bind(6, *app.comment); | ||||
| 		else | ||||
| 			ins_app.bind(6); | ||||
| 		ins_app.bind(7, app.dbus_activatable ? 1 : 0); | ||||
| 		ins_app.exec(); | ||||
|  | ||||
| 		app.id = m_db->getLastInsertRowid(); | ||||
|  | ||||
| 		for (auto const &action : app.actions) { | ||||
| 			ins_act.reset(); | ||||
| 			ins_act.clearBindings(); | ||||
| 			ins_act.bind(1, app.id); | ||||
| 			ins_act.bind(2, action.name); | ||||
| 			if (action.exec) | ||||
| 				ins_act.bind(3, *action.exec); | ||||
| 			else | ||||
| 				ins_act.bind(3); | ||||
|  | ||||
| 			if (action.icon) { | ||||
| 				std::string str; | ||||
| 				if (auto const *s = std::get_if<std::string>(&*action.icon)) { | ||||
| 					str = *s; | ||||
| 				} else if (auto const *p | ||||
| 				    = std::get_if<std::filesystem::path>(&*action.icon)) { | ||||
| 					str = std::filesystem::canonical(*p).string(); | ||||
| 				} | ||||
| 				ins_act.bind(4, str); | ||||
| 			} else { | ||||
| 				ins_act.bind(4); | ||||
| 			} | ||||
| 			ins_act.exec(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	tx.commit(); | ||||
| } | ||||
|  | ||||
| void Cache::load() | ||||
| { | ||||
| 	m_apps.clear(); | ||||
|  | ||||
| 	SQLite::Statement get_apps(*m_db, | ||||
| 	    "SELECT id, type, desktop_entry_path, terminal, no_display, path, " | ||||
| 	    "comment, dbus_activatable " | ||||
| 	    "FROM ApplicationCache"); | ||||
|  | ||||
| 	std::unordered_map<std::int64_t, std::size_t> id_to_index; | ||||
|  | ||||
| 	while (get_apps.executeStep()) { | ||||
| 		ApplicationCache app {}; | ||||
| 		app.id = get_apps.getColumn(0).getInt64(); | ||||
| 		app.type = static_cast<ApplicationCache::Type>( | ||||
| 		    get_apps.getColumn(1).getInt()); | ||||
| 		app.desktop_entry_path | ||||
| 		    = std::filesystem::path(get_apps.getColumn(2).getString()); | ||||
| 		app.terminal = get_apps.getColumn(3).getInt() != 0; | ||||
| 		app.no_display = get_apps.getColumn(4).getInt() != 0; | ||||
|  | ||||
| 		if (!get_apps.getColumn(5).isNull()) | ||||
| 			app.path = std::string(get_apps.getColumn(5).getString()); | ||||
| 		else | ||||
| 			app.path.reset(); | ||||
|  | ||||
| 		if (!get_apps.getColumn(6).isNull()) | ||||
| 			app.comment = std::string(get_apps.getColumn(6).getString()); | ||||
| 		else | ||||
| 			app.comment.reset(); | ||||
|  | ||||
| 		app.dbus_activatable = get_apps.getColumn(7).getInt() != 0; | ||||
|  | ||||
| 		id_to_index.emplace(app.id, m_apps.size()); | ||||
| 		m_apps.push_back(std::move(app)); | ||||
| 	} | ||||
|  | ||||
| 	if (m_apps.empty()) | ||||
| 		return; | ||||
|  | ||||
| 	SQLite::Statement get_actions(*m_db, | ||||
| 	    "SELECT id_app, name, exec, icon " | ||||
| 	    "FROM ApplicationActionCache " | ||||
| 	    "ORDER BY id_app"); | ||||
|  | ||||
| 	while (get_actions.executeStep()) { | ||||
| 		auto id_app = get_actions.getColumn(0).getInt64(); | ||||
| 		auto it = id_to_index.find(id_app); | ||||
| 		if (it == id_to_index.end()) | ||||
| 			continue; | ||||
|  | ||||
| 		ApplicationCache::Action action {}; | ||||
| 		action.name = std::string(get_actions.getColumn(1).getString()); | ||||
|  | ||||
| 		if (!get_actions.getColumn(2).isNull()) | ||||
| 			action.exec = std::string(get_actions.getColumn(2).getString()); | ||||
| 		else | ||||
| 			action.exec.reset(); | ||||
|  | ||||
| 		if (!get_actions.getColumn(3).isNull()) { | ||||
| 			auto const str = get_actions.getColumn(3).getString(); | ||||
| 			if (str.at(0) == '/') { | ||||
| 				action.icon | ||||
| 				    = std::filesystem::canonical(std::filesystem::path(str)); | ||||
| 			} else { | ||||
| 				action.icon = str; | ||||
| 			} | ||||
| 		} else { | ||||
| 			action.icon.reset(); | ||||
| 		} | ||||
|  | ||||
| 		m_apps[it->second].actions.push_back(std::move(action)); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
							
								
								
									
										66
									
								
								src/Cache.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/Cache.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <filesystem> | ||||
| #include <optional> | ||||
| #include <string> | ||||
| #include <thread> | ||||
| #include <variant> | ||||
| #include <vector> | ||||
|  | ||||
| #include "InotifyWatcher.hpp" | ||||
| #include <SQLiteCpp/Database.h> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| struct ApplicationCache { | ||||
| 	enum class Type { | ||||
| 		Application, | ||||
| 		Link, | ||||
| 		Directory, | ||||
| 	}; | ||||
|  | ||||
| 	struct Action { | ||||
| 		std::string name; | ||||
| 		// May not exist if DBusActivable=true | ||||
| 		std::optional<std::string> exec; | ||||
| 		// Freedesktop Desktop Entry Spec 11.2 Table 3 says: | ||||
| 		// | ||||
| 		//   If the name is an absolute path, the given file will be used. | ||||
| 		//   If the name is not an absolute path, the algorithm described in | ||||
| 		//   the Icon Theme Specification will be used to locate the icon. | ||||
| 		// | ||||
| 		// Thus, when deserializing, we will just check if it starts with / | ||||
| 		// to determine type. | ||||
| 		std::optional<std::variant<std::filesystem::path, std::string>> icon; | ||||
| 	}; | ||||
|  | ||||
| 	int id; | ||||
| 	std::filesystem::path desktop_entry_path; | ||||
|  | ||||
| 	Type type { Type::Application }; | ||||
| 	bool terminal { false }; | ||||
| 	bool no_display { false }; | ||||
| 	std::optional<std::string> path; | ||||
| 	std::optional<std::string> comment; | ||||
| 	std::vector<Action> actions; // There should always be at least 1. | ||||
| 	bool dbus_activatable {};    // Unimplemented for now. | ||||
| }; | ||||
|  | ||||
| struct Cache { | ||||
| 	explicit Cache(std::shared_ptr<SQLite::Database> db); | ||||
| 	~Cache(); | ||||
|  | ||||
| 	void rescan(); | ||||
| 	void dump(); | ||||
| 	void load(); | ||||
|  | ||||
| private: | ||||
| 	std::vector<ApplicationCache> m_apps; | ||||
| 	std::vector<std::filesystem::path> m_app_dirs; | ||||
| 	std::shared_ptr<SQLite::Database> m_db; | ||||
|  | ||||
| 	InotifyWatcher m_inotify; | ||||
| 	std::thread m_inotify_thread; | ||||
| }; | ||||
|  | ||||
| } // namespace Waylight | ||||
							
								
								
									
										47
									
								
								src/Config.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/Config.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| #include "Config.hpp" | ||||
|  | ||||
| #include <filesystem> | ||||
| #include <fstream> | ||||
| #include <print> | ||||
|  | ||||
| #include <toml++/toml.hpp> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| void Config::write(std::filesystem::path const &path) | ||||
| { | ||||
| 	std::ofstream f(path); | ||||
| 	if (!f) { | ||||
| 		throw std::runtime_error("Failed to open config file for writing"); | ||||
| 	} | ||||
| 	std::println(f, "[settings]"); | ||||
| 	std::println(f, "terminal_cmdline=\"{}\"", terminal_cmdline); | ||||
| } | ||||
|  | ||||
| auto Config::load(std::filesystem::path const &config_dir_path) -> Config const | ||||
| { | ||||
| 	if (!std::filesystem::is_directory(config_dir_path)) | ||||
| 		throw std::runtime_error("Provided path is not a directory!"); | ||||
|  | ||||
| 	Config cfg {}; | ||||
|  | ||||
| 	std::filesystem::path path_config { config_dir_path / "config.toml" }; | ||||
| 	if (!std::filesystem::is_regular_file(path_config)) { | ||||
| 		try { | ||||
| 			std::filesystem::remove_all(path_config); | ||||
| 		} catch (std::exception const &e) { | ||||
| 		} | ||||
| 		cfg.write(path_config); | ||||
| 	} | ||||
|  | ||||
| 	std::println("Config file: {}", path_config.string()); | ||||
| 	auto const tbl { toml::parse_file(path_config.string()) }; | ||||
| 	auto const terminal_cmdline { tbl["settings"]["terminal_cmdline"].value_or( | ||||
| 		"kitty -c") }; | ||||
|  | ||||
| 	cfg.terminal_cmdline = terminal_cmdline; | ||||
|  | ||||
| 	return cfg; | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
							
								
								
									
										17
									
								
								src/Config.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/Config.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <filesystem> | ||||
| #include <string> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| struct Config { | ||||
| 	std::string terminal_cmdline { "kitty" }; | ||||
|  | ||||
| 	void write(std::filesystem::path const &path); | ||||
|  | ||||
| 	static auto load(std::filesystem::path const &config_dir_path) | ||||
| 	    -> Config const; | ||||
| }; | ||||
|  | ||||
| } // namespace Waylight | ||||
							
								
								
									
										449
									
								
								src/IconRegistry.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										449
									
								
								src/IconRegistry.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,449 @@ | ||||
| #include "IconRegistry.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cassert> | ||||
| #include <cmath> | ||||
| #include <cstdlib> | ||||
| #include <cstring> | ||||
| #include <filesystem> | ||||
| #include <ranges> | ||||
| #include <stdexcept> | ||||
|  | ||||
| #include <gio/gio.h> | ||||
| #include <lunasvg.h> | ||||
| #include <mini/ini.h> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| static inline auto color_to_string(Color const &c) -> std::string | ||||
| { | ||||
| 	auto const r { c.r / 255.0 }, g { c.g / 255.0 }, b { c.b / 255.0 }; | ||||
|  | ||||
| 	auto const maxv { std::fmax(r, std::fmax(g, b)) }; | ||||
| 	auto const minv { std::fmin(r, std::fmin(g, b)) }; | ||||
| 	auto const d { maxv - minv }; | ||||
|  | ||||
| 	double h = 0.0; | ||||
| 	if (d > 1e-6) { | ||||
| 		if (maxv == r) | ||||
| 			h = 60.0 * std::fmod(((g - b) / d), 6.0); | ||||
| 		else if (maxv == g) | ||||
| 			h = 60.0 * (((b - r) / d) + 2.0); | ||||
| 		else | ||||
| 			h = 60.0 * (((r - g) / d) + 4.0); | ||||
| 	} | ||||
| 	if (h < 0.0) | ||||
| 		h += 360.0; | ||||
|  | ||||
| 	if (h >= 345 || h < 15) | ||||
| 		return "red"; | ||||
| 	if (h < 45) | ||||
| 		return "orange"; | ||||
| 	if (h < 70) | ||||
| 		return "yellow"; | ||||
| 	if (h < 170) | ||||
| 		return "green"; | ||||
| 	if (h < 200) | ||||
| 		return "teal"; | ||||
| 	if (h < 250) | ||||
| 		return "cyan"; | ||||
| 	if (h < 290) | ||||
| 		return "blue"; | ||||
| 	if (h < 330) | ||||
| 		return "purple"; | ||||
| 	return "pink"; | ||||
| } | ||||
|  | ||||
| static auto detect_desktop_environment() -> std::string const | ||||
| { | ||||
| 	if (auto const de { getenv("XDG_CURRENT_DESKTOP") }) | ||||
| 		return de; | ||||
| 	if (auto const sess { getenv("DESKTOP_SESSION") }) | ||||
| 		return sess; | ||||
| 	return "unknown"; | ||||
| } | ||||
|  | ||||
| static auto kde_get_theme() -> std::string const | ||||
| { | ||||
| 	std::string home { getenv("HOME") ? getenv("HOME") : "" }; | ||||
|  | ||||
| 	std::string const paths[] { | ||||
| 		home + "/.config/kdeglobals", | ||||
| 		home + "/.config/kdedefaults/kdeglobals", | ||||
| 	}; | ||||
|  | ||||
| 	for (auto const &p : paths) { | ||||
| 		std::ifstream f(p); | ||||
| 		if (!f) | ||||
| 			continue; | ||||
| 		std::string line; | ||||
| 		auto in_icons { false }; | ||||
| 		while (std::getline(f, line)) { | ||||
| 			if (line == "[Icons]") { | ||||
| 				in_icons = true; | ||||
| 				continue; | ||||
| 			} | ||||
| 			if (line.starts_with("[")) | ||||
| 				in_icons = false; | ||||
| 			if (in_icons && line.starts_with("Theme=")) | ||||
| 				return line.substr(strlen("Theme=")); | ||||
| 		} | ||||
| 	} | ||||
| 	return {}; | ||||
| } | ||||
|  | ||||
| static auto other_get_theme() -> std::string | ||||
| { | ||||
| 	char const *schema_id { "org.gnome.desktop.interface" }; | ||||
| 	char const *key { "icon-theme" }; | ||||
|  | ||||
| 	GSettingsSchemaSource *src { g_settings_schema_source_get_default() }; | ||||
| 	if (!src) | ||||
| 		return {}; | ||||
|  | ||||
| 	GSettingsSchema *schema { g_settings_schema_source_lookup( | ||||
| 		src, schema_id, TRUE) }; | ||||
| 	if (!schema) | ||||
| 		return {}; | ||||
|  | ||||
| 	GSettings *settings { g_settings_new_full(schema, nullptr, nullptr) }; | ||||
| 	g_settings_schema_unref(schema); | ||||
| 	if (!settings) | ||||
| 		return {}; | ||||
|  | ||||
| 	gchar *cstr { g_settings_get_string(settings, key) }; | ||||
| 	g_object_unref(settings); | ||||
| 	if (!cstr) | ||||
| 		return {}; | ||||
|  | ||||
| 	std::string theme { cstr }; | ||||
| 	g_free(cstr); | ||||
| 	return theme; | ||||
| } | ||||
|  | ||||
| static auto get_current_icon_theme() -> std::optional<std::string> const | ||||
| { | ||||
| 	auto de { detect_desktop_environment() }; | ||||
| 	std::transform(de.begin(), de.end(), de.begin(), ::tolower); | ||||
|  | ||||
| 	if (de.find("kde") != std::string::npos | ||||
| 	    || de.find("plasma") != std::string::npos) { | ||||
| 		if (auto const t = kde_get_theme(); !t.empty()) { | ||||
| 			return t; | ||||
| 		} | ||||
| 	} else { | ||||
| 		if (auto const t = other_get_theme(); !t.empty()) { | ||||
| 			return t; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return std::nullopt; | ||||
| } | ||||
|  | ||||
| auto IconTheme::lookup(std::string_view const name, | ||||
|     std::optional<int> optimal_size) const -> Icon const | ||||
| { | ||||
| 	for (auto const &dir : m_directories) { | ||||
| 		if (optimal_size && *optimal_size < dir.size) | ||||
| 			continue; | ||||
|  | ||||
| 		for (auto const &dir_entry : | ||||
| 		    std::filesystem::recursive_directory_iterator(dir.path)) { | ||||
| 			if (!dir_entry.is_regular_file()) | ||||
| 				continue; | ||||
|  | ||||
| 			if (dir_entry.path().stem() != name) | ||||
| 				continue; | ||||
|  | ||||
| 			// This can be derived from the image filename. | ||||
| 			// But we probably won't need it either way... | ||||
| 			if (dir_entry.path().extension() == ".icon") | ||||
| 				continue; | ||||
|  | ||||
| 			if (dir_entry.path().extension() == ".svg") { | ||||
| 				auto const document { lunasvg::Document::loadFromFile( | ||||
| 					dir_entry.path()) }; | ||||
| 				if (!document) { | ||||
| 					throw std::runtime_error("Failed to load SVG file"); | ||||
| 				} | ||||
|  | ||||
| 				auto const bitmap { document->renderToBitmap() }; | ||||
| 				if (bitmap.width() == 0 || bitmap.height() == 0) | ||||
| 					continue; | ||||
|  | ||||
| 				std::vector<unsigned char> rgba( | ||||
| 				    bitmap.width() * bitmap.height() * 4); | ||||
| 				for (size_t i = 0, px = bitmap.width() * bitmap.height(); | ||||
| 				    i < px; ++i) { | ||||
| 					auto *src { bitmap.data() }; | ||||
|  | ||||
| 					uint8_t b { src[i * 4 + 0] }; | ||||
| 					uint8_t g { src[i * 4 + 1] }; | ||||
| 					uint8_t r { src[i * 4 + 2] }; | ||||
| 					uint8_t a { src[i * 4 + 3] }; | ||||
|  | ||||
| 					if (a != 0) { | ||||
| 						r = (uint8_t)std::min( | ||||
| 						    255, (int)((r * 255 + a / 2) / a)); | ||||
| 						g = (uint8_t)std::min( | ||||
| 						    255, (int)((g * 255 + a / 2) / a)); | ||||
| 						b = (uint8_t)std::min( | ||||
| 						    255, (int)((b * 255 + a / 2) / a)); | ||||
| 					} | ||||
|  | ||||
| 					rgba[i * 4 + 0] = r; | ||||
| 					rgba[i * 4 + 1] = g; | ||||
| 					rgba[i * 4 + 2] = b; | ||||
| 					rgba[i * 4 + 3] = a; | ||||
| 				} | ||||
|  | ||||
| 				Image const img { | ||||
| 					.data = rgba.data(), | ||||
| 					.width = bitmap.width(), | ||||
| 					.height = bitmap.height(), | ||||
| 					.mipmaps = 1, | ||||
| 					.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, | ||||
| 				}; | ||||
|  | ||||
| 				auto const tex { LoadTextureFromImage(img) }; | ||||
| 				if (!IsTextureValid(tex)) { | ||||
| 					throw std::runtime_error( | ||||
| 					    "Failed to load texture from image"); | ||||
| 				} | ||||
|  | ||||
| 				Icon const icon(dir_entry.path(), tex, dir.size); | ||||
| 				return icon; | ||||
| 			} else { | ||||
| 				auto const tex { LoadTexture(dir_entry.path().c_str()) }; | ||||
| 				if (!IsTextureValid(tex)) { | ||||
| 					throw std::runtime_error( | ||||
| 					    "Failed to load texture from file"); | ||||
| 				} | ||||
|  | ||||
| 				Icon const icon(dir_entry.path(), tex, dir.size); | ||||
| 				return icon; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (optimal_size) { | ||||
| 		// We failed to find a icon big enough, try again with smaller sizes | ||||
| 		// than our optimal. | ||||
| 		return lookup(name, std::nullopt); | ||||
| 	} | ||||
|  | ||||
| 	throw std::runtime_error( | ||||
| 	    std::format("Failed to find icon `{}` in theme!", name)); | ||||
| } | ||||
|  | ||||
| IconTheme::IconTheme(std::filesystem::path const &themes_directory_path) | ||||
| { | ||||
| 	for (auto const &dir : | ||||
| 	    std::filesystem::directory_iterator(themes_directory_path)) { | ||||
| 		if (!dir.is_directory()) | ||||
| 			continue; | ||||
|  | ||||
| 		auto const index_path = dir.path() / "index.theme"; | ||||
| 		if (!std::filesystem::is_regular_file(index_path)) | ||||
| 			continue; | ||||
|  | ||||
| 		m_names.push_back(dir.path().filename().string()); | ||||
|  | ||||
| 		mINI::INIFile ini_file(index_path); | ||||
| 		mINI::INIStructure ini; | ||||
| 		ini_file.read(ini); | ||||
|  | ||||
| 		auto const &inherits { ini["Icon Theme"]["Inherits"] }; | ||||
| 		std::ranges::copy(std::string_view(inherits) | std::views::split(',') | ||||
| 		        | std::views::transform( | ||||
| 		            [](auto &&s) { return std::string(s.begin(), s.end()); }), | ||||
| 		    std::back_inserter(m_inherits)); | ||||
|  | ||||
| 		auto const &directories { ini["Icon Theme"]["Directories"] }; | ||||
| 		for (auto const &&dir_entry : directories | std::views::split(',')) { | ||||
| 			auto const dir_entry_str { std::string( | ||||
| 				dir_entry.begin(), dir_entry.end()) }; | ||||
| 			auto const path { std::filesystem::path(dir_entry_str) }; | ||||
| 			auto const path_actual { dir.path() / path }; | ||||
|  | ||||
| 			if (!std::filesystem::is_directory(path_actual)) | ||||
| 				continue; | ||||
|  | ||||
| 			auto const &type_raw { ini[dir_entry_str]["Type"] }; | ||||
| 			DirectoryEntry::Type type; | ||||
| 			if (type_raw == "Fixed") { | ||||
| 				type = DirectoryEntry::Type::Fixed; | ||||
| 			} else if (type_raw == "Scalable") { | ||||
| 				type = DirectoryEntry::Type::Scalable; | ||||
| 			} else if (type_raw == "Threshold") { | ||||
| 				type = DirectoryEntry::Type::Threshold; | ||||
| 			} else { | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			auto const &context_raw { ini[dir_entry_str]["Context"] }; | ||||
| 			DirectoryEntry::Context context; | ||||
| 			if (context_raw == "Actions") { | ||||
| 				context = DirectoryEntry::Context::Actions; | ||||
| 			} else if (context_raw == "Devices") { | ||||
| 				context = DirectoryEntry::Context::Devices; | ||||
| 			} else if (context_raw == "FileSystems") { | ||||
| 				context = DirectoryEntry::Context::FileSystems; | ||||
| 			} else if (context_raw == "MimeTypes") { | ||||
| 				context = DirectoryEntry::Context::MimeTypes; | ||||
| 			} else if (context_raw == "Places") { | ||||
| 				context = DirectoryEntry::Context::Places; | ||||
| 			} else { | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			int size { std::atoi(ini[dir_entry_str]["Size"].c_str()) }; | ||||
| 			if (size == 0) { | ||||
| 				if (type == DirectoryEntry::Type::Scalable) { | ||||
| 					int minSize | ||||
| 					    = std::atoi(ini[dir_entry_str]["MinSize"].c_str()); | ||||
| 					int maxSize | ||||
| 					    = std::atoi(ini[dir_entry_str]["MaxSize"].c_str()); | ||||
| 					size = std::max(minSize, maxSize); | ||||
| 				} | ||||
| 				if (size == 0) | ||||
| 					continue; | ||||
| 			} | ||||
|  | ||||
| 			m_directories.push_back({ | ||||
| 			    .path = path_actual, | ||||
| 			    .size = size, | ||||
| 			    .type = type, | ||||
| 			    .context = context, | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		// Sort by biggest sizes first. This is important for the lookup | ||||
| 		// algorithm. Mess with this, change that. | ||||
| 		std::sort(m_directories.begin(), m_directories.end(), | ||||
| 		    [](DirectoryEntry const &a, DirectoryEntry const &b) { | ||||
| 			    return a.size > b.size; | ||||
| 		    }); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| IconRegistry::IconRegistry() | ||||
|     : m_preferred_theme(get_current_icon_theme()) | ||||
| { | ||||
| 	std::vector<std::filesystem::path> theme_directory_paths; | ||||
|  | ||||
| 	{ | ||||
| 		auto const *env { getenv("HOME") }; | ||||
| 		if (env && *env) { | ||||
| 			theme_directory_paths.push_back( | ||||
| 			    std::filesystem::path(env) / ".icons"); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	{ | ||||
| 		auto const *env { getenv("XDG_DATA_DIRS") }; | ||||
| 		if (env && *env) { | ||||
| 			std::ranges::copy(std::string_view(env) | std::views::split(':') | ||||
| 			        | std::views::transform([](auto &&s) { | ||||
| 				          return std::filesystem::path(s.begin(), s.end()) | ||||
| 				              / "icons"; | ||||
| 			          }) | ||||
| 			        | std::views::filter([](auto &&p) { | ||||
| 				          if (!std::filesystem::is_directory(p)) | ||||
| 					          return false; | ||||
| 				          if (std::filesystem::directory_iterator(p) | ||||
| 				              == std::filesystem::directory_iterator {}) | ||||
| 					          return false; | ||||
| 				          return true; | ||||
| 			          }), | ||||
| 			    std::back_inserter(theme_directory_paths)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	{ | ||||
| 		std::array<std::filesystem::path, 3> const paths { | ||||
| 			"/usr/share/pixmaps", | ||||
| 			"/usr/local/share/icons", | ||||
| 			"/usr/share/icons", | ||||
| 		}; | ||||
| 		std::copy_if(paths.begin(), paths.end(), theme_directory_paths.begin(), | ||||
| 		    [](auto const path) { return std::filesystem::exists(path); }); | ||||
| 	} | ||||
|  | ||||
| 	for (auto &&path : theme_directory_paths | ||||
| 	        | std::views::filter([](std::filesystem::path const &path) { | ||||
| 		          return std::filesystem::is_directory(path); | ||||
| 	          })) { | ||||
| 		try { | ||||
| 			m_themes.push_back(IconTheme(path)); | ||||
| 		} catch (...) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (m_themes.empty()) { | ||||
| 		throw std::runtime_error("Could not find any icon themes."); | ||||
| 	} | ||||
|  | ||||
| 	if (m_preferred_theme) { | ||||
| 		TraceLog(LOG_INFO, | ||||
| 		    std::format("Preferred theme: {}", *m_preferred_theme).c_str()); | ||||
|  | ||||
| 		std::stable_partition( | ||||
| 		    m_themes.begin(), m_themes.end(), [&](auto const &t) { | ||||
| 			    return std::any_of(t.names().begin(), t.names().end(), | ||||
| 			        [&](auto const &e) { return e == *m_preferred_theme; }); | ||||
| 		    }); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| auto IconRegistry::lookup(std::string_view const name, | ||||
|     std::optional<int> optimal_size, bool symbolic, std::optional<Color> color) | ||||
|     -> Icon const & | ||||
| { | ||||
| 	if (!color && m_color) | ||||
| 		color = m_color; | ||||
|  | ||||
| 	std::string color_name {}; | ||||
| 	if (color) { | ||||
| 		auto const col { color_to_string(*color) }; | ||||
| 		if (!col.empty()) { | ||||
| 			color_name = "-" + col; | ||||
| 		} | ||||
| 	} | ||||
| 	if (symbolic) { | ||||
| 		try { | ||||
| 			auto const n { std::format("{}{}-symbolic", color_name, name) }; | ||||
| 			return lookup_cached(n, optimal_size); | ||||
| 		} catch (...) { | ||||
| 			return lookup(name, optimal_size, false, color); | ||||
| 		} | ||||
| 	} else { | ||||
| 		return lookup_cached( | ||||
| 		    std::string_view(std::format("{}{}", name, color_name)), | ||||
| 		    optimal_size); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| auto IconRegistry::lookup_cached(std::string_view const name, | ||||
|     std::optional<int> optimal_size) -> Icon const & | ||||
| { | ||||
| 	std::string name_s(name); | ||||
| 	if (m_cached_icons.contains(name_s)) { | ||||
| 		auto const &icon = m_cached_icons.at(name_s); | ||||
| 		if (optimal_size && icon.size() >= *optimal_size) | ||||
| 			return icon; | ||||
| 	} | ||||
|  | ||||
| 	for (auto const &theme : m_themes) { | ||||
| 		try { | ||||
| 			auto const icon = theme.lookup(name, optimal_size); | ||||
| 			m_cached_icons.insert_or_assign(name_s, icon); | ||||
| 			return m_cached_icons.at(name_s); | ||||
| 		} catch (...) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	throw std::runtime_error(std::format("Failed to find icon `{}`!", name)); | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
							
								
								
									
										93
									
								
								src/IconRegistry.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/IconRegistry.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <filesystem> | ||||
| #include <optional> | ||||
| #include <span> | ||||
| #include <unordered_map> | ||||
| #include <vector> | ||||
|  | ||||
| #include <raylib.h> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| struct Icon { | ||||
| 	Icon(std::filesystem::path path, Texture2D texture, int size) | ||||
| 	    : m_path(path) | ||||
| 	    , m_texture(texture) | ||||
| 	    , m_size(size) | ||||
| 	{ | ||||
| 	} | ||||
|  | ||||
| 	constexpr auto path() const -> std::filesystem::path const & | ||||
| 	{ | ||||
| 		return m_path; | ||||
| 	} | ||||
| 	constexpr auto texture() const -> Texture2D const & { return m_texture; } | ||||
| 	constexpr auto size() const -> int const & { return m_size; } | ||||
|  | ||||
| private: | ||||
| 	std::filesystem::path m_path; | ||||
| 	Texture2D m_texture; | ||||
| 	int m_size { 0 }; | ||||
| }; | ||||
|  | ||||
| struct IconTheme { | ||||
| 	explicit IconTheme(std::filesystem::path const &themes_directory_path); | ||||
| 	~IconTheme() = default; | ||||
|  | ||||
| 	constexpr auto inherits() const -> std::span<std::string const> | ||||
| 	{ | ||||
| 		return std::span { m_inherits }; | ||||
| 	} | ||||
| 	auto lookup(std::string_view const name, | ||||
| 	    std::optional<int> optimal_size = std::nullopt) const -> Icon const; | ||||
| 	auto names() const -> std::vector<std::string> const & { return m_names; } | ||||
|  | ||||
| private: | ||||
| 	struct DirectoryEntry { | ||||
| 		enum class Type { | ||||
| 			Fixed, | ||||
| 			Scalable, | ||||
| 			Threshold, | ||||
| 		}; | ||||
|  | ||||
| 		enum class Context { | ||||
| 			Actions, | ||||
| 			Devices, | ||||
| 			FileSystems, | ||||
| 			MimeTypes, | ||||
| 			Places, | ||||
| 		}; | ||||
|  | ||||
| 		std::filesystem::path path; | ||||
| 		int size; | ||||
| 		Type type; | ||||
| 		Context context; | ||||
| 	}; | ||||
|  | ||||
| 	std::vector<std::string> m_names; | ||||
| 	std::vector<std::string> m_inherits; | ||||
| 	std::vector<DirectoryEntry> m_directories; | ||||
| }; | ||||
|  | ||||
| struct IconRegistry { | ||||
| 	IconRegistry(); | ||||
| 	~IconRegistry() = default; | ||||
|  | ||||
| 	auto lookup(std::string_view const name, | ||||
| 	    std::optional<int> optimal_size = std::nullopt, bool symbolic = false, | ||||
| 	    std::optional<Color> color = std::nullopt) -> Icon const &; | ||||
| 	auto color(std::optional<Color> const &color) { m_color = color; } | ||||
|  | ||||
| private: | ||||
| 	std::optional<Color> m_color { std::nullopt }; | ||||
|  | ||||
| 	auto lookup_cached(std::string_view const name, | ||||
| 	    std::optional<int> optimal_size) -> Icon const &; | ||||
|  | ||||
| 	std::vector<IconTheme> m_themes; | ||||
| 	std::unordered_map<std::string, Icon> m_cached_icons; | ||||
| 	std::optional<std::string> m_preferred_theme; | ||||
| }; | ||||
|  | ||||
| } // namespace Waylight | ||||
							
								
								
									
										792
									
								
								src/ImGui.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										792
									
								
								src/ImGui.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,792 @@ | ||||
| #include "ImGui.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cassert> | ||||
| #include <cctype> | ||||
| #include <cmath> | ||||
| #include <format> | ||||
| #include <string> | ||||
| #include <string_view> | ||||
| #include <vector> | ||||
|  | ||||
| #include <raylib.h> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| struct CodepointSpan { | ||||
| 	u32 codepoint {}; | ||||
| 	usize start {}; | ||||
| 	usize end {}; | ||||
| }; | ||||
|  | ||||
| constexpr inline float px_pos(float x) { return std::floor(x + 0.5f); } | ||||
| constexpr inline float px_w(float w) { return std::ceil(w); } | ||||
|  | ||||
| constexpr auto utf8_rune_from_first(char const *s) -> u32 | ||||
| { | ||||
| 	u8 b0 = static_cast<u8>(s[0]); | ||||
| 	if (b0 < 0x80) | ||||
| 		return b0; | ||||
|  | ||||
| 	if ((b0 & 0xE0) == 0xC0) | ||||
| 		return ((b0 & 0x1F) << 6) | (static_cast<u8>(s[1]) & 0x3F); | ||||
|  | ||||
| 	if ((b0 & 0xF0) == 0xE0) | ||||
| 		return ((b0 & 0x0F) << 12) | ((static_cast<u8>(s[1]) & 0x3F) << 6) | ||||
| 		    | (static_cast<u8>(s[2]) & 0x3F); | ||||
|  | ||||
| 	if ((b0 & 0xF8) == 0xF0) | ||||
| 		return ((b0 & 0x07) << 18) | ((static_cast<u8>(s[1]) & 0x3F) << 12) | ||||
| 		    | ((static_cast<u8>(s[2]) & 0x3F) << 6) | ||||
| 		    | (static_cast<u8>(s[3]) & 0x3F); | ||||
|  | ||||
| 	return 0xFFFD; | ||||
| } | ||||
|  | ||||
| constexpr auto decode_utf8(std::string_view text) -> std::vector<CodepointSpan> | ||||
| { | ||||
| 	std::vector<CodepointSpan> spans; | ||||
| 	usize i = 0; | ||||
| 	spans.reserve(text.size()); | ||||
|  | ||||
| 	while (i < text.size()) { | ||||
| 		u8 b = static_cast<u8>(text[i]); | ||||
| 		usize len = 1; | ||||
|  | ||||
| 		if (b < 0x80) | ||||
| 			len = 1; | ||||
| 		else if ((b & 0xE0) == 0xC0) | ||||
| 			len = 2; | ||||
| 		else if ((b & 0xF0) == 0xE0) | ||||
| 			len = 3; | ||||
| 		else if ((b & 0xF8) == 0xF0) | ||||
| 			len = 4; | ||||
|  | ||||
| 		if (i + len > text.size()) | ||||
| 			len = 1; | ||||
|  | ||||
| 		u32 cp = utf8_rune_from_first(text.data() + i); | ||||
| 		spans.push_back({ cp, i, i + len }); | ||||
| 		i += len; | ||||
| 	} | ||||
|  | ||||
| 	return spans; | ||||
| } | ||||
|  | ||||
| auto encode_utf8(u32 cp) -> std::string | ||||
| { | ||||
| 	char buf[5] = { 0, 0, 0, 0, 0 }; | ||||
| 	int len = 0; | ||||
| 	if (cp <= 0x7F) { | ||||
| 		buf[len++] = static_cast<char>(cp); | ||||
| 	} else if (cp <= 0x7FF) { | ||||
| 		buf[len++] = static_cast<char>(0xC0 | (cp >> 6)); | ||||
| 		buf[len++] = static_cast<char>(0x80 | (cp & 0x3F)); | ||||
| 	} else if (cp <= 0xFFFF) { | ||||
| 		if (cp >= 0xD800 && cp <= 0xDFFF) | ||||
| 			return {}; | ||||
| 		buf[len++] = static_cast<char>(0xE0 | (cp >> 12)); | ||||
| 		buf[len++] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); | ||||
| 		buf[len++] = static_cast<char>(0x80 | (cp & 0x3F)); | ||||
| 	} else if (cp <= 0x10FFFF) { | ||||
| 		buf[len++] = static_cast<char>(0xF0 | (cp >> 18)); | ||||
| 		buf[len++] = static_cast<char>(0x80 | ((cp >> 12) & 0x3F)); | ||||
| 		buf[len++] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); | ||||
| 		buf[len++] = static_cast<char>(0x80 | (cp & 0x3F)); | ||||
| 	} else { | ||||
| 		return {}; | ||||
| 	} | ||||
| 	return std::string(buf, len); | ||||
| } | ||||
|  | ||||
| auto rune_index_for_byte(std::string_view text, usize byte_offset) -> int | ||||
| { | ||||
| 	auto spans { decode_utf8(text) }; | ||||
| 	int idx = 0; | ||||
| 	for (auto const &span : spans) { | ||||
| 		if (span.start >= byte_offset) | ||||
| 			break; | ||||
| 		idx++; | ||||
| 	} | ||||
| 	if (byte_offset >= text.size()) | ||||
| 		idx = static_cast<int>(spans.size()); | ||||
| 	return idx; | ||||
| } | ||||
|  | ||||
| auto clamp_preedit_index(int value, usize text_size) -> usize | ||||
| { | ||||
| 	if (value < 0) | ||||
| 		return 0; | ||||
| 	auto const as_size { static_cast<usize>(value) }; | ||||
| 	return std::min(as_size, text_size); | ||||
| } | ||||
|  | ||||
| constexpr float HORIZONTAL_PADDING = 6.0f; | ||||
| constexpr float VERTICAL_PADDING = 4.0f; | ||||
| constexpr double CARET_BLINK_INTERVAL = 0.5; | ||||
| constexpr float CARET_DESCENT_FRACTION = 0.25f; | ||||
|  | ||||
| } // namespace | ||||
|  | ||||
| ImGui::ImGui(std::shared_ptr<TextRenderer> text_renderer) | ||||
|     : m_text_renderer(text_renderer) | ||||
| { | ||||
| } | ||||
|  | ||||
| void ImGui::begin(u32 const rune, bool ctrl, bool shift, | ||||
|     std::string_view const clipboard, | ||||
|     std::function<void(std::string_view const &)> clipboard_set) | ||||
| { | ||||
| 	m_rune = rune; | ||||
| 	m_ctrl = ctrl; | ||||
| 	m_shift = shift; | ||||
| 	m_clipboard = clipboard; | ||||
| 	m_clipboard_set = clipboard_set; | ||||
| } | ||||
|  | ||||
| void ImGui::end() | ||||
| { | ||||
| 	m_rune = false; | ||||
| 	m_ctrl = false; | ||||
| 	m_shift = false; | ||||
| 	m_clipboard = {}; | ||||
| 	m_clipboard_set = nullptr; | ||||
| } | ||||
|  | ||||
| void ImGui::set_font(FontHandle font) { m_font = font; } | ||||
|  | ||||
| auto ImGui::focused_text_input() const -> std::optional<usize> | ||||
| { | ||||
| 	if (m_focused_id == 0) | ||||
| 		return std::nullopt; | ||||
| 	return m_focused_id; | ||||
| } | ||||
|  | ||||
| auto ImGui::text_input_surrounding(usize id, std::pmr::string const &str) const | ||||
|     -> std::optional<TextInputSurrounding> | ||||
| { | ||||
| 	auto it { m_ti_states.find(id) }; | ||||
| 	if (it == m_ti_states.end()) | ||||
| 		return std::nullopt; | ||||
| 	TextInputSurrounding info; | ||||
| 	info.text.assign(str.data(), str.size()); | ||||
| 	info.caret_byte = std::min(it->second.caret_byte, str.size()); | ||||
| 	info.cursor = static_cast<int>(info.caret_byte); | ||||
| 	info.anchor = static_cast<int>(info.caret_byte); | ||||
| 	return info; | ||||
| } | ||||
|  | ||||
| auto ImGui::text_input_cursor(usize id) const -> std::optional<TextInputCursor> | ||||
| { | ||||
| 	auto it { m_ti_states.find(id) }; | ||||
| 	if (it == m_ti_states.end()) | ||||
| 		return std::nullopt; | ||||
| 	TextInputCursor cursor; | ||||
| 	cursor.rect = it->second.caret_rect; | ||||
| 	cursor.visible | ||||
| 	    = it->second.caret_visible && !it->second.preedit_cursor_hidden; | ||||
| 	return cursor; | ||||
| } | ||||
|  | ||||
| void ImGui::ime_commit_text(std::pmr::string &str, std::string_view text) | ||||
| { | ||||
| 	if (m_focused_id == 0) | ||||
| 		return; | ||||
| 	auto it { m_ti_states.find(m_focused_id) }; | ||||
| 	if (it == m_ti_states.end()) | ||||
| 		return; | ||||
| 	auto &state { it->second }; | ||||
| 	usize insert_pos = std::min(state.caret_byte, str.size()); | ||||
| 	if (!text.empty()) | ||||
| 		str.insert(insert_pos, text); | ||||
| 	state.caret_byte = insert_pos + text.size(); | ||||
| 	std::string_view const view(str.data(), str.size()); | ||||
| 	state.current_rune_idx = rune_index_for_byte(view, state.caret_byte); | ||||
| 	state.caret_timer = 0.0; | ||||
| 	state.caret_visible = true; | ||||
| 	state.external_change = true; | ||||
| } | ||||
|  | ||||
| void ImGui::ime_delete_surrounding( | ||||
|     std::pmr::string &str, usize before, usize after) | ||||
| { | ||||
| 	if (m_focused_id == 0) | ||||
| 		return; | ||||
| 	auto it { m_ti_states.find(m_focused_id) }; | ||||
| 	if (it == m_ti_states.end()) | ||||
| 		return; | ||||
| 	auto &state { it->second }; | ||||
| 	usize caret_byte { std::min(state.caret_byte, str.size()) }; | ||||
| 	usize start { before > caret_byte ? 0 : caret_byte - before }; | ||||
| 	usize end { std::min(caret_byte + after, str.size()) }; | ||||
| 	if (end > start) { | ||||
| 		str.erase(start, end - start); | ||||
| 		state.caret_byte = start; | ||||
| 		std::string_view const view(str.data(), str.size()); | ||||
| 		state.current_rune_idx = rune_index_for_byte(view, state.caret_byte); | ||||
| 		state.caret_timer = 0.0; | ||||
| 		state.caret_visible = true; | ||||
| 		state.external_change = true; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void ImGui::ime_set_preedit(std::string text, int cursor_begin, int cursor_end) | ||||
| { | ||||
| 	if (m_focused_id == 0) | ||||
| 		return; | ||||
| 	auto it { m_ti_states.find(m_focused_id) }; | ||||
| 	if (it == m_ti_states.end()) | ||||
| 		return; | ||||
| 	auto &state { it->second }; | ||||
| 	state.preedit_text = std::move(text); | ||||
| 	state.preedit_cursor_hidden = (cursor_begin == -1 && cursor_end == -1); | ||||
| 	usize const size = state.preedit_text.size(); | ||||
| 	if (state.preedit_cursor_hidden) { | ||||
| 		state.preedit_cursor_begin = 0; | ||||
| 		state.preedit_cursor_end = 0; | ||||
| 	} else { | ||||
| 		auto begin_clamped { clamp_preedit_index(cursor_begin, size) }; | ||||
| 		auto end_clamped { clamp_preedit_index(cursor_end, size) }; | ||||
| 		state.preedit_cursor_begin = static_cast<int>(begin_clamped); | ||||
| 		state.preedit_cursor_end = static_cast<int>(end_clamped); | ||||
| 	} | ||||
| 	state.preedit_active | ||||
| 	    = !state.preedit_text.empty() || !state.preedit_cursor_hidden; | ||||
| 	if (state.preedit_active) { | ||||
| 		state.caret_timer = 0.0; | ||||
| 		state.caret_visible = !state.preedit_cursor_hidden; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void ImGui::ime_clear_preedit() | ||||
| { | ||||
| 	if (m_focused_id == 0) | ||||
| 		return; | ||||
| 	auto it { m_ti_states.find(m_focused_id) }; | ||||
| 	if (it == m_ti_states.end()) | ||||
| 		return; | ||||
| 	auto &state { it->second }; | ||||
| 	state.preedit_text.clear(); | ||||
| 	state.preedit_cursor_begin = 0; | ||||
| 	state.preedit_cursor_end = 0; | ||||
| 	state.preedit_active = false; | ||||
| 	state.preedit_cursor_hidden = false; | ||||
| 	state.caret_visible = true; | ||||
| 	state.caret_timer = 0.0; | ||||
| } | ||||
|  | ||||
| size_t utf8_length(std::string_view const &s) | ||||
| { | ||||
| 	size_t count = std::count_if( | ||||
| 	    s.begin(), s.end(), [](auto const &c) { return (c & 0xC0) != 0x80; }); | ||||
| 	return count; | ||||
| } | ||||
|  | ||||
| auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, | ||||
|     TextInputOptions options) -> std::bitset<2> | ||||
| { | ||||
| 	assert(id != 0); | ||||
| 	assert( | ||||
| 	    m_font.has_value() && "ImGui font must be set before using text input"); | ||||
|  | ||||
| 	bool submitted { false }; | ||||
| 	bool changed { false }; | ||||
|  | ||||
| 	auto &state { m_ti_states[id] }; | ||||
| 	assert(!options.multiline && "Multiline not yet implemented."); | ||||
|  | ||||
| 	if (m_focused_id == 0) | ||||
| 		m_focused_id = id; | ||||
|  | ||||
| 	if (style().font_size > rec.height) { | ||||
| 		TraceLog(LOG_WARNING, | ||||
| 		    std::format("Text size for text input {} is bigger than height ({} " | ||||
| 		                "> {}). Clipping will occur.", | ||||
| 		        id, style().font_size, rec.height) | ||||
| 		        .c_str()); | ||||
| 	} | ||||
|  | ||||
| 	std::string_view str_view(str.data(), str.size()); | ||||
| 	auto spans { decode_utf8(str_view) }; | ||||
|  | ||||
| 	auto is_space = [](u32 cp) -> bool { | ||||
| 		if (cp == '\n' || cp == '\r' || cp == '\t' || cp == '\v' || cp == '\f') | ||||
| 			return true; | ||||
| 		if (cp <= 0x7F) | ||||
| 			return std::isspace(static_cast<unsigned char>(cp)) != 0; | ||||
| 		return false; | ||||
| 	}; | ||||
|  | ||||
| 	auto clamp_cursor = [&]() -> usize { | ||||
| 		int const max_idx = static_cast<int>(spans.size()); | ||||
| 		state.current_rune_idx = std::clamp(state.current_rune_idx, 0, max_idx); | ||||
| 		if (state.current_rune_idx == max_idx) | ||||
| 			return str.size(); | ||||
| 		return spans[(usize)state.current_rune_idx].start; | ||||
| 	}; | ||||
|  | ||||
| 	usize caret_byte = clamp_cursor(); | ||||
|  | ||||
| 	auto refresh_spans = [&]() { | ||||
| 		str_view = std::string_view(str.data(), str.size()); | ||||
| 		spans = decode_utf8(str_view); | ||||
| 		caret_byte = clamp_cursor(); | ||||
| 	}; | ||||
|  | ||||
| 	auto erase_range = [&](usize byte_begin, usize byte_end) { | ||||
| 		if (byte_end > byte_begin && byte_begin < str.size()) { | ||||
| 			str.erase(byte_begin, byte_end - byte_begin); | ||||
| 			changed = true; | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	auto selection_range_bytes | ||||
| 	    = [&](int a_idx, int b_idx) -> std::pair<usize, usize> { | ||||
| 		int lo = std::max(0, std::min(a_idx, b_idx)); | ||||
| 		int hi = std::max(0, std::max(a_idx, b_idx)); | ||||
| 		usize byte_begin | ||||
| 		    = (lo >= (int)spans.size()) ? str.size() : spans[(usize)lo].start; | ||||
| 		usize byte_end | ||||
| 		    = (hi >= (int)spans.size()) ? str.size() : spans[(usize)hi].start; | ||||
| 		return { byte_begin, byte_end }; | ||||
| 	}; | ||||
|  | ||||
| 	auto erase_selection_if_any = [&]() -> bool { | ||||
| 		if (!state.has_selection(state.current_rune_idx)) | ||||
| 			return false; | ||||
| 		auto [b, e] = selection_range_bytes( | ||||
| 		    state.sel_anchor_idx, state.current_rune_idx); | ||||
| 		if (e > b) { | ||||
| 			str.erase(b, e - b); | ||||
| 			changed = true; | ||||
| 			refresh_spans(); | ||||
| 			state.current_rune_idx = rune_index_for_byte(str_view, b); | ||||
| 			state.clear_selection(); | ||||
| 		} | ||||
| 		return true; | ||||
| 	}; | ||||
|  | ||||
| 	auto move_left_word = [&]() { | ||||
| 		while (state.current_rune_idx > 0 | ||||
| 		    && is_space(spans[(usize)(state.current_rune_idx - 1)].codepoint)) | ||||
| 			state.current_rune_idx--; | ||||
| 		while (state.current_rune_idx > 0 | ||||
| 		    && !is_space(spans[(usize)(state.current_rune_idx - 1)].codepoint)) | ||||
| 			state.current_rune_idx--; | ||||
| 	}; | ||||
| 	auto move_right_word = [&]() { | ||||
| 		while (state.current_rune_idx < (int)spans.size() | ||||
| 		    && is_space(spans[(usize)state.current_rune_idx].codepoint)) | ||||
| 			state.current_rune_idx++; | ||||
| 		while (state.current_rune_idx < (int)spans.size() | ||||
| 		    && !is_space(spans[(usize)state.current_rune_idx].codepoint)) | ||||
| 			state.current_rune_idx++; | ||||
| 	}; | ||||
|  | ||||
| 	bool caret_activity = false; | ||||
|  | ||||
| 	if (m_focused_id == id && m_rune != 0) { | ||||
| 		bool request_refresh = false; | ||||
|  | ||||
| 		auto handle_backspace = [&]() { | ||||
| 			if (state.current_rune_idx <= 0 | ||||
| 			    || state.current_rune_idx > (int)spans.size()) | ||||
| 				return; | ||||
| 			if (m_ctrl) { | ||||
| 				int idx = state.current_rune_idx, scan = idx - 1; | ||||
| 				while (scan >= 0 && is_space(spans[(usize)scan].codepoint)) | ||||
| 					scan--; | ||||
| 				while (scan >= 0 && !is_space(spans[(usize)scan].codepoint)) | ||||
| 					scan--; | ||||
| 				int start_idx = std::max(scan + 1, 0); | ||||
| 				usize b = spans[(usize)start_idx].start; | ||||
| 				usize e = (idx >= (int)spans.size()) ? str.size() | ||||
| 				                                     : spans[(usize)idx].start; | ||||
| 				erase_range(b, e); | ||||
| 				state.current_rune_idx = start_idx; | ||||
| 			} else { | ||||
| 				auto const &prev = spans[(usize)(state.current_rune_idx - 1)]; | ||||
| 				erase_range(prev.start, prev.end); | ||||
| 				state.current_rune_idx--; | ||||
| 			} | ||||
| 			request_refresh = true; | ||||
| 		}; | ||||
|  | ||||
| 		auto handle_delete = [&]() { | ||||
| 			if (state.current_rune_idx < 0 | ||||
| 			    || state.current_rune_idx >= (int)spans.size()) { | ||||
| 				if (!m_ctrl) | ||||
| 					return; | ||||
| 			} | ||||
| 			int idx = state.current_rune_idx; | ||||
| 			if (m_ctrl) { | ||||
| 				int scan = idx; | ||||
| 				while (scan < (int)spans.size() | ||||
| 				    && is_space(spans[(usize)scan].codepoint)) | ||||
| 					scan++; | ||||
| 				while (scan < (int)spans.size() | ||||
| 				    && !is_space(spans[(usize)scan].codepoint)) | ||||
| 					scan++; | ||||
| 				usize b = (idx < (int)spans.size()) ? spans[(usize)idx].start | ||||
| 				                                    : str.size(); | ||||
| 				usize e = (scan < (int)spans.size()) ? spans[(usize)scan].start | ||||
| 				                                     : str.size(); | ||||
| 				erase_range(b, e); | ||||
| 			} else if (idx < (int)spans.size()) { | ||||
| 				auto const &curr = spans[(usize)idx]; | ||||
| 				erase_range(curr.start, curr.end); | ||||
| 			} | ||||
| 			request_refresh = true; | ||||
| 		}; | ||||
|  | ||||
| 		bool extend = m_shift; | ||||
|  | ||||
| 		switch (m_rune) { | ||||
| 		case 1: // Left | ||||
| 			if (!extend) | ||||
| 				state.clear_selection(); | ||||
| 			if (state.current_rune_idx > 0) { | ||||
| 				if (extend && state.sel_anchor_idx == -1) | ||||
| 					state.sel_anchor_idx = state.current_rune_idx; | ||||
| 				if (m_ctrl) | ||||
| 					move_left_word(); | ||||
| 				else | ||||
| 					state.current_rune_idx--; | ||||
| 				caret_byte = clamp_cursor(); | ||||
| 			} | ||||
| 			break; | ||||
| 		case 4: // Right | ||||
| 			if (!extend) | ||||
| 				state.clear_selection(); | ||||
| 			if (state.current_rune_idx < (int)spans.size()) { | ||||
| 				if (extend && state.sel_anchor_idx == -1) | ||||
| 					state.sel_anchor_idx = state.current_rune_idx; | ||||
| 				if (m_ctrl) | ||||
| 					move_right_word(); | ||||
| 				else | ||||
| 					state.current_rune_idx++; | ||||
| 				caret_byte = clamp_cursor(); | ||||
| 			} | ||||
| 			break; | ||||
| 		case 3: // Up -> home | ||||
| 			if (!extend) | ||||
| 				state.clear_selection(); | ||||
| 			if (extend && state.sel_anchor_idx == -1) | ||||
| 				state.sel_anchor_idx = state.current_rune_idx; | ||||
| 			state.current_rune_idx = 0; | ||||
| 			caret_byte = clamp_cursor(); | ||||
| 			break; | ||||
| 		case 2: // Down -> end | ||||
| 			if (!extend) | ||||
| 				state.clear_selection(); | ||||
| 			if (extend && state.sel_anchor_idx == -1) | ||||
| 				state.sel_anchor_idx = state.current_rune_idx; | ||||
| 			state.current_rune_idx = (int)spans.size(); | ||||
| 			caret_byte = clamp_cursor(); | ||||
| 			break; | ||||
| 		case 8: // Backspace | ||||
| 			if (erase_selection_if_any()) { | ||||
| 				request_refresh = true; | ||||
| 				break; | ||||
| 			} | ||||
| 			handle_backspace(); | ||||
| 			break; | ||||
| 		case 0x7F: // Delete | ||||
| 			if (erase_selection_if_any()) { | ||||
| 				request_refresh = true; | ||||
| 				break; | ||||
| 			} | ||||
| 			handle_delete(); | ||||
| 			break; | ||||
| 		case 'a': | ||||
| 			if (m_ctrl) { | ||||
| 				state.sel_anchor_idx = 0; | ||||
| 				state.current_rune_idx = (int)spans.size(); | ||||
| 				request_refresh = true; | ||||
| 				break; | ||||
| 			} | ||||
| 			[[fallthrough]]; | ||||
| 		case 'c': | ||||
| 			if (m_ctrl) { | ||||
| 				if (state.has_selection(state.current_rune_idx) | ||||
| 				    && m_clipboard_set) { | ||||
| 					auto [b, e] = selection_range_bytes( | ||||
| 					    state.sel_anchor_idx, state.current_rune_idx); | ||||
| 					m_clipboard_set(std::string_view(str.data() + b, e - b)); | ||||
| 				} | ||||
| 				break; | ||||
| 			} | ||||
| 			[[fallthrough]]; | ||||
| 		case 'x': | ||||
| 			if (m_ctrl) { | ||||
| 				if (state.has_selection(state.current_rune_idx) | ||||
| 				    && m_clipboard_set) { | ||||
| 					auto [b, e] = selection_range_bytes( | ||||
| 					    state.sel_anchor_idx, state.current_rune_idx); | ||||
| 					m_clipboard_set(std::string_view(str.data() + b, e - b)); | ||||
| 					str.erase(b, e - b); | ||||
| 					changed = true; | ||||
| 					request_refresh = true; | ||||
| 					refresh_spans(); | ||||
| 					state.current_rune_idx = rune_index_for_byte(str_view, b); | ||||
| 					state.clear_selection(); | ||||
| 				} | ||||
| 				break; | ||||
| 			} | ||||
| 			[[fallthrough]]; | ||||
| 		case 'v': | ||||
| 			if (m_ctrl && !m_clipboard.empty()) { | ||||
| 				erase_selection_if_any(); | ||||
| 				if (!options.multiline) { | ||||
| 					std::string clip2; | ||||
| 					clip2.reserve(m_clipboard.size()); | ||||
| 					std::copy_if(m_clipboard.begin(), m_clipboard.end(), | ||||
| 					    clip2.begin(), | ||||
| 					    [](char ch) { return ch != '\n' && ch != '\r'; }); | ||||
| 					str.insert(caret_byte, clip2); | ||||
| 					state.current_rune_idx += (int)utf8_length(clip2); | ||||
| 				} else { | ||||
| 					str.insert(caret_byte, m_clipboard); | ||||
| 					state.current_rune_idx += (int)utf8_length(m_clipboard); | ||||
| 				} | ||||
| 				changed = true; | ||||
| 				request_refresh = true; | ||||
| 				break; | ||||
| 			} else { | ||||
| 				goto insert_printable; | ||||
| 			} | ||||
| 			break; | ||||
| 		case '\r': | ||||
| 		case '\n': | ||||
| 			if (options.multiline) { | ||||
| 				erase_selection_if_any(); | ||||
| 				auto encoded { encode_utf8('\n') }; | ||||
| 				if (!encoded.empty()) { | ||||
| 					str.insert(caret_byte, encoded); | ||||
| 					state.current_rune_idx++; | ||||
| 					changed = true; | ||||
| 					request_refresh = true; | ||||
| 				} | ||||
| 			} else { | ||||
| 				submitted = true; | ||||
| 			} | ||||
| 			break; | ||||
| 		default: | ||||
| 		insert_printable: | ||||
| 			if (m_rune >= 0x20) { | ||||
| 				erase_selection_if_any(); | ||||
| 				auto encoded { encode_utf8(m_rune) }; | ||||
| 				if (!encoded.empty()) { | ||||
| 					str.insert(caret_byte, encoded); | ||||
| 					state.current_rune_idx++; | ||||
| 					changed = true; | ||||
| 					request_refresh = true; | ||||
| 				} | ||||
| 			} | ||||
| 			break; | ||||
| 		} | ||||
|  | ||||
| 		if (request_refresh) | ||||
| 			refresh_spans(); | ||||
| 		else | ||||
| 			caret_byte = clamp_cursor(); | ||||
| 		caret_activity = true; | ||||
| 	} | ||||
|  | ||||
| 	state.caret_byte = caret_byte; | ||||
|  | ||||
| 	double const dt = (double)GetFrameTime(); | ||||
| 	if (m_focused_id == id) { | ||||
| 		if (state.preedit_active && state.preedit_cursor_hidden) { | ||||
| 			state.caret_visible = false; | ||||
| 			state.caret_timer = 0.0; | ||||
| 		} else if (caret_activity) { | ||||
| 			state.caret_timer = 0.0; | ||||
| 			state.caret_visible = true; | ||||
| 		} else { | ||||
| 			if (state.caret_timer == 0.0) | ||||
| 				state.caret_visible = true; | ||||
| 			state.caret_timer += dt; | ||||
| 			if (state.caret_timer >= CARET_BLINK_INTERVAL) { | ||||
| 				int toggles = (int)(state.caret_timer / CARET_BLINK_INTERVAL); | ||||
| 				state.caret_timer | ||||
| 				    = std::fmod(state.caret_timer, CARET_BLINK_INTERVAL); | ||||
| 				if (toggles % 2 == 1) | ||||
| 					state.caret_visible = !state.caret_visible; | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		state.caret_visible = false; | ||||
| 		state.caret_timer = 0.0; | ||||
| 	} | ||||
|  | ||||
| 	Color const bg_col { 16, 16, 16, 100 }; | ||||
| 	Color const border_col { 220, 220, 220, 180 }; | ||||
| 	DrawRectangleRec(rec, bg_col); | ||||
| 	DrawRectangleLinesEx(rec, 1.0f, border_col); | ||||
|  | ||||
| 	float const text_top = rec.y + VERTICAL_PADDING; | ||||
| 	float const baseline_y = text_top + style().font_size; | ||||
| 	float const max_caret_h | ||||
| 	    = std::max(0.0f, rec.height - 2.0f * VERTICAL_PADDING); | ||||
| 	float caret_height = style().font_size * (1.0f + CARET_DESCENT_FRACTION); | ||||
| 	caret_height = std::min( | ||||
| 	    caret_height, max_caret_h > 0.0f ? max_caret_h : caret_height); | ||||
| 	if (caret_height <= 0.0f) | ||||
| 		caret_height = style().font_size; | ||||
| 	float caret_top = baseline_y - style().font_size; | ||||
| 	float const min_top = rec.y + VERTICAL_PADDING; | ||||
| 	float const max_top = rec.y + rec.height - VERTICAL_PADDING - caret_height; | ||||
| 	caret_top = std::clamp(caret_top, min_top, max_top); | ||||
| 	float caret_bottom = caret_top + caret_height; | ||||
| 	float const desired_bottom | ||||
| 	    = baseline_y + style().font_size * CARET_DESCENT_FRACTION; | ||||
| 	if (caret_bottom < desired_bottom) { | ||||
| 		float const adjust = desired_bottom - caret_bottom; | ||||
| 		caret_top = std::min(caret_top + adjust, max_top); | ||||
| 		caret_bottom = caret_top + caret_height; | ||||
| 	} | ||||
| 	state.cursor_position.y = caret_top; | ||||
|  | ||||
| 	float const available_width | ||||
| 	    = std::max(0.0f, rec.width - 2.0f * HORIZONTAL_PADDING); | ||||
| 	float const base_y = px_pos(baseline_y); | ||||
|  | ||||
| 	int const font_px = (int)style().font_size; | ||||
|  | ||||
| 	Vector2 prefix_metrics { 0.0f, 0.0f }; | ||||
| 	{ | ||||
| 		std::string_view prefix(str.data(), caret_byte); | ||||
| 		if (m_text_renderer) | ||||
| 			prefix_metrics | ||||
| 			    = m_text_renderer->measure_text(*m_font, prefix, font_px); | ||||
| 	} | ||||
|  | ||||
| 	Vector2 caret_preedit_metrics { 0.0f, 0.0f }; | ||||
| 	bool const has_preedit = state.preedit_active | ||||
| 	    && (!state.preedit_text.empty() || !state.preedit_cursor_hidden); | ||||
| 	if (has_preedit && m_text_renderer) { | ||||
| 		auto pe_end = clamp_preedit_index( | ||||
| 		    state.preedit_cursor_end, state.preedit_text.size()); | ||||
| 		caret_preedit_metrics = m_text_renderer->measure_text(*m_font, | ||||
| 		    std::string_view(state.preedit_text.data(), (usize)pe_end), | ||||
| 		    font_px); | ||||
| 	} | ||||
|  | ||||
| 	Vector2 full_metrics { 0.0f, 0.0f }; | ||||
| 	{ | ||||
| 		std::string display; | ||||
| 		display.reserve( | ||||
| 		    str.size() + (has_preedit ? state.preedit_text.size() : 0)); | ||||
| 		display.append(std::string_view(str.data(), caret_byte)); | ||||
| 		if (has_preedit) | ||||
| 			display.append(state.preedit_text); | ||||
| 		display.append( | ||||
| 		    std::string_view(str.data() + caret_byte, str.size() - caret_byte)); | ||||
| 		if (m_text_renderer) | ||||
| 			full_metrics = m_text_renderer->measure_text(*m_font, | ||||
| 			    std::string_view(display.data(), display.size()), font_px); | ||||
| 	} | ||||
|  | ||||
| 	float caret_offset = prefix_metrics.x + caret_preedit_metrics.x; | ||||
| 	state.cursor_position.x = caret_offset; | ||||
|  | ||||
| 	if (full_metrics.x <= available_width) { | ||||
| 		state.scroll_offset.x = 0.0f; | ||||
| 	} else { | ||||
| 		float &scroll = state.scroll_offset.x; | ||||
| 		float caret_local = caret_offset - scroll; | ||||
| 		float const pad = 8.0f; | ||||
| 		if (caret_local > available_width - pad) | ||||
| 			scroll = caret_offset - (available_width - pad); | ||||
| 		else if (caret_local < pad) | ||||
| 			scroll = caret_offset - pad; | ||||
| 		scroll = std::clamp( | ||||
| 		    scroll, 0.0f, std::max(0.0f, full_metrics.x - available_width)); | ||||
| 	} | ||||
| 	state.scroll_offset.y = 0.0f; | ||||
|  | ||||
| 	float const origin = rec.x + HORIZONTAL_PADDING - state.scroll_offset.x; | ||||
|  | ||||
| 	BeginScissorMode(rec.x, rec.y, rec.width, rec.height); | ||||
| 	{ | ||||
| 		if (m_font.has_value() && m_text_renderer) { | ||||
| 			Color const &text_color = style().text_color; | ||||
|  | ||||
| 			std::string display; | ||||
| 			display.reserve( | ||||
| 			    str.size() + (has_preedit ? state.preedit_text.size() : 0)); | ||||
| 			display.append(std::string_view(str.data(), caret_byte)); | ||||
| 			if (has_preedit) | ||||
| 				display.append(state.preedit_text); | ||||
| 			display.append(std::string_view( | ||||
| 			    str.data() + caret_byte, str.size() - caret_byte)); | ||||
|  | ||||
| 			m_text_renderer->draw_text(*m_font, | ||||
| 			    std::string_view(display.data(), display.size()), | ||||
| 			    { origin, base_y }, font_px, text_color); | ||||
|  | ||||
| 			if (state.has_selection(state.current_rune_idx)) { | ||||
| 				auto [sb, se] = selection_range_bytes( | ||||
| 				    state.sel_anchor_idx, state.current_rune_idx); | ||||
|  | ||||
| 				Vector2 sel_prefix = m_text_renderer->measure_text( | ||||
| 				    *m_font, std::string_view(str.data(), sb), font_px); | ||||
| 				Vector2 sel_width = m_text_renderer->measure_text(*m_font, | ||||
| 				    std::string_view(str.data() + sb, se - sb), font_px); | ||||
|  | ||||
| 				Rectangle sel_rect { std::floor(origin + sel_prefix.x + 0.5f), | ||||
| 					std::floor(caret_top + 0.5f), | ||||
| 					std::max(1.0f, sel_width.x) + 1, | ||||
| 					std::max(1.0f, std::round(caret_height)) }; | ||||
| 				DrawRectangleRec(sel_rect, style().selection_color); | ||||
|  | ||||
| 				m_text_renderer->draw_text(*m_font, | ||||
| 				    std::string_view(str.data() + sb, se - sb), | ||||
| 				    { origin + sel_prefix.x, base_y }, font_px, | ||||
| 				    style().selection_text_color); | ||||
| 			} | ||||
|  | ||||
| 			if (m_focused_id == id && state.caret_visible) { | ||||
| 				float const caret_x = std::floor(origin + caret_offset + 0.5f); | ||||
| 				Vector2 const p0 { caret_x, std::floor(caret_top + 0.5f) }; | ||||
| 				Vector2 const p1 { caret_x, | ||||
| 					std::floor((caret_top + caret_height) + 0.5f) }; | ||||
| 				DrawLineV(p0, p1, text_color); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	EndScissorMode(); | ||||
|  | ||||
| 	if (state.external_change) { | ||||
| 		changed = true; | ||||
| 		state.external_change = false; | ||||
| 	} | ||||
|  | ||||
| 	return std::bitset<2> { static_cast<unsigned long long>( | ||||
| 		(submitted ? 1 : 0) | (changed ? 2 : 0)) }; | ||||
| } | ||||
|  | ||||
| auto ImGui::list_view(usize id, Rectangle bounds, usize elements, | ||||
|     std::function<Vector2(usize i)> draw_cb, ListViewOptions options) -> bool | ||||
| { | ||||
| 	auto const &state { m_lv_states[id] }; | ||||
|  | ||||
| 	bool submitted { false }; | ||||
|  | ||||
| 	m_next_lv_next = false; | ||||
| 	m_next_lv_previous = false; | ||||
| 	m_next_lv_clear = false; | ||||
|  | ||||
| 	BeginScissorMode(bounds.x, bounds.y, bounds.width, bounds.height); | ||||
|  | ||||
| 	EndScissorMode(); | ||||
|  | ||||
| 	m_prev_lv_selected_item = state.selected_item; | ||||
|  | ||||
| 	return submitted; | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
							
								
								
									
										164
									
								
								src/ImGui.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/ImGui.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <bitset> | ||||
| #include <functional> | ||||
| #include <memory> | ||||
| #include <optional> | ||||
| #include <queue> | ||||
| #include <string> | ||||
| #include <unordered_map> | ||||
|  | ||||
| #include <raylib.h> | ||||
|  | ||||
| #include "TextRenderer.hpp" | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| constexpr float DEFAULT_FONT_SIZE { 24 }; | ||||
|  | ||||
| struct TextInputOptions { | ||||
| 	bool multiline { false }; | ||||
| }; | ||||
|  | ||||
| struct ListViewOptions { | ||||
| 	bool selectable { false }; | ||||
| }; | ||||
|  | ||||
| struct ImGui { | ||||
| 	struct Style { | ||||
| 		float font_size { DEFAULT_FONT_SIZE }; | ||||
| 		Color text_color { BLACK }; | ||||
| 		Color preedit_color { BLACK }; | ||||
| 		Color selection_color { 127, 127, 255, 255 }; | ||||
| 		Color selection_text_color { WHITE }; | ||||
| 	}; | ||||
|  | ||||
| 	explicit ImGui(std::shared_ptr<TextRenderer> text_renderer); | ||||
|  | ||||
| 	ImGui(ImGui const &) = delete; | ||||
| 	auto operator=(ImGui const &) -> ImGui & = delete; | ||||
| 	ImGui(ImGui &&) = default; | ||||
| 	auto operator=(ImGui &&) -> ImGui & = default; | ||||
|  | ||||
| 	void begin(u32 const rune, bool ctrl, bool shift, | ||||
| 	    std::string_view const clipboard, | ||||
| 	    std::function<void(std::string_view const &)> clipboard_set); | ||||
| 	void end(); | ||||
|  | ||||
| 	// Bit 0 -> Submitted | ||||
| 	// Bit 1 -> String changed | ||||
| 	auto text_input(usize id, std::pmr::string &str, Rectangle rec, | ||||
| 	    TextInputOptions options = {}) -> std::bitset<2>; | ||||
|  | ||||
| 	struct TextInputSurrounding { | ||||
| 		std::string text; | ||||
| 		int cursor { 0 }; | ||||
| 		int anchor { 0 }; | ||||
| 		usize caret_byte { 0 }; | ||||
| 	}; | ||||
|  | ||||
| 	struct TextInputCursor { | ||||
| 		Rectangle rect {}; | ||||
| 		bool visible { true }; | ||||
| 	}; | ||||
|  | ||||
| 	auto focused_text_input() const -> std::optional<usize>; | ||||
| 	auto text_input_surrounding(usize id, std::pmr::string const &str) const | ||||
| 	    -> std::optional<TextInputSurrounding>; | ||||
| 	auto text_input_cursor(usize id) const -> std::optional<TextInputCursor>; | ||||
| 	void ime_commit_text(std::pmr::string &str, std::string_view text); | ||||
| 	void ime_delete_surrounding( | ||||
| 	    std::pmr::string &str, usize before, usize after); | ||||
| 	void ime_set_preedit(std::string text, int cursor_begin, int cursor_end); | ||||
| 	void ime_clear_preedit(); | ||||
|  | ||||
| 	auto list_view(usize id, Rectangle bounds, usize elements, | ||||
| 	    std::function<Vector2(usize i)> draw_cb, ListViewOptions options = {}) | ||||
| 	    -> bool; | ||||
| 	auto prev_list_view_selected_item() const -> std::optional<usize> | ||||
| 	{ | ||||
| 		return m_prev_lv_selected_item; | ||||
| 	} | ||||
|  | ||||
| 	void set_font(FontHandle font); | ||||
|  | ||||
| 	auto style() -> Style & { return m_styles.back(); } | ||||
| 	auto push_style(Style const &style) -> Style & | ||||
| 	{ | ||||
| 		m_styles.push(style); | ||||
| 		return m_styles.back(); | ||||
| 	} | ||||
| 	auto push_style() -> Style & { return push_style(style()); } | ||||
|  | ||||
| 	[[nodiscard]] inline auto id(std::string_view const str) -> usize | ||||
| 	{ | ||||
| 		std::hash<std::string_view> hasher; | ||||
| 		return hasher(str); | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	struct TextInputState { | ||||
| 		int current_rune_idx { 0 }; | ||||
|  | ||||
| 		// y not used if multiline == false | ||||
| 		Vector2 scroll_offset { 0, 0 }; | ||||
| 		Vector2 cursor_position { 0, 0 }; | ||||
|  | ||||
| 		bool caret_visible { true }; | ||||
| 		double caret_timer { 0.0 }; | ||||
| 		std::string preedit_text; | ||||
| 		int preedit_cursor_begin { 0 }; | ||||
| 		int preedit_cursor_end { 0 }; | ||||
| 		bool preedit_active { false }; | ||||
| 		bool preedit_cursor_hidden { false }; | ||||
| 		usize caret_byte { 0 }; | ||||
| 		Rectangle caret_rect {}; | ||||
| 		bool external_change { false }; | ||||
| 		int sel_anchor_idx = -1; | ||||
|  | ||||
| 		bool has_selection(int curr_idx) const | ||||
| 		{ | ||||
| 			return sel_anchor_idx != -1 && sel_anchor_idx != curr_idx; | ||||
| 		} | ||||
| 		void clear_selection() { sel_anchor_idx = -1; } | ||||
| 	}; | ||||
|  | ||||
| 	struct ListViewState { | ||||
| 		float scroll_offset_y { 0 }; | ||||
| 		std::optional<usize> selected_item { std::nullopt }; | ||||
| 	}; | ||||
|  | ||||
| 	std::unordered_map<usize, ListViewState> m_lv_states; | ||||
| 	std::unordered_map<usize, TextInputState> m_ti_states; | ||||
| 	bool m_next_lv_next { false }; | ||||
| 	bool m_next_lv_previous { false }; | ||||
| 	bool m_next_lv_clear { false }; | ||||
| 	std::optional<usize> m_prev_lv_selected_item { std::nullopt }; | ||||
| 	usize m_focused_id {}; | ||||
| 	u32 m_rune {}; // 1234 <-> hjkl arrow keys | ||||
| 	bool m_ctrl {}; | ||||
| 	bool m_shift {}; | ||||
| 	std::string_view m_clipboard {}; | ||||
| 	std::function<void(std::string_view const &)> m_clipboard_set; | ||||
|  | ||||
| 	std::queue<Style> m_styles { { Style {} } }; | ||||
|  | ||||
| 	std::optional<FontHandle> m_font {}; | ||||
| 	std::shared_ptr<TextRenderer> m_text_renderer {}; | ||||
| }; | ||||
|  | ||||
| struct ImGuiGuard { | ||||
| 	ImGuiGuard(std::shared_ptr<ImGui> imgui, u32 const rune, bool const ctrl, | ||||
| 	    bool const shift, std::string_view const clipboard, | ||||
| 	    std::function<void(std::string_view const &)> clipboard_set) | ||||
| 	    : m_imgui(imgui) | ||||
| 	{ | ||||
| 		m_imgui->begin(rune, ctrl, shift, clipboard, clipboard_set); | ||||
| 	} | ||||
| 	~ImGuiGuard() { m_imgui->end(); } | ||||
|  | ||||
| private: | ||||
| 	std::shared_ptr<ImGui> m_imgui { nullptr }; | ||||
| }; | ||||
|  | ||||
| } // namespace Waylight | ||||
							
								
								
									
										241
									
								
								src/InotifyWatcher.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								src/InotifyWatcher.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| #include "InotifyWatcher.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <array> | ||||
| #include <cerrno> | ||||
| #include <cstring> | ||||
| #include <poll.h> | ||||
| #include <stdexcept> | ||||
| #include <system_error> | ||||
| #include <unistd.h> | ||||
| #include <utility> | ||||
|  | ||||
| #include <sys/inotify.h> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| namespace { | ||||
| constexpr std::uint32_t watch_mask { IN_ATTRIB | IN_CREATE | IN_DELETE | ||||
| 	| IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO | ||||
| 	| IN_CLOSE_WRITE }; | ||||
| constexpr int poll_timeout_ms { 250 }; | ||||
| } // namespace | ||||
|  | ||||
| InotifyWatcher::InotifyWatcher() | ||||
|     : m_fd(inotify_init1(IN_CLOEXEC)) | ||||
| { | ||||
| 	if (m_fd < 0) { | ||||
| 		throw std::system_error( | ||||
| 		    errno, std::generic_category(), "inotify_init1 failed"); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| InotifyWatcher::~InotifyWatcher() | ||||
| { | ||||
| 	stop(); | ||||
|  | ||||
| 	if (m_fd >= 0) { | ||||
| 		::close(m_fd); | ||||
| 		m_fd = -1; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void InotifyWatcher::watch_path_recursively(std::filesystem::path const &path) | ||||
| { | ||||
| 	add_watch_recursively(path); | ||||
| } | ||||
|  | ||||
| void InotifyWatcher::set_callback(callback_t cb) | ||||
| { | ||||
| 	std::scoped_lock lock(m_watch_mutex); | ||||
| 	m_callback = std::move(cb); | ||||
| } | ||||
|  | ||||
| void InotifyWatcher::run() | ||||
| { | ||||
| 	if (m_running.exchange(true)) { | ||||
| 		throw std::runtime_error("InotifyWatcher::run called while running"); | ||||
| 	} | ||||
|  | ||||
| 	std::array<char, 4096> buffer {}; | ||||
| 	while (m_running.load()) { | ||||
| 		pollfd fd_set { | ||||
| 			.fd = m_fd, | ||||
| 			.events = POLLIN, | ||||
| 			.revents = 0, | ||||
| 		}; | ||||
|  | ||||
| 		int const poll_res { ::poll(&fd_set, 1, poll_timeout_ms) }; | ||||
| 		if (!m_running.load()) | ||||
| 			break; | ||||
|  | ||||
| 		if (poll_res < 0) { | ||||
| 			if (errno == EINTR) | ||||
| 				continue; | ||||
| 			if (errno == EBADF || errno == EINVAL) | ||||
| 				break; | ||||
| 			throw std::system_error(errno, std::generic_category(), "poll"); | ||||
| 		} | ||||
|  | ||||
| 		if (poll_res == 0) | ||||
| 			continue; | ||||
|  | ||||
| 		if (fd_set.revents & (POLLERR | POLLHUP | POLLNVAL)) | ||||
| 			break; | ||||
|  | ||||
| 		if (!(fd_set.revents & POLLIN)) | ||||
| 			continue; | ||||
|  | ||||
| 		ssize_t const bytes_read { ::read(m_fd, buffer.data(), buffer.size()) }; | ||||
| 		if (bytes_read < 0) { | ||||
| 			if (errno == EINTR || errno == EAGAIN) | ||||
| 				continue; | ||||
| 			if (errno == EBADF || errno == EINVAL) | ||||
| 				break; | ||||
| 			throw std::system_error(errno, std::generic_category(), "read"); | ||||
| 		} | ||||
|  | ||||
| 		ssize_t offset { 0 }; | ||||
| 		while (offset + static_cast<ssize_t>(sizeof(inotify_event)) | ||||
| 		    <= bytes_read) { | ||||
| 			auto const *event = reinterpret_cast<inotify_event const *>( | ||||
| 			    buffer.data() + offset); | ||||
| 			std::string_view name; | ||||
| 			if (event->len > 0) { | ||||
| 				auto const *raw_name | ||||
| 				    = buffer.data() + offset + sizeof(inotify_event); | ||||
| 				auto const *end | ||||
| 				    = std::find(raw_name, raw_name + event->len, '\0'); | ||||
| 				name = std::string_view(raw_name, end - raw_name); | ||||
| 			} | ||||
|  | ||||
| 			dispatch_event(event->wd, event->mask, name); | ||||
|  | ||||
| 			offset += sizeof(inotify_event) + event->len; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	m_running.store(false); | ||||
| } | ||||
|  | ||||
| void InotifyWatcher::stop() { m_running.store(false); } | ||||
|  | ||||
| void InotifyWatcher::add_watch_for_path(std::filesystem::path const &path) | ||||
| { | ||||
| 	auto normalized { normalize_path(path) }; | ||||
| 	if (normalized.empty()) | ||||
| 		return; | ||||
|  | ||||
| 	auto const key = normalized.string(); | ||||
| 	{ | ||||
| 		std::scoped_lock lock(m_watch_mutex); | ||||
| 		auto it = m_path_to_watch.find(key); | ||||
| 		if (it != m_path_to_watch.end()) | ||||
| 			return; | ||||
| 	} | ||||
|  | ||||
| 	int const wd { ::inotify_add_watch(m_fd, normalized.c_str(), watch_mask) }; | ||||
| 	if (wd < 0) { | ||||
| 		if (errno == ENOENT || errno == ENOTDIR) | ||||
| 			return; | ||||
| 		throw std::system_error(errno, std::generic_category(), | ||||
| 		    "inotify_add_watch failed for " + normalized.string()); | ||||
| 	} | ||||
|  | ||||
| 	std::scoped_lock lock(m_watch_mutex); | ||||
| 	m_watch_to_path.emplace(wd, normalized); | ||||
| 	m_path_to_watch.emplace(key, wd); | ||||
| } | ||||
|  | ||||
| void InotifyWatcher::add_watch_recursively(std::filesystem::path const &path) | ||||
| { | ||||
| 	auto normalized = normalize_path(path); | ||||
| 	if (normalized.empty()) | ||||
| 		return; | ||||
|  | ||||
| 	add_watch_for_path(normalized); | ||||
|  | ||||
| 	std::error_code ec; | ||||
| 	if (!std::filesystem::is_directory(normalized, ec)) | ||||
| 		return; | ||||
|  | ||||
| 	try { | ||||
| 		auto const options { | ||||
| 			std::filesystem::directory_options::follow_directory_symlink | ||||
| 			| std::filesystem::directory_options::skip_permission_denied | ||||
| 		}; | ||||
| 		for (auto const &entry : std::filesystem::recursive_directory_iterator( | ||||
| 		         normalized, options)) { | ||||
| 			std::error_code inner_ec; | ||||
| 			if (!entry.is_directory(inner_ec) || inner_ec) | ||||
| 				continue; | ||||
| 			add_watch_for_path(entry.path()); | ||||
| 		} | ||||
| 	} catch (std::filesystem::filesystem_error const &) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void InotifyWatcher::remove_watch(int wd) | ||||
| { | ||||
| 	std::filesystem::path removed_path; | ||||
| 	{ | ||||
| 		std::scoped_lock lock(m_watch_mutex); | ||||
| 		auto it { m_watch_to_path.find(wd) }; | ||||
| 		if (it == m_watch_to_path.end()) | ||||
| 			return; | ||||
| 		removed_path = it->second; | ||||
| 		m_watch_to_path.erase(it); | ||||
| 		m_path_to_watch.erase(removed_path.string()); | ||||
| 	} | ||||
|  | ||||
| 	::inotify_rm_watch(m_fd, wd); | ||||
| } | ||||
|  | ||||
| void InotifyWatcher::dispatch_event( | ||||
|     int wd, std::uint32_t mask, std::string_view name) | ||||
| { | ||||
| 	std::filesystem::path base_path; | ||||
| 	callback_t callback_copy; | ||||
| 	{ | ||||
| 		std::scoped_lock lock(m_watch_mutex); | ||||
| 		auto it { m_watch_to_path.find(wd) }; | ||||
| 		if (it == m_watch_to_path.end()) | ||||
| 			return; | ||||
| 		base_path = it->second; | ||||
| 		callback_copy = m_callback; | ||||
| 	} | ||||
|  | ||||
| 	auto event_path { base_path }; | ||||
| 	if (!name.empty()) | ||||
| 		event_path /= name; | ||||
|  | ||||
| 	if ((mask & IN_ISDIR) | ||||
| 	    && (mask & (IN_CREATE | IN_MOVED_TO | IN_ATTRIB | IN_CLOSE_WRITE))) { | ||||
| 		add_watch_recursively(event_path); | ||||
| 	} | ||||
|  | ||||
| 	if (mask & (IN_DELETE_SELF | IN_MOVE_SELF | IN_IGNORED)) { | ||||
| 		remove_watch(wd); | ||||
| 	} | ||||
|  | ||||
| 	if (callback_copy) { | ||||
| 		callback_copy(FileWatchEvent { | ||||
| 		    .path = std::move(event_path), | ||||
| 		    .mask = mask, | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| std::filesystem::path InotifyWatcher::normalize_path( | ||||
|     std::filesystem::path const &path) | ||||
| { | ||||
| 	std::error_code ec; | ||||
| 	auto normalized { std::filesystem::weakly_canonical(path, ec) }; | ||||
| 	if (ec) | ||||
| 		normalized = std::filesystem::absolute(path, ec); | ||||
| 	if (ec) | ||||
| 		return {}; | ||||
| 	return normalized; | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
							
								
								
									
										57
									
								
								src/InotifyWatcher.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/InotifyWatcher.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <atomic> | ||||
| #include <cstdint> | ||||
| #include <filesystem> | ||||
| #include <functional> | ||||
| #include <mutex> | ||||
| #include <string> | ||||
| #include <string_view> | ||||
| #include <unordered_map> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| struct FileWatchEvent { | ||||
| 	std::filesystem::path path; | ||||
| 	std::uint32_t mask; | ||||
| }; | ||||
|  | ||||
| class InotifyWatcher { | ||||
| public: | ||||
| 	using callback_t = std::function<void(FileWatchEvent const &)>; | ||||
|  | ||||
| 	InotifyWatcher(); | ||||
| 	~InotifyWatcher(); | ||||
|  | ||||
| 	InotifyWatcher(InotifyWatcher const &) = delete; | ||||
| 	InotifyWatcher &operator=(InotifyWatcher const &) = delete; | ||||
|  | ||||
| 	InotifyWatcher(InotifyWatcher &&) = delete; | ||||
| 	InotifyWatcher &operator=(InotifyWatcher &&) = delete; | ||||
|  | ||||
| 	void watch_path_recursively(std::filesystem::path const &path); | ||||
|  | ||||
| 	void set_callback(callback_t cb); | ||||
|  | ||||
| 	void run(); | ||||
| 	void stop(); | ||||
|  | ||||
| private: | ||||
| 	void add_watch_for_path(std::filesystem::path const &path); | ||||
| 	void add_watch_recursively(std::filesystem::path const &path); | ||||
| 	void remove_watch(int wd); | ||||
|  | ||||
| 	void dispatch_event(int wd, std::uint32_t mask, std::string_view name); | ||||
| 	static std::filesystem::path normalize_path( | ||||
| 	    std::filesystem::path const &path); | ||||
|  | ||||
| 	int m_fd; | ||||
| 	std::atomic<bool> m_running { false }; | ||||
| 	callback_t m_callback; | ||||
|  | ||||
| 	std::mutex m_watch_mutex; | ||||
| 	std::unordered_map<int, std::filesystem::path> m_watch_to_path; | ||||
| 	std::unordered_map<std::string, int> m_path_to_watch; | ||||
| }; | ||||
|  | ||||
| } // namespace Waylight | ||||
| @@ -1,56 +1,795 @@ | ||||
| #include "TextRenderer.hpp" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cassert> | ||||
| #include <chrono> | ||||
| #include <cmath> | ||||
| #include <cstdlib> | ||||
| #include <cstring> | ||||
| #include <limits> | ||||
| #include <mutex> | ||||
| #include <optional> | ||||
| #include <string> | ||||
| #include <string_view> | ||||
| #include <unordered_map> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include <fontconfig/fontconfig.h> | ||||
|  | ||||
| #include <raylib.h> | ||||
| #include <rlgl.h> | ||||
|  | ||||
| #undef BLACK | ||||
| #undef WHITE | ||||
| #undef RED | ||||
| #undef GREEN | ||||
| #undef BLUE | ||||
| #undef YELLOW | ||||
| #undef MAGENTA | ||||
|  | ||||
| #include <ft2build.h> | ||||
| #include FT_FREETYPE_H | ||||
| #include FT_GLYPH_H | ||||
| #include <hb-ft.h> | ||||
| #include <hb.h> | ||||
|  | ||||
| #include <ext/import-font.h> | ||||
| #include <msdfgen.h> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| constexpr int ATLAS_DIMENSION = 1024; | ||||
| constexpr int ATLAS_PADDING = 2; | ||||
| constexpr float DEFAULT_EM_SCALE = 48.0f; | ||||
|  | ||||
| constexpr float hb_to_em(hb_position_t value, unsigned upem) | ||||
| { | ||||
| 	return static_cast<float>(value) | ||||
| 	    / (64.0f * static_cast<float>(upem ? upem : 1)); | ||||
| } | ||||
|  | ||||
| auto ft_library() -> FT_Library | ||||
| { | ||||
| 	static FT_Library library = nullptr; | ||||
| 	static std::once_flag once; | ||||
| 	std::call_once(once, [] { | ||||
| 		if (FT_Init_FreeType(&library) != 0) | ||||
| 			library = nullptr; | ||||
| 		else | ||||
| 			std::atexit([] { | ||||
| 				if (library) | ||||
| 					FT_Done_FreeType(library); | ||||
| 			}); | ||||
| 	}); | ||||
| 	return library; | ||||
| } | ||||
|  | ||||
| struct CodepointSpan { | ||||
| 	uint32_t codepoint {}; | ||||
| 	usize start {}; | ||||
| 	usize end {}; | ||||
| }; | ||||
|  | ||||
| auto decode_utf8(std::string_view text) -> std::vector<CodepointSpan> | ||||
| { | ||||
| 	std::vector<CodepointSpan> spans; | ||||
| 	usize i = 0; | ||||
| 	while (i < text.size()) { | ||||
| 		u8 const byte = static_cast<u8>(text[i]); | ||||
| 		usize const start = i; | ||||
| 		usize length = 1; | ||||
| 		uint32_t cp = 0xFFFD; | ||||
| 		if (byte < 0x80) { | ||||
| 			cp = byte; | ||||
| 		} else if ((byte & 0xE0) == 0xC0) { | ||||
| 			if (i + 1 < text.size()) { | ||||
| 				u8 const b1 = static_cast<u8>(text[i + 1]); | ||||
| 				if ((b1 & 0xC0) == 0x80) { | ||||
| 					uint32_t t = ((byte & 0x1F) << 6) | ||||
| 					    | (static_cast<uint32_t>(b1) & 0x3F); | ||||
| 					if (t >= 0x80) { | ||||
| 						cp = t; | ||||
| 						length = 2; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} else if ((byte & 0xF0) == 0xE0) { | ||||
| 			if (i + 2 < text.size()) { | ||||
| 				u8 const b1 = static_cast<u8>(text[i + 1]); | ||||
| 				u8 const b2 = static_cast<u8>(text[i + 2]); | ||||
| 				if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80) { | ||||
| 					uint32_t t = ((byte & 0x0F) << 12) | ||||
| 					    | ((static_cast<uint32_t>(b1) & 0x3F) << 6) | ||||
| 					    | (static_cast<uint32_t>(b2) & 0x3F); | ||||
| 					if (t >= 0x800 && (t < 0xD800 || t > 0xDFFF)) { | ||||
| 						cp = t; | ||||
| 						length = 3; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} else if ((byte & 0xF8) == 0xF0) { | ||||
| 			if (i + 3 < text.size()) { | ||||
| 				u8 const b1 = static_cast<u8>(text[i + 1]); | ||||
| 				u8 const b2 = static_cast<u8>(text[i + 2]); | ||||
| 				u8 const b3 = static_cast<u8>(text[i + 3]); | ||||
| 				if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80 | ||||
| 				    && (b3 & 0xC0) == 0x80) { | ||||
| 					uint32_t t = ((byte & 0x07) << 18) | ||||
| 					    | ((static_cast<uint32_t>(b1) & 0x3F) << 12) | ||||
| 					    | ((static_cast<uint32_t>(b2) & 0x3F) << 6) | ||||
| 					    | (static_cast<uint32_t>(b3) & 0x3F); | ||||
| 					if (t >= 0x10000 && t <= 0x10FFFF) { | ||||
| 						cp = t; | ||||
| 						length = 4; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		spans.push_back(CodepointSpan { | ||||
| 		    .codepoint = cp, | ||||
| 		    .start = start, | ||||
| 		    .end = std::min(text.size(), start + length), | ||||
| 		}); | ||||
| 		i += length; | ||||
| 	} | ||||
| 	return spans; | ||||
| } | ||||
|  | ||||
| } // namespace | ||||
|  | ||||
| auto TextRenderer::flush_font(FontRuntime &rt, FontData &fd) -> void | ||||
| { | ||||
| 	rt.glyph_cache.clear(); | ||||
| 	fd.glyphs.clear(); | ||||
| 	rt.pen_x = ATLAS_PADDING; | ||||
| 	rt.pen_y = ATLAS_PADDING; | ||||
| 	rt.row_height = 0; | ||||
| 	if (fd.atlas_img.data) | ||||
| 		ImageClearBackground(&fd.atlas_img, BLANK); | ||||
| 	if (fd.atlas.id != 0 && fd.atlas_img.data) | ||||
| 		UpdateTexture(fd.atlas, fd.atlas_img.data); | ||||
| } | ||||
|  | ||||
| auto TextRenderer::allocate_region(FontRuntime &rt, FontData &fd, int width, | ||||
|     int height) -> std::optional<std::pair<int, int>> | ||||
| { | ||||
| 	(void)fd; | ||||
| 	int padded_w = width + ATLAS_PADDING; | ||||
| 	if (padded_w > rt.atlas_width || height + ATLAS_PADDING > rt.atlas_height) | ||||
| 		return std::nullopt; | ||||
| 	if (rt.pen_x + padded_w > rt.atlas_width) { | ||||
| 		rt.pen_x = ATLAS_PADDING; | ||||
| 		rt.pen_y += rt.row_height; | ||||
| 		rt.row_height = 0; | ||||
| 	} | ||||
| 	if (rt.pen_y + height + ATLAS_PADDING > rt.atlas_height) | ||||
| 		return std::nullopt; | ||||
| 	int x = rt.pen_x; | ||||
| 	int y = rt.pen_y; | ||||
| 	rt.pen_x += padded_w; | ||||
| 	rt.row_height = std::max(rt.row_height, height + ATLAS_PADDING); | ||||
| 	return std::pair { x, y }; | ||||
| } | ||||
|  | ||||
| auto TextRenderer::upload_region(FontData &fd, int dst_x, int dst_y, int width, | ||||
|     int height, std::vector<Color> const &buffer) -> void | ||||
| { | ||||
| 	Rectangle rec { static_cast<float>(dst_x), static_cast<float>(dst_y), | ||||
| 		static_cast<float>(width), static_cast<float>(height) }; | ||||
| 	if (fd.atlas.id != 0) | ||||
| 		UpdateTextureRec(fd.atlas, rec, buffer.data()); | ||||
| 	if (!fd.atlas_img.data) | ||||
| 		return; | ||||
| 	auto *pixels = static_cast<Color *>(fd.atlas_img.data); | ||||
| 	for (int row = 0; row < height; ++row) { | ||||
| 		auto *dst = pixels + (dst_y + row) * fd.atlas_img.width + dst_x; | ||||
| 		std::memcpy(dst, buffer.data() + row * width, sizeof(Color) * width); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd, | ||||
|     u32 glyph_index) -> std::optional<GlyphCacheEntry> | ||||
| { | ||||
| 	auto const gen_start = std::chrono::steady_clock::now(); | ||||
| 	msdfgen::Shape shape; | ||||
| 	double advance_em = 0.0; | ||||
| 	msdfgen::GlyphIndex const index(glyph_index); | ||||
| 	if (!rt.msdf_font | ||||
| 	    || !msdfgen::loadGlyph(shape, rt.msdf_font, index, | ||||
| 	        msdfgen::FONT_SCALING_EM_NORMALIZED, &advance_em)) | ||||
| 		return std::nullopt; | ||||
| 	shape.normalize(); | ||||
| 	// FIXME: Figure out shader | ||||
| 	// msdfgen::edgeColoringInkTrap(shape, 3.0); | ||||
| 	auto bounds = shape.getBounds(); | ||||
| 	float const width_em = static_cast<float>(bounds.r - bounds.l); | ||||
| 	float const height_em = static_cast<float>(bounds.t - bounds.b); | ||||
| 	double const scale = rt.em_scale; | ||||
| 	int bmp_w = std::max( | ||||
| 	    1, static_cast<int>(std::ceil(width_em * scale + 2.0 * rt.px_range))); | ||||
| 	int bmp_h = std::max( | ||||
| 	    1, static_cast<int>(std::ceil(height_em * scale + 2.0 * rt.px_range))); | ||||
|  | ||||
| 	if (bmp_w + ATLAS_PADDING > rt.atlas_width | ||||
| 	    || bmp_h + ATLAS_PADDING > rt.atlas_height) { | ||||
| 		TraceLog(LOG_WARNING, "Glyph %u bitmap %dx%d exceeds atlas %dx%d", | ||||
| 		    glyph_index, bmp_w, bmp_h, rt.atlas_width, rt.atlas_height); | ||||
| 		GlyphCacheEntry too_large {}; | ||||
| 		too_large.width = 0; | ||||
| 		too_large.height = 0; | ||||
| 		return too_large; | ||||
| 	} | ||||
|  | ||||
| 	auto place = allocate_region(rt, fd, bmp_w, bmp_h); | ||||
| 	if (!place) { | ||||
| 		TraceLog(LOG_INFO, "Atlas full, flushing before glyph %u", glyph_index); | ||||
| 		flush_font(rt, fd); | ||||
| 		place = allocate_region(rt, fd, bmp_w, bmp_h); | ||||
| 		if (!place) | ||||
| 			return std::nullopt; | ||||
| 	} | ||||
|  | ||||
| 	msdfgen::Bitmap<float, 3> msdf_bitmap(bmp_w, bmp_h); | ||||
| 	msdfgen::Vector2 scale_vec(scale, scale); | ||||
| 	double const inv_scale = 1.0 / scale; | ||||
| 	msdfgen::Vector2 translate(-bounds.l + rt.px_range * inv_scale, | ||||
| 	    -bounds.b + rt.px_range * inv_scale); | ||||
| 	msdfgen::generateMSDF( | ||||
| 	    msdf_bitmap, shape, rt.px_range, scale_vec, translate); | ||||
|  | ||||
| 	std::vector<Color> buffer(static_cast<usize>(bmp_w) * bmp_h); | ||||
| 	// FIXME: Figure out shader | ||||
| 	// for (int y = 0; y < bmp_h; ++y) { | ||||
| 	//	int const dst_y = bmp_h - 1 - y; | ||||
| 	//	for (int x = 0; x < bmp_w; ++x) { | ||||
| 	//		float const *px = msdf_bitmap(x, y); | ||||
| 	//		auto const r = msdfgen::pixelFloatToByte(px[0]); | ||||
| 	//		auto const g = msdfgen::pixelFloatToByte(px[1]); | ||||
| 	//		auto const b = msdfgen::pixelFloatToByte(px[2]); | ||||
| 	//		buffer[static_cast<usize>(dst_y) * bmp_w + x] | ||||
| 	//		    = Color { r, g, b, 255 }; | ||||
| 	//	} | ||||
| 	//} | ||||
|  | ||||
| 	auto c1 { (int)std::round(msdf_bitmap(0, 0)[3]) }; | ||||
| 	auto c4 { (int)std::round(msdf_bitmap(bmp_w - 1, bmp_h - 1)[3]) }; | ||||
|  | ||||
| 	auto sum_white = 0; | ||||
| 	auto sum_black = 0; | ||||
| 	for (int y = 0; y < bmp_h; ++y) { | ||||
| 		for (int x = 0; x < bmp_w; ++x) { | ||||
| 			float const *px = msdf_bitmap(x, y); | ||||
| 			auto const r = msdfgen::pixelFloatToByte(px[0]); | ||||
| 			if (r > 127) { | ||||
| 				sum_white++; | ||||
| 			} else { | ||||
| 				sum_black++; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	bool flip { sum_white > sum_black && (float)bmp_w / (float)bmp_h > 0.6 }; | ||||
| 	if (c1 == c4) { | ||||
| 		flip = false; | ||||
| 	} | ||||
|  | ||||
| 	// This really isn't the most accurate thing in the world but should work | ||||
| 	// for now. Things like commas might be fucked. | ||||
| 	for (int y = 0; y < bmp_h; ++y) { | ||||
| 		int const dst_y = bmp_h - 1 - y; | ||||
| 		for (int x = 0; x < bmp_w; ++x) { | ||||
| 			float const *px = msdf_bitmap(x, y); | ||||
| 			auto const r = msdfgen::pixelFloatToByte(px[0]); | ||||
| 			if (flip) { | ||||
| 				buffer[static_cast<usize>(dst_y) * bmp_w + x] = Color { 255, | ||||
| 					255, 255, static_cast<unsigned char>(255 - r) }; | ||||
| 			} else { | ||||
| 				buffer[static_cast<usize>(dst_y) * bmp_w + x] | ||||
| 				    = Color { 255, 255, 255, r }; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	upload_region(fd, place->first, place->second, bmp_w, bmp_h, buffer); | ||||
|  | ||||
| 	GlyphCacheEntry entry; | ||||
| 	entry.atlas_x = place->first; | ||||
| 	entry.atlas_y = place->second; | ||||
| 	entry.width = bmp_w; | ||||
| 	entry.height = bmp_h; | ||||
|  | ||||
| 	entry.glyph.advance = static_cast<float>(advance_em); | ||||
| 	entry.glyph.plane_bounds.left = static_cast<float>(bounds.l); | ||||
| 	entry.glyph.plane_bounds.right = static_cast<float>(bounds.r); | ||||
| 	entry.glyph.plane_bounds.top = static_cast<float>(bounds.t); | ||||
| 	entry.glyph.plane_bounds.bottom = static_cast<float>(bounds.b); | ||||
| 	entry.glyph.glyph_bounds.left = static_cast<float>(entry.atlas_x); | ||||
| 	entry.glyph.glyph_bounds.top = static_cast<float>(entry.atlas_y); | ||||
| 	entry.glyph.glyph_bounds.right | ||||
| 	    = static_cast<float>(entry.atlas_x + entry.width); | ||||
| 	entry.glyph.glyph_bounds.bottom | ||||
| 	    = static_cast<float>(entry.atlas_y + entry.height); | ||||
|  | ||||
| 	auto const gen_end = std::chrono::steady_clock::now(); | ||||
| 	auto const gen_ms | ||||
| 	    = std::chrono::duration<double, std::milli>(gen_end - gen_start) | ||||
| 	          .count(); | ||||
| 	if (gen_ms > 2.0) | ||||
| 		TraceLog(LOG_INFO, "Generated glyph %u in %.2f ms (%dx%d texels)", | ||||
| 		    glyph_index, gen_ms, entry.width, entry.height); | ||||
| 	return entry; | ||||
| } | ||||
|  | ||||
| auto TextRenderer::ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index, | ||||
|     bool mark_usage) -> GlyphCacheEntry * | ||||
| { | ||||
| 	auto it = rt.glyph_cache.find(glyph_index); | ||||
| 	if (it != rt.glyph_cache.end()) { | ||||
| 		if (mark_usage) | ||||
| 			it->second.stamp = rt.frame_stamp; | ||||
| 		return &it->second; | ||||
| 	} | ||||
| 	auto entry = generate_glyph(rt, fd, glyph_index); | ||||
| 	if (!entry) | ||||
| 		return nullptr; | ||||
| 	auto [inserted_it, ok] | ||||
| 	    = rt.glyph_cache.emplace(glyph_index, std::move(*entry)); | ||||
| 	if (!ok) | ||||
| 		return nullptr; | ||||
| 	inserted_it->second.stamp | ||||
| 	    = mark_usage ? rt.frame_stamp : inserted_it->second.stamp; | ||||
| 	fd.glyphs[glyph_index] = inserted_it->second.glyph; | ||||
| 	return &inserted_it->second; | ||||
| } | ||||
|  | ||||
| TextRenderer::TextRenderer() | ||||
| { | ||||
| 	static char const msdf_fs_data[] { | ||||
| #embed "msdf.fs" | ||||
| 		, 0 | ||||
| 	static char const msdf_vs_data[] { | ||||
| #embed "base.vert" | ||||
| 		, 0 // cppcheck-suppress syntaxError | ||||
| 	}; | ||||
| 	m_msdf_shader = LoadShaderFromMemory(nullptr, msdf_fs_data); | ||||
| 	static char const msdf_fs_data[] { | ||||
| #embed "msdf.frag" | ||||
| 		, 0 // cppcheck-suppress syntaxError | ||||
| 	}; | ||||
| 	m_msdf_shader = LoadShaderFromMemory(msdf_vs_data, msdf_fs_data); | ||||
| 	assert(IsShaderValid(m_msdf_shader)); | ||||
| 	m_px_range_uniform = GetShaderLocation(m_msdf_shader, "pxRange"); | ||||
| } | ||||
|  | ||||
| TextRenderer::~TextRenderer() { UnloadShader(m_msdf_shader); } | ||||
| TextRenderer::~TextRenderer() | ||||
| { | ||||
| 	for (usize i = 0; i < m_font_sets.size(); ++i) { | ||||
| 		FontHandle handle; | ||||
| 		handle.id = i; | ||||
| 		unload_font(handle); | ||||
| 	} | ||||
| 	// Not unloading the shader... I have no clue why, but there's some sort of | ||||
| 	// double free. I love C interop!!!! | ||||
| } | ||||
|  | ||||
| auto TextRenderer::measure_text(FontHandle const font, | ||||
|     std::string_view const text, int const size) -> Vector2 | ||||
| { | ||||
| 	// FIXME: Implement. | ||||
| 	return {}; | ||||
| 	usize const handle_id = font(); | ||||
| 	if (handle_id >= m_font_sets.size()) | ||||
| 		return Vector2 { 0.0f, 0.0f }; | ||||
| 	auto const &font_set = m_font_sets[handle_id]; | ||||
| 	if (font_set.font_indices.empty()) | ||||
| 		return Vector2 { 0.0f, 0.0f }; | ||||
|  | ||||
| 	auto placements = shape_text(font, text); | ||||
|  | ||||
| 	auto primary_runtime_index = font_set.font_indices.front(); | ||||
| 	if (placements.empty()) { | ||||
| 		if (primary_runtime_index >= m_font_runtime.size() | ||||
| 		    || !m_font_runtime[primary_runtime_index]) | ||||
| 			return Vector2 { 0.0f, 0.0f }; | ||||
| 		auto const &rt_primary = *m_font_runtime[primary_runtime_index]; | ||||
| 		float height_em = rt_primary.ascent - rt_primary.descent; | ||||
| 		return Vector2 { 0.0f, height_em * static_cast<float>(size) }; | ||||
| 	} | ||||
|  | ||||
| 	float advance_em = 0.0f; | ||||
| 	float min_x_em = 0.0f; | ||||
| 	float max_x_em = 0.0f; | ||||
| 	bool first = true; | ||||
| 	bool have_metrics = false; | ||||
| 	float max_ascent = 0.0f; | ||||
| 	float min_descent = 0.0f; | ||||
|  | ||||
| 	for (auto const &placement : placements) { | ||||
| 		usize const runtime_index = placement.runtime_index; | ||||
| 		if (runtime_index >= m_font_runtime.size() | ||||
| 		    || !m_font_runtime[runtime_index]) | ||||
| 			continue; | ||||
| 		auto &rt = *m_font_runtime[runtime_index]; | ||||
| 		auto &fd = m_font_data[runtime_index]; | ||||
| 		auto *entry = ensure_glyph(rt, fd, placement.glyph_index, false); | ||||
| 		if (!entry || entry->width == 0 || entry->height == 0) | ||||
| 			continue; | ||||
| 		float const x_offset_em = hb_to_em(placement.x_offset, rt.units_per_em); | ||||
| 		float const left | ||||
| 		    = advance_em + x_offset_em + entry->glyph.plane_bounds.left; | ||||
| 		float const right | ||||
| 		    = advance_em + x_offset_em + entry->glyph.plane_bounds.right; | ||||
| 		if (first) { | ||||
| 			min_x_em = left; | ||||
| 			max_x_em = right; | ||||
| 			first = false; | ||||
| 		} else { | ||||
| 			min_x_em = std::min(min_x_em, left); | ||||
| 			max_x_em = std::max(max_x_em, right); | ||||
| 		} | ||||
| 		if (!have_metrics) { | ||||
| 			max_ascent = rt.ascent; | ||||
| 			min_descent = rt.descent; | ||||
| 			have_metrics = true; | ||||
| 		} else { | ||||
| 			max_ascent = std::max(max_ascent, rt.ascent); | ||||
| 			min_descent = std::min(min_descent, rt.descent); | ||||
| 		} | ||||
| 		advance_em += hb_to_em(placement.x_advance, rt.units_per_em); | ||||
| 	} | ||||
|  | ||||
| 	if (first) { | ||||
| 		if (primary_runtime_index >= m_font_runtime.size() | ||||
| 		    || !m_font_runtime[primary_runtime_index]) | ||||
| 			return Vector2 { 0.0f, 0.0f }; | ||||
| 		auto const &rt = *m_font_runtime[primary_runtime_index]; | ||||
| 		float height_em = rt.ascent - rt.descent; | ||||
| 		return Vector2 { 0.0f, height_em * static_cast<float>(size) }; | ||||
| 	} | ||||
|  | ||||
| 	float width_em = std::max(max_x_em, advance_em) - min_x_em; | ||||
| 	float height_em = 0.0f; | ||||
| 	if (have_metrics) { | ||||
| 		height_em = max_ascent - min_descent; | ||||
| 	} else if (primary_runtime_index < m_font_runtime.size() | ||||
| 	    && m_font_runtime[primary_runtime_index]) { | ||||
| 		auto const &rt = *m_font_runtime[primary_runtime_index]; | ||||
| 		height_em = rt.ascent - rt.descent; | ||||
| 	} | ||||
|  | ||||
| 	return Vector2 { width_em * static_cast<float>(size), | ||||
| 		height_em * static_cast<float>(size) }; | ||||
| } | ||||
|  | ||||
| auto TextRenderer::draw_text(FontHandle const font, std::string_view const text, | ||||
|     Vector2 const pos, int const size, Color const color) -> void | ||||
| { | ||||
| 	auto const draw_start = std::chrono::steady_clock::now(); | ||||
| 	int const pos_x = pos.x; | ||||
| 	int const pos_y = pos.y; | ||||
| 	// Don't use pos from here on out! | ||||
| 	// FIXME: Implement. | ||||
| 	(void)pos_x, (void)pos_y; | ||||
| 	usize const handle_id = font(); | ||||
| 	if (handle_id >= m_font_sets.size()) | ||||
| 		return; | ||||
| 	auto const &font_set = m_font_sets[handle_id]; | ||||
| 	if (font_set.font_indices.empty()) | ||||
| 		return; | ||||
|  | ||||
| 	auto placements = shape_text(font, text); | ||||
| 	if (placements.empty()) | ||||
| 		return; | ||||
|  | ||||
| 	float const size_f = static_cast<float>(size); | ||||
| 	float pen_x_em = 0.0f; | ||||
| 	float pen_y_em = 0.0f; | ||||
| 	std::vector<usize> updated_stamp; | ||||
| 	updated_stamp.reserve(font_set.font_indices.size()); | ||||
|  | ||||
| 	for (auto const &placement : placements) { | ||||
| 		usize const runtime_index = placement.runtime_index; | ||||
| 		if (runtime_index >= m_font_runtime.size() | ||||
| 		    || !m_font_runtime[runtime_index]) | ||||
| 			continue; | ||||
| 		auto &rt = *m_font_runtime[runtime_index]; | ||||
| 		auto &fd = m_font_data[runtime_index]; | ||||
| 		if (std::find(updated_stamp.begin(), updated_stamp.end(), runtime_index) | ||||
| 		    == updated_stamp.end()) { | ||||
| 			rt.frame_stamp++; | ||||
| 			updated_stamp.push_back(runtime_index); | ||||
| 		} | ||||
|  | ||||
| auto TextRenderer::load_font(std::filesystem::path const &path) | ||||
|     -> std::optional<FontHandle> | ||||
| 		auto *entry = ensure_glyph(rt, fd, placement.glyph_index, true); | ||||
| 		if (!entry || entry->width == 0 || entry->height == 0) | ||||
| 			continue; | ||||
|  | ||||
| 		float const advance_em = hb_to_em(placement.x_advance, rt.units_per_em); | ||||
| 		float const x_offset_em = hb_to_em(placement.x_offset, rt.units_per_em); | ||||
| 		float const y_offset_em = hb_to_em(placement.y_offset, rt.units_per_em); | ||||
| 		float const x_base_em = pen_x_em + x_offset_em; | ||||
| 		float const y_base_em = pen_y_em + y_offset_em; | ||||
| 		float const scale_px = size_f / static_cast<float>(rt.em_scale); | ||||
| 		float const margin_px = static_cast<float>(rt.px_range) * scale_px; | ||||
| 		float const dest_x = pos_x | ||||
| 		    + (x_base_em + entry->glyph.plane_bounds.left) * size_f - margin_px; | ||||
| 		float const dest_y = pos_y | ||||
| 		    - (y_base_em + entry->glyph.plane_bounds.top) * size_f - margin_px; | ||||
| 		float const dest_w = static_cast<float>(entry->width) * scale_px; | ||||
| 		float const dest_h = static_cast<float>(entry->height) * scale_px; | ||||
|  | ||||
| 		Rectangle source { | ||||
| 			entry->glyph.glyph_bounds.left, | ||||
| 			entry->glyph.glyph_bounds.top, | ||||
| 			static_cast<float>(entry->width), | ||||
| 			static_cast<float>(entry->height), | ||||
| 		}; | ||||
| 		Rectangle dest { dest_x, dest_y, dest_w, dest_h }; | ||||
| 		DrawTexturePro( | ||||
| 		    fd.atlas, source, dest, Vector2 { 0.0f, 0.0f }, 0.0f, color); | ||||
|  | ||||
| 		pen_x_em += advance_em; | ||||
| 		pen_y_em += hb_to_em(placement.y_advance, rt.units_per_em); | ||||
| 	} | ||||
|  | ||||
| 	auto const draw_end = std::chrono::steady_clock::now(); | ||||
| 	auto const draw_ms | ||||
| 	    = std::chrono::duration<double, std::milli>(draw_end - draw_start) | ||||
| 	          .count(); | ||||
| 	if (draw_ms > 5.0) | ||||
| 		TraceLog(LOG_INFO, "draw_text took %.2f ms for %zu glyphs", draw_ms, | ||||
| 		    placements.size()); | ||||
| } | ||||
|  | ||||
| auto TextRenderer::load_single_font(std::filesystem::path const &path) | ||||
|     -> std::optional<usize> | ||||
| { | ||||
| 	// FIXME: Implement. | ||||
| 	FT_Library const ft = ft_library(); | ||||
| 	if (!ft) | ||||
| 		return std::nullopt; | ||||
|  | ||||
| 	FT_Face face = nullptr; | ||||
| 	if (FT_New_Face(ft, path.string().c_str(), 0, &face) != 0) | ||||
| 		return std::nullopt; | ||||
| 	if (FT_Select_Charmap(face, FT_ENCODING_UNICODE) != 0) { | ||||
| 		FT_Done_Face(face); | ||||
| 		return std::nullopt; | ||||
| 	} | ||||
|  | ||||
| auto TextRenderer::unload_font(FontHandle const font) | ||||
| 	auto runtime = std::make_unique<FontRuntime>(); | ||||
| 	runtime->face = face; | ||||
| 	runtime->atlas_width = ATLAS_DIMENSION; | ||||
| 	runtime->atlas_height = ATLAS_DIMENSION; | ||||
| 	runtime->pen_x = ATLAS_PADDING; | ||||
| 	runtime->pen_y = ATLAS_PADDING; | ||||
| 	runtime->row_height = 0; | ||||
| 	runtime->px_range = 0.05; // kDefaultPxRange; | ||||
| 	runtime->em_scale = DEFAULT_EM_SCALE; | ||||
| 	runtime->frame_stamp = 0; | ||||
| 	runtime->units_per_em | ||||
| 	    = static_cast<unsigned>(face->units_per_EM ? face->units_per_EM : 2048); | ||||
| 	runtime->ascent = static_cast<float>(face->ascender) | ||||
| 	    / (64.0f * static_cast<float>(runtime->units_per_em)); | ||||
| 	runtime->descent = static_cast<float>(face->descender) | ||||
| 	    / (64.0f * static_cast<float>(runtime->units_per_em)); | ||||
| 	float line_height = static_cast<float>(face->height) | ||||
| 	    / (64.0f * static_cast<float>(runtime->units_per_em)); | ||||
| 	float adv_height = runtime->ascent - runtime->descent; | ||||
| 	runtime->line_gap = std::max(0.0f, line_height - adv_height); | ||||
|  | ||||
| 	runtime->hb_face = hb_ft_face_create_referenced(face); | ||||
| 	if (!runtime->hb_face) { | ||||
| 		FT_Done_Face(face); | ||||
| 		return std::nullopt; | ||||
| 	} | ||||
| 	runtime->hb_font = hb_ft_font_create_referenced(face); | ||||
| 	if (!runtime->hb_font) { | ||||
| 		hb_face_destroy(runtime->hb_face); | ||||
| 		FT_Done_Face(face); | ||||
| 		return std::nullopt; | ||||
| 	} | ||||
| 	hb_font_set_scale(runtime->hb_font, | ||||
| 	    static_cast<int>(runtime->units_per_em) << 6, | ||||
| 	    static_cast<int>(runtime->units_per_em) << 6); | ||||
| 	hb_ft_font_set_funcs(runtime->hb_font); | ||||
|  | ||||
| 	runtime->msdf_font = msdfgen::adoptFreetypeFont(face); | ||||
| 	if (!runtime->msdf_font) { | ||||
| 		hb_font_destroy(runtime->hb_font); | ||||
| 		hb_face_destroy(runtime->hb_face); | ||||
| 		FT_Done_Face(face); | ||||
| 		return std::nullopt; | ||||
| 	} | ||||
|  | ||||
| 	FontData font_data {}; | ||||
| 	font_data.font_path = path; | ||||
| 	font_data.atlas_img | ||||
| 	    = GenImageColor(runtime->atlas_width, runtime->atlas_height, BLANK); | ||||
| 	if (!font_data.atlas_img.data) { | ||||
| 		msdfgen::destroyFont(runtime->msdf_font); | ||||
| 		runtime->msdf_font = nullptr; | ||||
| 		hb_font_destroy(runtime->hb_font); | ||||
| 		hb_face_destroy(runtime->hb_face); | ||||
| 		return std::nullopt; | ||||
| 	} | ||||
| 	font_data.atlas = LoadTextureFromImage(font_data.atlas_img); | ||||
| 	if (font_data.atlas.id == 0) { | ||||
| 		UnloadImage(font_data.atlas_img); | ||||
| 		msdfgen::destroyFont(runtime->msdf_font); | ||||
| 		runtime->msdf_font = nullptr; | ||||
| 		hb_font_destroy(runtime->hb_font); | ||||
| 		hb_face_destroy(runtime->hb_face); | ||||
| 		return std::nullopt; | ||||
| 	} | ||||
| 	SetTextureFilter(font_data.atlas, TEXTURE_FILTER_BILINEAR); | ||||
| 	SetTextureWrap(font_data.atlas, TEXTURE_WRAP_CLAMP); | ||||
| 	flush_font(*runtime, font_data); | ||||
|  | ||||
| 	m_font_data.emplace_back(std::move(font_data)); | ||||
| 	m_font_runtime.emplace_back(std::move(runtime)); | ||||
| 	return m_font_data.size() - 1; | ||||
| } | ||||
|  | ||||
| auto TextRenderer::load_font(std::filesystem::path const &path, | ||||
|     std::span<std::filesystem::path const> fallback_fonts) | ||||
|     -> std::optional<FontHandle> | ||||
| { | ||||
| 	// FIXME: Implement. | ||||
| 	auto primary_index = load_single_font(path); | ||||
| 	if (!primary_index) | ||||
| 		return std::nullopt; | ||||
|  | ||||
| 	FontSet set; | ||||
| 	set.font_indices.push_back(*primary_index); | ||||
|  | ||||
| 	for (auto const &fallback_path : fallback_fonts) { | ||||
| 		auto fallback_index = load_single_font(fallback_path); | ||||
| 		if (!fallback_index) { | ||||
| 			TraceLog(LOG_WARNING, "Failed to load fallback font: %s", | ||||
| 			    fallback_path.string().c_str()); | ||||
| 			continue; | ||||
| 		} | ||||
| 		set.font_indices.push_back(*fallback_index); | ||||
| 	} | ||||
|  | ||||
| 	m_font_sets.emplace_back(std::move(set)); | ||||
| 	FontHandle handle; | ||||
| 	handle.id = m_font_sets.size() - 1; | ||||
| 	return handle; | ||||
| } | ||||
|  | ||||
| auto TextRenderer::shape_text(FontHandle const font, | ||||
|     std::string_view const text) -> std::vector<GlyphPlacement> | ||||
| { | ||||
| 	std::vector<GlyphPlacement> shaped; | ||||
| 	if (text.empty()) | ||||
| 		return shaped; | ||||
|  | ||||
| 	usize const handle_id = font(); | ||||
| 	if (handle_id >= m_font_sets.size()) | ||||
| 		return shaped; | ||||
| 	auto const &font_set = m_font_sets[handle_id]; | ||||
| 	if (font_set.font_indices.empty()) | ||||
| 		return shaped; | ||||
|  | ||||
| 	auto codepoints = decode_utf8(text); | ||||
| 	if (codepoints.empty()) | ||||
| 		return shaped; | ||||
|  | ||||
| 	constexpr usize kNoFont = std::numeric_limits<usize>::max(); | ||||
| 	std::vector<usize> selections(codepoints.size(), kNoFont); | ||||
| 	for (usize i = 0; i < codepoints.size(); ++i) { | ||||
| 		bool matched = false; | ||||
| 		for (usize candidate = 0; candidate < font_set.font_indices.size(); | ||||
| 		    ++candidate) { | ||||
| 			usize runtime_index = font_set.font_indices[candidate]; | ||||
| 			if (runtime_index >= m_font_runtime.size()) | ||||
| 				continue; | ||||
| 			auto const &runtime_ptr = m_font_runtime[runtime_index]; | ||||
| 			if (!runtime_ptr || !runtime_ptr->face) | ||||
| 				continue; | ||||
| 			FT_UInt glyph | ||||
| 			    = FT_Get_Char_Index(runtime_ptr->face, codepoints[i].codepoint); | ||||
| 			if (glyph != 0) { | ||||
| 				selections[i] = candidate; | ||||
| 				matched = true; | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
| 		if (!matched) | ||||
| 			selections[i] = kNoFont; | ||||
| 	} | ||||
|  | ||||
| 	usize idx = 0; | ||||
| 	while (idx < codepoints.size()) { | ||||
| 		usize font_choice = selections[idx]; | ||||
| 		if (font_choice == kNoFont) { | ||||
| 			++idx; | ||||
| 			continue; | ||||
| 		} | ||||
| 		if (font_choice >= font_set.font_indices.size()) | ||||
| 			font_choice = 0; | ||||
| 		usize runtime_index = font_set.font_indices[font_choice]; | ||||
| 		if (runtime_index >= m_font_runtime.size() | ||||
| 		    || !m_font_runtime[runtime_index] | ||||
| 		    || !m_font_runtime[runtime_index]->hb_font) { | ||||
| 			++idx; | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		usize segment_start = codepoints[idx].start; | ||||
| 		usize segment_end = codepoints[idx].end; | ||||
| 		usize end_idx = idx + 1; | ||||
| 		while ( | ||||
| 		    end_idx < codepoints.size() && selections[end_idx] == font_choice) { | ||||
| 			segment_end = codepoints[end_idx].end; | ||||
| 			++end_idx; | ||||
| 		} | ||||
|  | ||||
| 		if (segment_end <= segment_start) { | ||||
| 			idx = end_idx; | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		std::string_view segment | ||||
| 		    = text.substr(segment_start, segment_end - segment_start); | ||||
| 		if (segment.empty()) { | ||||
| 			idx = end_idx; | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		hb_buffer_t *buffer = hb_buffer_create(); | ||||
| 		hb_buffer_add_utf8(buffer, segment.data(), | ||||
| 		    static_cast<int>(segment.size()), 0, | ||||
| 		    static_cast<int>(segment.size())); | ||||
| 		hb_buffer_guess_segment_properties(buffer); | ||||
| 		hb_shape(m_font_runtime[runtime_index]->hb_font, buffer, nullptr, 0); | ||||
|  | ||||
| 		unsigned length = hb_buffer_get_length(buffer); | ||||
| 		auto *infos = hb_buffer_get_glyph_infos(buffer, nullptr); | ||||
| 		auto *positions = hb_buffer_get_glyph_positions(buffer, nullptr); | ||||
| 		for (unsigned i = 0; i < length; ++i) { | ||||
| 			GlyphPlacement placement; | ||||
| 			placement.runtime_index = runtime_index; | ||||
| 			placement.glyph_index = infos[i].codepoint; | ||||
| 			placement.x_advance = positions[i].x_advance; | ||||
| 			placement.y_advance = positions[i].y_advance; | ||||
| 			placement.x_offset = positions[i].x_offset; | ||||
| 			placement.y_offset = positions[i].y_offset; | ||||
| 			shaped.emplace_back(placement); | ||||
| 		} | ||||
| 		hb_buffer_destroy(buffer); | ||||
| 		idx = end_idx; | ||||
| 	} | ||||
|  | ||||
| 	return shaped; | ||||
| } | ||||
|  | ||||
| auto TextRenderer::unload_font(FontHandle const font) -> void | ||||
| { | ||||
| 	usize const handle_id = font(); | ||||
| 	if (handle_id >= m_font_sets.size()) | ||||
| 		return; | ||||
|  | ||||
| 	auto &font_set = m_font_sets[handle_id]; | ||||
| 	for (usize runtime_index : font_set.font_indices) { | ||||
| 		if (runtime_index >= m_font_runtime.size()) | ||||
| 			continue; | ||||
|  | ||||
| 		if (auto &runtime_ptr = m_font_runtime[runtime_index]) { | ||||
| 			auto &rt = *runtime_ptr; | ||||
| 			rt.glyph_cache.clear(); | ||||
| 			// No freeing here because they are already cleaned up somewhere... | ||||
| 			// idk. fml. | ||||
| 			rt.face = nullptr; | ||||
| 		} | ||||
| 		m_font_runtime[runtime_index].reset(); | ||||
|  | ||||
| 		if (runtime_index < m_font_data.size()) { | ||||
| 			auto &fd = m_font_data[runtime_index]; | ||||
| 			if (fd.atlas.id != 0) | ||||
| 				UnloadTexture(fd.atlas); | ||||
| 			if (fd.atlas_img.data) | ||||
| 				UnloadImage(fd.atlas_img); | ||||
| 			fd.atlas = Texture2D {}; | ||||
| 			fd.atlas_img = Image {}; | ||||
| 			fd.glyphs.clear(); | ||||
| 		} | ||||
| 	} | ||||
| 	font_set.font_indices.clear(); | ||||
| } | ||||
|  | ||||
| auto find_font_path(std::string_view path) | ||||
| @@ -102,3 +841,5 @@ auto find_font_path(std::string_view path) | ||||
| 	} | ||||
| 	return final_path; | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
|   | ||||
| @@ -1,33 +1,65 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <filesystem> | ||||
| #include <memory> | ||||
| #include <optional> | ||||
| #include <span> | ||||
| #include <unordered_map> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include <raylib.h> | ||||
|  | ||||
| #include "common.hpp" | ||||
|  | ||||
| struct FontHandle { | ||||
| 	auto operator()() const -> auto const & { return id; } | ||||
| struct hb_face_t; | ||||
| struct hb_font_t; | ||||
| struct FT_FaceRec_; | ||||
| using FT_Face = FT_FaceRec_ *; | ||||
|  | ||||
| namespace msdfgen { | ||||
| class FontHandle; | ||||
| } | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| struct FontHandle { // cppcheck-supress noConstructor | ||||
| 	FontHandle() = default; | ||||
|  | ||||
| 	auto operator()() const -> auto const & | ||||
| 	{ | ||||
| 		if (id == 0xffffffff) { | ||||
| 			throw std::runtime_error("Uninitialized FontHandle"); | ||||
| 		} | ||||
| 		return id; | ||||
| 	} | ||||
|  | ||||
| private: | ||||
| 	usize id; | ||||
| 	friend struct TextRenderer; | ||||
| 	usize id { 0xffffffff }; | ||||
| }; | ||||
|  | ||||
| struct FontRuntime; | ||||
|  | ||||
| struct TextRenderer { | ||||
| 	TextRenderer(); // Requires raylib to be initialized! | ||||
| 	~TextRenderer(); | ||||
|  | ||||
| 	TextRenderer(TextRenderer const &) = delete; | ||||
| 	auto operator=(TextRenderer const &) -> TextRenderer & = delete; | ||||
| 	TextRenderer(TextRenderer &&) = default; | ||||
| 	auto operator=(TextRenderer &&) -> TextRenderer & = default; | ||||
|  | ||||
| 	auto measure_text(FontHandle const font, std::string_view const text, | ||||
| 	    int const size = 16) -> Vector2; | ||||
| 	auto draw_text(FontHandle const font, std::string_view const text, | ||||
| 	    Vector2 const pos, int const size = 16, Color const color = WHITE) | ||||
| 	    -> void; | ||||
|  | ||||
| 	auto load_font(std::filesystem::path const &path) | ||||
| 	auto load_font(std::filesystem::path const &path, | ||||
| 	    std::span<std::filesystem::path const> fallback_fonts) | ||||
| 	    -> std::optional<FontHandle>; | ||||
| 	auto unload_font(FontHandle const font); | ||||
| 	auto unload_font(FontHandle const font) -> void; | ||||
|  | ||||
| private: | ||||
| 	struct FontData { | ||||
| @@ -48,9 +80,72 @@ private: | ||||
| 	}; | ||||
|  | ||||
| 	Shader m_msdf_shader; | ||||
| 	int m_px_range_uniform { -1 }; | ||||
|  | ||||
| 	std::vector<FontData> m_font_data; | ||||
| 	struct GlyphCacheEntry { | ||||
| 		FontData::Glyph glyph; | ||||
| 		int atlas_x {}; | ||||
| 		int atlas_y {}; | ||||
| 		int width {}; | ||||
| 		int height {}; | ||||
| 		int stamp {}; | ||||
| 	}; | ||||
|  | ||||
| 	struct FontRuntime { | ||||
| 		FT_Face face {}; | ||||
| 		hb_face_t *hb_face {}; | ||||
| 		hb_font_t *hb_font {}; | ||||
| 		msdfgen::FontHandle *msdf_font {}; | ||||
|  | ||||
| 		int atlas_width {}; | ||||
| 		int atlas_height {}; | ||||
| 		int pen_x {}; | ||||
| 		int pen_y {}; | ||||
| 		int row_height {}; | ||||
| 		float px_range {}; | ||||
| 		float em_scale {}; | ||||
| 		int frame_stamp {}; | ||||
|  | ||||
| 		unsigned units_per_em {}; | ||||
| 		float ascent {}; | ||||
| 		float descent {}; | ||||
| 		float line_gap {}; | ||||
|  | ||||
| 		std::unordered_map<u32, GlyphCacheEntry> glyph_cache; | ||||
| 	}; | ||||
|  | ||||
| 	std::vector<std::unique_ptr<FontRuntime>> m_font_runtime; | ||||
| 	struct FontSet { | ||||
| 		std::vector<usize> font_indices; | ||||
| 	}; | ||||
| 	std::vector<FontSet> m_font_sets; | ||||
|  | ||||
| 	struct GlyphPlacement { | ||||
| 		usize runtime_index {}; | ||||
| 		u32 glyph_index {}; | ||||
| 		i32 x_advance {}; | ||||
| 		i32 y_advance {}; | ||||
| 		i32 x_offset {}; | ||||
| 		i32 y_offset {}; | ||||
| 	}; | ||||
|  | ||||
| 	static auto flush_font(FontRuntime &rt, FontData &fd) -> void; | ||||
| 	static auto allocate_region(FontRuntime &rt, FontData &fd, int width, | ||||
| 	    int height) -> std::optional<std::pair<int, int>>; | ||||
| 	static auto upload_region(FontData &fd, int dst_x, int dst_y, int width, | ||||
| 	    int height, std::vector<Color> const &buffer) -> void; | ||||
| 	static auto generate_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index) | ||||
| 	    -> std::optional<GlyphCacheEntry>; | ||||
| 	static auto ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index, | ||||
| 	    bool mark_usage) -> GlyphCacheEntry *; | ||||
| 	auto load_single_font(std::filesystem::path const &path) | ||||
| 	    -> std::optional<usize>; | ||||
| 	auto shape_text(FontHandle const font, std::string_view const text) | ||||
| 	    -> std::vector<GlyphPlacement>; | ||||
| }; | ||||
|  | ||||
| auto find_font_path(std::string_view path = "sans-serif:style=Regular") | ||||
|     -> std::optional<std::filesystem::path>; | ||||
|  | ||||
| } // namespace Waylight | ||||
|   | ||||
| @@ -4,7 +4,11 @@ | ||||
|  | ||||
| #include "enum_array.hpp" | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| struct ColorScheme { | ||||
| 	Color foreground; | ||||
| 	Color foreground_preedit; | ||||
| 	struct { | ||||
| 		Color background; | ||||
| 	} window; | ||||
| @@ -21,12 +25,16 @@ constexpr auto make_default_themes() -> enum_array<Theme, ColorScheme> const | ||||
| { | ||||
| 	enum_array<Theme, ColorScheme> array; | ||||
| 	array[Theme::Light] = { | ||||
| 	    .foreground = { 0, 0, 0, 255 }, | ||||
| 	    .foreground_preedit = { 0, 0, 0, 255 }, | ||||
| 	    .window = | ||||
| 	        { | ||||
| 	            .background = { 255, 255, 255, 100 }, | ||||
| 	        }, | ||||
| 	}; | ||||
| 	array[Theme::Dark] = { | ||||
| 	    .foreground = { 255, 255, 255, 255 }, | ||||
| 	    .foreground_preedit = { 255, 255, 255, 255 }, | ||||
| 	    .window = | ||||
| 	        { | ||||
| 	            .background = { 0, 0, 0, 100 }, | ||||
| @@ -34,3 +42,5 @@ constexpr auto make_default_themes() -> enum_array<Theme, ColorScheme> const | ||||
| 	}; | ||||
| 	return array; | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
|   | ||||
							
								
								
									
										68
									
								
								src/Tick.cpp
									
									
									
									
									
								
							
							
						
						
									
										68
									
								
								src/Tick.cpp
									
									
									
									
									
								
							| @@ -1,26 +1,31 @@ | ||||
| #include "App.hpp" | ||||
|  | ||||
| #include <cassert> | ||||
|  | ||||
| #include <EGL/egl.h> | ||||
| #include <GLES3/gl3.h> | ||||
| #include <print> | ||||
| #include <raylib.h> | ||||
| #include <rlgl.h> | ||||
| #include <xkbcommon/xkbcommon.h> | ||||
|  | ||||
| #include <optional> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| auto App::tick() -> void | ||||
| { | ||||
| 	static std::pmr::string text_input_data {}; | ||||
|  | ||||
| 	m_ime.bound_text = &text_input_data; | ||||
| 	m_ime.bound_id = 1; | ||||
| 	process_pending_text_input(); | ||||
|  | ||||
| 	if (!m_visible || m_gl.edpy == EGL_NO_DISPLAY | ||||
| 	    || m_gl.esurf == EGL_NO_SURFACE) | ||||
| 		return; | ||||
|  | ||||
| 	glViewport(0, 0, m_win_w, m_win_h); | ||||
|  | ||||
| 	for (auto const cp : m_kbd.typing) { | ||||
| 		std::println("Char typed: {} ({})  shift={} ctrl={}", | ||||
| 		    rune_to_string(cp), cp, m_kbd.shift() ? 'y' : 'n', | ||||
| 		    m_kbd.ctrl() ? 'y' : 'n'); | ||||
| 	} | ||||
|  | ||||
| 	if (m_kbd.is_sym_pressed(XKB_KEY_Escape)) { | ||||
| 		set_visible(!visible()); | ||||
| 		if (m_kbd.ctrl() && m_kbd.shift()) { | ||||
| @@ -30,13 +35,54 @@ auto App::tick() -> void | ||||
|  | ||||
| 	BeginDrawing(); | ||||
|  | ||||
| 	ClearBackground(BLANK); | ||||
| 	ClearBackground(theme().window.background); | ||||
|  | ||||
| 	DrawFPS(10, 10); | ||||
| 	{ | ||||
| 		assert(m_gui); | ||||
|  | ||||
| 		m_gui->style().preedit_color = theme().foreground_preedit; | ||||
| 		m_gui->style().text_color = theme().foreground; | ||||
| 		m_gui->style().selection_color = m_accent_color; | ||||
| 		m_gui->style().selection_text_color = WHITE; | ||||
|  | ||||
| 		u32 rune { 0 }; | ||||
| 		if (!m_kbd.typing.empty()) { | ||||
| 			rune = m_kbd.typing.back(); | ||||
| 			m_kbd.typing.clear(); | ||||
| 		} | ||||
| 		ImGuiGuard gui_scope(m_gui, rune, m_kbd.ctrl(), m_kbd.shift(), | ||||
| 		    clipboard(), | ||||
| 		    [this](std::string_view const &str) { clipboard(str); }); | ||||
|  | ||||
| 		Rectangle const input_rect { | ||||
| 			0.0f, | ||||
| 			0.0f, | ||||
| 			static_cast<float>(GetScreenWidth()), | ||||
| 			static_cast<float>(GetScreenHeight()), | ||||
| 		}; | ||||
| 		; | ||||
| 		if (auto const result | ||||
| 		    = m_gui->text_input(1, text_input_data, input_rect); | ||||
| 		    result.test(1)) { | ||||
| 			m_ime.surrounding_dirty = true; | ||||
| 		} else if (result.test(0)) { | ||||
| 			if (text_input_data == "kitty") { | ||||
| 				execute_command(false, "kitty"); | ||||
| 			} else if (text_input_data == "nvim") { | ||||
| 				execute_command(true, "nvim"); | ||||
| 			} | ||||
|  | ||||
| 			text_input_data = ""; | ||||
| 		} | ||||
|  | ||||
| 		update_text_input_state(text_input_data, 1, input_rect); | ||||
| 	} | ||||
|  | ||||
| 	DrawTexture(m_ir.lookup("folder", 48).texture(), 48, 48, WHITE); | ||||
|  | ||||
| 	EndDrawing(); | ||||
|  | ||||
| 	eglSwapBuffers(m_gl.edpy, m_gl.esurf); | ||||
| 	m_kbd.typing.clear(); | ||||
| 	m_kbd.clear_transients(); | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
|   | ||||
							
								
								
									
										19
									
								
								src/base.vert
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/base.vert
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| #version 100 | ||||
|  | ||||
| attribute vec3 vertexPosition; | ||||
| attribute vec2 vertexTexCoord; | ||||
| attribute vec3 vertexNormal; | ||||
| attribute vec4 vertexColor; | ||||
|  | ||||
| uniform mat4 mvp; | ||||
|  | ||||
| varying vec2 fragTexCoord; | ||||
| varying vec4 fragColor; | ||||
|  | ||||
| void main() | ||||
| { | ||||
|     fragTexCoord = vertexTexCoord; | ||||
|     fragColor = vertexColor; | ||||
|  | ||||
|     gl_Position = mvp*vec4(vertexPosition, 1.0); | ||||
| } | ||||
| @@ -1,7 +1,11 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <array> | ||||
| #include <cstdint> | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| using u8 = std::uint8_t; | ||||
| using i8 = std::int8_t; | ||||
| using u16 = std::uint16_t; | ||||
| @@ -15,9 +19,8 @@ using isize = std::intptr_t; | ||||
|  | ||||
| [[maybe_unused]] static inline auto rune_to_string(uint32_t cp) -> char const * | ||||
| { | ||||
| 	static char utf8[5] = { 0 }; | ||||
| 	for (auto &c : utf8) | ||||
| 		c = 0; | ||||
| 	static std::array<char, 5> utf8 {}; | ||||
| 	std::fill(utf8.begin(), utf8.end(), 0); | ||||
|  | ||||
| 	if (cp < 0x80) { | ||||
| 		utf8[0] = cp; | ||||
| @@ -35,5 +38,7 @@ using isize = std::intptr_t; | ||||
| 		utf8[3] = 0x80 | (cp & 0x3F); | ||||
| 	} | ||||
|  | ||||
| 	return utf8; | ||||
| 	return utf8.data(); | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
|   | ||||
| @@ -5,28 +5,31 @@ | ||||
| #include <stdexcept> | ||||
| #include <type_traits> | ||||
|  | ||||
| #include "common.hpp" | ||||
|  | ||||
| namespace Waylight { | ||||
|  | ||||
| template<class E> struct enum_traits; | ||||
|  | ||||
| template<class E> | ||||
| concept EnumLike = std::is_enum_v<E>; | ||||
|  | ||||
| template<EnumLike E> | ||||
| constexpr std::size_t enum_count_v | ||||
|     = static_cast<std::size_t>(enum_traits<E>::last) | ||||
|     - static_cast<std::size_t>(enum_traits<E>::first) + 1; | ||||
| constexpr usize enum_count_v = static_cast<usize>(enum_traits<E>::last) | ||||
|     - static_cast<usize>(enum_traits<E>::first) + 1; | ||||
|  | ||||
| template<EnumLike E, class T> struct enum_array { | ||||
| 	using value_type = T; | ||||
| 	using enum_type = E; | ||||
| 	using underlying_index_type = std::size_t; | ||||
| 	using underlying_index_type = usize; | ||||
|  | ||||
| 	static constexpr E first = enum_traits<E>::first; | ||||
| 	static constexpr E last = enum_traits<E>::last; | ||||
| 	static constexpr std::size_t size_value = enum_count_v<E>; | ||||
| 	static constexpr usize size_value = enum_count_v<E>; | ||||
|  | ||||
| 	std::array<T, size_value> _data {}; | ||||
|  | ||||
| 	static constexpr std::size_t size() noexcept { return size_value; } | ||||
| 	static constexpr usize size() noexcept { return size_value; } | ||||
| 	constexpr T *data() noexcept { return _data.data(); } | ||||
| 	constexpr T const *data() const noexcept { return _data.data(); } | ||||
| 	constexpr T *begin() noexcept { return _data.begin().operator->(); } | ||||
| @@ -61,9 +64,9 @@ template<EnumLike E, class T> struct enum_array { | ||||
| 	constexpr void fill(T const &v) { _data.fill(v); } | ||||
|  | ||||
| private: | ||||
| 	static constexpr std::size_t to_index(E e) noexcept | ||||
| 	static constexpr usize to_index(E e) noexcept | ||||
| 	{ | ||||
| 		return static_cast<std::size_t>(e) - static_cast<std::size_t>(first); | ||||
| 		return static_cast<usize>(e) - static_cast<usize>(first); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| @@ -77,3 +80,5 @@ constexpr auto make_enum_array(T &&first_val, U &&...rest) | ||||
| 	arr._data = { std::forward<T>(first_val), std::forward<U>(rest)... }; | ||||
| 	return arr; | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
|   | ||||
							
								
								
									
										72
									
								
								src/main.cpp
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								src/main.cpp
									
									
									
									
									
								
							| @@ -1,35 +1,31 @@ | ||||
| #include <algorithm> | ||||
| #include <csignal> | ||||
| #include <cstdio> | ||||
| #include <cstdlib> | ||||
| #include <fcntl.h> | ||||
| #include <iostream> | ||||
| #include <optional> | ||||
| #include <print> | ||||
| #include <signal.h> | ||||
| #include <sys/file.h> | ||||
| #include <sys/types.h> | ||||
| #include <unistd.h> | ||||
|  | ||||
| #include <cpptrace/cpptrace.hpp> | ||||
| #include <cpptrace/from_current.hpp> | ||||
| #include <tinyfiledialogs.h> | ||||
|  | ||||
| #include "App.hpp" | ||||
|  | ||||
| bool check_or_signal_running(); | ||||
| namespace Waylight { | ||||
|  | ||||
| bool signal_running(); | ||||
|  | ||||
| std::optional<App> g_app {}; | ||||
|  | ||||
| auto main() -> int { | ||||
|   if (check_or_signal_running()) { | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   std::signal(SIGINT, [](int) { | ||||
|     if (g_app) | ||||
|       g_app->stop(); | ||||
|   }); | ||||
|  | ||||
|   g_app.emplace(); | ||||
|   g_app->run(); | ||||
| } | ||||
|  | ||||
| bool check_or_signal_running() { | ||||
|   const char *lock_path = "/tmp/waylight.lock"; | ||||
| bool signal_running() | ||||
| { | ||||
| 	char const *lock_path = "/tmp/waylight.lock"; | ||||
| 	int fd = open(lock_path, O_CREAT | O_RDWR, 0666); | ||||
| 	if (fd == -1) | ||||
| 		return false; | ||||
| @@ -46,7 +42,47 @@ bool check_or_signal_running() { | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
|   ftruncate(fd, 0); | ||||
| 	if (ftruncate(fd, 0) == -1) { | ||||
| 		close(fd); | ||||
| 		unlink(lock_path); | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	dprintf(fd, "%d\n", getpid()); | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| } // namespace Waylight | ||||
|  | ||||
| auto main() -> int | ||||
| { | ||||
| 	if (Waylight::signal_running()) { | ||||
| 		return 0; | ||||
| 	} | ||||
|  | ||||
| 	std::signal(SIGINT, [](int) { | ||||
| 		if (Waylight::g_app) | ||||
| 			Waylight::g_app->stop(); | ||||
| 	}); | ||||
|  | ||||
| 	CPPTRACE_TRY | ||||
| 	{ | ||||
| 		Waylight::g_app.emplace(); | ||||
| 		Waylight::g_app->run(); | ||||
| 	} | ||||
| 	CPPTRACE_CATCH(std::exception const &e) | ||||
| 	{ | ||||
| 		std::string what { e.what() }; | ||||
| 		std::ranges::replace(what, '"', '.'); | ||||
| 		std::ranges::replace(what, '\'', '.'); | ||||
| 		std::ranges::replace(what, '`', '.'); | ||||
| 		if (what.empty()) { | ||||
| 			std::println(std::cerr, "Unexpected exception!"); | ||||
| 		} else { | ||||
| 			std::println(std::cerr, "Unexpected exception! Error: {}", what); | ||||
| 		} | ||||
| 		cpptrace::from_current_exception().print(); | ||||
| 		tinyfd_messageBox( | ||||
| 		    "Unexpected exception", what.c_str(), "ok", "error", 1); | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										5
									
								
								todo.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								todo.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| - [-] Fix text moving left and right based on cursor position (right side of cursor is too left) | ||||
| 	- [ ] Fix selection using wrong offset for text | ||||
| - [x] Implement selection in ImGui::text_edit | ||||
| - [x] Implement clipboard copy/paste | ||||
|  | ||||
							
								
								
									
										1
									
								
								vendor/CMakeLists.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								vendor/CMakeLists.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| add_subdirectory(tinyfiledialogs) | ||||
							
								
								
									
										8
									
								
								vendor/tinyfiledialogs/CMakeLists.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								vendor/tinyfiledialogs/CMakeLists.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| cmake_minimum_required(VERSION 3.30) | ||||
|  | ||||
| project(tinyfiledialogs C) | ||||
|  | ||||
| add_library(${PROJECT_NAME} STATIC tinyfiledialogs.c) | ||||
|  | ||||
| target_include_directories(${PROJECT_NAME} PUBLIC include) | ||||
|  | ||||
							
								
								
									
										314
									
								
								vendor/tinyfiledialogs/include/tinyfiledialogs.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								vendor/tinyfiledialogs/include/tinyfiledialogs.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,314 @@ | ||||
| /* SPDX-License-Identifier: Zlib | ||||
| Copyright (c) 2014 - 2025 Guillaume Vareille http://ysengrin.com | ||||
| 	 ____________________________________________________________________ | ||||
| 	|                                                                    | | ||||
| 	| 100% compatible C C++  ->  You can rename tinfiledialogs.c as .cpp | | ||||
| 	|____________________________________________________________________| | ||||
|  | ||||
| ********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE ********* | ||||
|   _________ | ||||
|  /         \ tinyfiledialogs.h v3.21.1 [Oct 5, 2025] | ||||
|  |tiny file| Unique header file created [November 9, 2014] | ||||
|  | dialogs | | ||||
|  \____  ___/ http://tinyfiledialogs.sourceforge.net | ||||
|       \|     git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd | ||||
|  ____________________________________________ | ||||
| |                                            | | ||||
| |   email: tinyfiledialogs at ysengrin.com   | | ||||
| |____________________________________________| | ||||
|  ________________________________________________________________________________ | ||||
| |  ____________________________________________________________________________  | | ||||
| | |                                                                            | | | ||||
| | |  - in tinyfiledialogs, char is UTF-8 by default (since v3.6)               | | | ||||
| | |                                                                            | | | ||||
| | | on windows:                                                                | | | ||||
| | |  - for UTF-16, use the wchar_t functions at the bottom of the header file  | | | ||||
| | |                                                                            | | | ||||
| | |  - _wfopen() requires wchar_t                                              | | | ||||
| | |  - fopen() uses char but expects ASCII or MBCS (not UTF-8)                 | | | ||||
| | |  - if you want char to be MBCS: set tinyfd_winUtf8 to 0                    | | | ||||
| | |                                                                            | | | ||||
| | |  - alternatively, tinyfiledialogs provides                                 | | | ||||
| | |                        functions to convert between UTF-8, UTF-16 and MBCS | | | ||||
| | |____________________________________________________________________________| | | ||||
| |________________________________________________________________________________| | ||||
|  | ||||
| If you like tinyfiledialogs, please upvote my stackoverflow answer | ||||
| https://stackoverflow.com/a/47651444 | ||||
|  | ||||
| - License - | ||||
| This software is provided 'as-is', without any express or implied | ||||
| warranty.  In no event will the authors be held liable for any damages | ||||
| arising from the use of this software. | ||||
| Permission is granted to anyone to use this software for any purpose, | ||||
| including commercial applications, and to alter it and redistribute it | ||||
| freely, subject to the following restrictions: | ||||
| 1. The origin of this software must not be misrepresented; you must not | ||||
| claim that you wrote the original software.  If you use this software | ||||
| in a product, an acknowledgment in the product documentation would be | ||||
| appreciated but is not required. | ||||
| 2. Altered source versions must be plainly marked as such, and must not be | ||||
| misrepresented as being the original software. | ||||
| 3. This notice may not be removed or altered from any source distribution. | ||||
|  | ||||
|      __________________________________________ | ||||
|     |  ______________________________________  | | ||||
|     | |                                      | | | ||||
|     | | DO NOT USE USER INPUT IN THE DIALOGS | | | ||||
|     | |______________________________________| | | ||||
|     |__________________________________________| | ||||
| */ | ||||
|  | ||||
| #ifndef TINYFILEDIALOGS_H | ||||
| #define TINYFILEDIALOGS_H | ||||
|  | ||||
| #ifdef	__cplusplus | ||||
| extern "C" { | ||||
| #endif | ||||
|  | ||||
| /******************************************************************************************************/ | ||||
| /**************************************** UTF-8 on Windows ********************************************/ | ||||
| /******************************************************************************************************/ | ||||
| #ifdef _WIN32 | ||||
| /* On windows, if you want to use UTF-8 ( instead of the UTF-16/wchar_t functions at the end of this file ) | ||||
| Make sure your code is really prepared for UTF-8 (on windows, functions like fopen() expect MBCS and not UTF-8) */ | ||||
| extern int tinyfd_winUtf8; /* on windows char strings can be 1:UTF-8(default) or 0:MBCS */ | ||||
| /* for MBCS change this to 0, in tinyfiledialogs.c or in your code */ | ||||
|  | ||||
| /* Here are some functions to help you convert between UTF-16 UTF-8 MBSC */ | ||||
| char * tinyfd_utf8toMbcs(char const * aUtf8string); | ||||
| char * tinyfd_utf16toMbcs(wchar_t const * aUtf16string); | ||||
| wchar_t * tinyfd_mbcsTo16(char const * aMbcsString); | ||||
| char * tinyfd_mbcsTo8(char const * aMbcsString); | ||||
| wchar_t * tinyfd_utf8to16(char const * aUtf8string); | ||||
| char * tinyfd_utf16to8(wchar_t const * aUtf16string); | ||||
| #endif | ||||
| /******************************************************************************************************/ | ||||
| /******************************************************************************************************/ | ||||
| /******************************************************************************************************/ | ||||
|  | ||||
| /************* 3 funtions for C# (you don't need this in C or C++) : */ | ||||
| char const * tinyfd_getGlobalChar(char const * aCharVariableName); /* returns NULL on error */ | ||||
| int tinyfd_getGlobalInt(char const * aIntVariableName); /* returns -1 on error */ | ||||
| int tinyfd_setGlobalInt(char const * aIntVariableName, int aValue); /* returns -1 on error */ | ||||
| /* aCharVariableName: "tinyfd_version" "tinyfd_needs" "tinyfd_response" | ||||
|    aIntVariableName : "tinyfd_verbose" "tinyfd_silent" "tinyfd_allowCursesDialogs" | ||||
| 				      "tinyfd_forceConsole" "tinyfd_assumeGraphicDisplay" "tinyfd_winUtf8" | ||||
| **************/ | ||||
|  | ||||
| extern char tinyfd_version[8]; /* contains tinyfd current version number */ | ||||
| extern char tinyfd_needs[]; /* info about requirements */ | ||||
| extern int tinyfd_verbose; /* 0 (default) or 1 : on unix, prints the command line calls */ | ||||
| extern int tinyfd_silent; /* 1 (default) or 0 : on unix, hide errors and warnings from called dialogs */ | ||||
|  | ||||
| /** Curses dialogs are difficult to use and counter-intuitive. | ||||
| On windows they are only ascii and still uses the unix backslash ! **/ | ||||
| extern int tinyfd_allowCursesDialogs; /* 0 (default) or 1 */ | ||||
|  | ||||
| extern int tinyfd_forceConsole;  /* 0 (default) or 1 */ | ||||
| /* for unix & windows: 0 (graphic mode) or 1 (console mode). | ||||
| 0: try to use a graphic solution, if it fails then it uses console mode. | ||||
| 1: forces all dialogs into console mode even when an X server is present. | ||||
|    if enabled, it can use the package Dialog or dialog.exe. | ||||
|    on windows it only make sense for console applications */ | ||||
|  | ||||
| /* extern int tinyfd_assumeGraphicDisplay; */ /* 0 (default) or 1  */ | ||||
| /* some systems don't set the environment variable DISPLAY even when a graphic display is present. | ||||
| set this to 1 to tell tinyfiledialogs to assume the existence of a graphic display */ | ||||
|  | ||||
| extern char tinyfd_response[1024]; | ||||
| /* if you pass "tinyfd_query" as aTitle, | ||||
| the functions will not display the dialogs | ||||
| but will return 0 for console mode, 1 for graphic mode. | ||||
| tinyfd_response is then filled with the retain solution. | ||||
| possible values for tinyfd_response are (all lowercase) | ||||
| for graphic mode: | ||||
|   windows_wchar windows applescript kdialog zenity zenity3 yad matedialog | ||||
|   shellementary qarma shanty boxer python2-tkinter python3-tkinter python-dbus | ||||
|   perl-dbus gxmessage gmessage xmessage xdialog gdialog dunst | ||||
| for console mode: | ||||
|   dialog whiptail basicinput no_solution */ | ||||
|  | ||||
| void tinyfd_beep(void); | ||||
|  | ||||
| int tinyfd_notifyPopup( | ||||
| 	char const * aTitle, /* NULL or "" */ | ||||
| 	char const * aMessage, /* NULL or "" may contain \n \t */ | ||||
| 	char const * aIconType); /* "info" "warning" "error" */ | ||||
| 		/* return has only meaning for tinyfd_query */ | ||||
|  | ||||
| int tinyfd_messageBox( | ||||
| 	char const * aTitle , /* NULL or "" */ | ||||
| 	char const * aMessage , /* NULL or "" may contain \n \t */ | ||||
| 	char const * aDialogType , /* "ok" "okcancel" "yesno" "yesnocancel" */ | ||||
| 	char const * aIconType , /* "info" "warning" "error" "question" */ | ||||
| 	int aDefaultButton ) ; | ||||
| 		/* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */ | ||||
|  | ||||
| char * tinyfd_inputBox( | ||||
| 	char const * aTitle , /* NULL or "" */ | ||||
| 	char const * aMessage , /* NULL or "" (\n and \t have no effect) */ | ||||
| 	char const * aDefaultInput ) ;  /* NULL = passwordBox, "" = inputbox */ | ||||
| 		/* returns NULL on cancel */ | ||||
|  | ||||
| char * tinyfd_saveFileDialog( | ||||
| 	char const * aTitle , /* NULL or "" */ | ||||
| 	char const * aDefaultPathAndOrFile , /* NULL or "" , ends with / to set only a directory */ | ||||
| 	int aNumOfFilterPatterns , /* 0  (1 in the following example) */ | ||||
| 	char const * const * aFilterPatterns , /* NULL or char const * lFilterPatterns[1]={"*.txt"} */ | ||||
| 	char const * aSingleFilterDescription ) ; /* NULL or "text files" */ | ||||
| 		/* returns NULL on cancel */ | ||||
|  | ||||
| char * tinyfd_openFileDialog( | ||||
| 	char const * aTitle, /* NULL or "" */ | ||||
| 	char const * aDefaultPathAndOrFile, /* NULL or "" , ends with / to set only a directory */ | ||||
| 	int aNumOfFilterPatterns , /* 0 (2 in the following example) */ | ||||
| 	char const * const * aFilterPatterns, /* NULL or char const * lFilterPatterns[2]={"*.png","*.jpg"}; */ | ||||
| 	char const * aSingleFilterDescription, /* NULL or "image files" */ | ||||
| 	int aAllowMultipleSelects ) ; /* 0 or 1 */ | ||||
| 		/* in case of multiple files, the separator is | */ | ||||
| 		/* returns NULL on cancel */ | ||||
|  | ||||
| char * tinyfd_selectFolderDialog( | ||||
| 	char const * aTitle, /* NULL or "" */ | ||||
| 	char const * aDefaultPath); /* NULL or "" */ | ||||
| 		/* returns NULL on cancel */ | ||||
|  | ||||
| char * tinyfd_colorChooser( | ||||
| 	char const * aTitle, /* NULL or "" */ | ||||
| 	char const * aDefaultHexRGB, /* NULL or "" or "#FF0000" */ | ||||
| 	unsigned char const aDefaultRGB[3] , /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */ | ||||
| 	unsigned char aoResultRGB[3] ) ; /* unsigned char lResultRGB[3]; */ | ||||
| 		/* aDefaultRGB is used only if aDefaultHexRGB is absent */ | ||||
| 		/* aDefaultRGB and aoResultRGB can be the same array */ | ||||
| 		/* returns NULL on cancel */ | ||||
| 		/* returns the hexcolor as a string "#FF0000" */ | ||||
| 		/* aoResultRGB also contains the result */ | ||||
|  | ||||
|  | ||||
| /************ WINDOWS ONLY SECTION ************************/ | ||||
| #ifdef _WIN32 | ||||
|  | ||||
| /* windows only - utf-16 version */ | ||||
| int tinyfd_notifyPopupW( | ||||
| 	wchar_t const * aTitle, /* NULL or L"" */ | ||||
| 	wchar_t const * aMessage, /* NULL or L"" may contain \n \t */ | ||||
| 	wchar_t const * aIconType); /* L"info" L"warning" L"error" */ | ||||
|  | ||||
| /* windows only - utf-16 version */ | ||||
| int tinyfd_messageBoxW( | ||||
| 	wchar_t const * aTitle, /* NULL or L"" */ | ||||
| 	wchar_t const * aMessage, /* NULL or L"" may contain \n \t */ | ||||
| 	wchar_t const * aDialogType, /* L"ok" L"okcancel" L"yesno" */ | ||||
| 	wchar_t const * aIconType, /* L"info" L"warning" L"error" L"question" */ | ||||
| 	int aDefaultButton ); /* 0 for cancel/no , 1 for ok/yes */ | ||||
| 		/* returns 0 for cancel/no , 1 for ok/yes */ | ||||
|  | ||||
| /* windows only - utf-16 version */ | ||||
| wchar_t * tinyfd_inputBoxW( | ||||
| 	wchar_t const * aTitle, /* NULL or L"" */ | ||||
| 	wchar_t const * aMessage, /* NULL or L"" (\n nor \t not respected) */ | ||||
| 	wchar_t const * aDefaultInput); /* NULL passwordBox, L"" inputbox */ | ||||
|  | ||||
| /* windows only - utf-16 version */ | ||||
| wchar_t * tinyfd_saveFileDialogW( | ||||
| 	wchar_t const * aTitle, /* NULL or L"" */ | ||||
| 	wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */ | ||||
| 	int aNumOfFilterPatterns, /* 0 (1 in the following example) */ | ||||
| 	wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[1]={L"*.txt"} */ | ||||
| 	wchar_t const * aSingleFilterDescription); /* NULL or L"text files" */ | ||||
| 		/* returns NULL on cancel */ | ||||
|  | ||||
| /* windows only - utf-16 version */ | ||||
| wchar_t * tinyfd_openFileDialogW( | ||||
| 	wchar_t const * aTitle, /* NULL or L"" */ | ||||
| 	wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */ | ||||
| 	int aNumOfFilterPatterns , /* 0 (2 in the following example) */ | ||||
| 	wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[2]={L"*.png","*.jpg"} */ | ||||
| 	wchar_t const * aSingleFilterDescription, /* NULL or L"image files" */ | ||||
| 	int aAllowMultipleSelects ) ; /* 0 or 1 */ | ||||
| 		/* in case of multiple files, the separator is | */ | ||||
| 		/* returns NULL on cancel */ | ||||
|  | ||||
| /* windows only - utf-16 version */ | ||||
| wchar_t * tinyfd_selectFolderDialogW( | ||||
| 	wchar_t const * aTitle, /* NULL or L"" */ | ||||
| 	wchar_t const * aDefaultPath); /* NULL or L"" */ | ||||
| 		/* returns NULL on cancel */ | ||||
|  | ||||
| /* windows only - utf-16 version */ | ||||
| wchar_t * tinyfd_colorChooserW( | ||||
| 	wchar_t const * aTitle, /* NULL or L"" */ | ||||
| 	wchar_t const * aDefaultHexRGB, /* NULL or L"#FF0000" */ | ||||
| 	unsigned char const aDefaultRGB[3], /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */ | ||||
| 	unsigned char aoResultRGB[3]); /* unsigned char lResultRGB[3]; */ | ||||
| 		/* returns the hexcolor as a string L"#FF0000" */ | ||||
| 		/* aoResultRGB also contains the result */ | ||||
| 		/* aDefaultRGB is used only if aDefaultHexRGB is NULL */ | ||||
| 		/* aDefaultRGB and aoResultRGB can be the same array */ | ||||
| 		/* returns NULL on cancel */ | ||||
|  | ||||
| #endif /*_WIN32 */ | ||||
|  | ||||
| #ifdef	__cplusplus | ||||
| } /*extern "C"*/ | ||||
| #endif | ||||
|  | ||||
| #endif /* TINYFILEDIALOGS_H */ | ||||
|  | ||||
| /* | ||||
|  ________________________________________________________________________________ | ||||
| |  ____________________________________________________________________________  | | ||||
| | |                                                                            | | | ||||
| | | on windows:                                                                | | | ||||
| | |  - for UTF-16, use the wchar_t functions at the bottom of the header file  | | | ||||
| | |  - _wfopen() requires wchar_t                                              | | | ||||
| | |                                                                            | | | ||||
| | |  - in tinyfiledialogs, char is UTF-8 by default (since v3.6)               | | | ||||
| | |  - but fopen() expects MBCS (not UTF-8)                                    | | | ||||
| | |  - if you want char to be MBCS: set tinyfd_winUtf8 to 0                    | | | ||||
| | |                                                                            | | | ||||
| | |  - alternatively, tinyfiledialogs provides                                 | | | ||||
| | |                        functions to convert between UTF-8, UTF-16 and MBCS | | | ||||
| | |____________________________________________________________________________| | | ||||
| |________________________________________________________________________________| | ||||
|  | ||||
| - This is not for ios nor android (it works in termux though). | ||||
| - The files can be renamed with extension ".cpp" as the code is 100% compatible C C++ | ||||
|   (just comment out << extern "C" >> in the header file) | ||||
| - Windows is fully supported from XP to 10 (maybe even older versions) | ||||
| - C# & LUA via dll, see files in the folder EXTRAS | ||||
| - OSX supported from 10.4 to latest (maybe even older versions) | ||||
| - Do not use " and ' as the dialogs will be displayed with a warning | ||||
|   instead of the title, message, etc... | ||||
| - There's one file filter only, it may contain several patterns. | ||||
| - If no filter description is provided, | ||||
|   the list of patterns will become the description. | ||||
| - On windows link against Comdlg32.lib and Ole32.lib | ||||
|   (on windows the no linking claim is a lie) | ||||
| - On unix: it tries command line calls, so no such need (NO LINKING). | ||||
| - On unix you need one of the following: | ||||
|   applescript, kdialog, zenity, matedialog, shellementary, qarma, shanty, boxer, | ||||
|   yad, python (2 or 3)/tkinter/python-dbus (optional), Xdialog | ||||
|   or curses dialogs (opens terminal if running without console). | ||||
| - One of those is already included on most (if not all) desktops. | ||||
| - In the absence of those it will use gdialog, gxmessage or whiptail | ||||
|   with a textinputbox. If nothing is found, it switches to basic console input, | ||||
|   it opens a console if needed (requires xterm + bash). | ||||
| - for curses dialogs you must set tinyfd_allowCursesDialogs=1 | ||||
| - You can query the type of dialog that will be used (pass "tinyfd_query" as aTitle) | ||||
| - String memory is preallocated statically for all the returned values. | ||||
| - File and path names are tested before return, they should be valid. | ||||
| - tinyfd_forceConsole=1; at run time, forces dialogs into console mode. | ||||
| - On windows, console mode only make sense for console applications. | ||||
| - On windows, console mode is not implemented for wchar_T UTF-16. | ||||
| - Mutiple selects are not possible in console mode. | ||||
| - The package dialog must be installed to run in curses dialogs in console mode. | ||||
|   It is already installed on most unix systems. | ||||
| - On osx, the package dialog can be installed via | ||||
|   http://macappstore.org/dialog or http://macports.org | ||||
| - On windows, for curses dialogs console mode, | ||||
|   dialog.exe should be copied somewhere on your executable path. | ||||
|   It can be found at the bottom of the following page: | ||||
|   http://andrear.altervista.org/home/cdialog.php | ||||
| */ | ||||
							
								
								
									
										8397
									
								
								vendor/tinyfiledialogs/tinyfiledialogs.c
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8397
									
								
								vendor/tinyfiledialogs/tinyfiledialogs.c
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user