Compare commits
	
		
			35 Commits
		
	
	
		
			596f24becd
			...
			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 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,5 @@ | |||||||
| [Bb]uild* | [Bb]uild* | ||||||
|  | CMakeLists.txt.* | ||||||
| result | result | ||||||
| .cache | .cache | ||||||
| .direnv | .direnv | ||||||
|   | |||||||
| @@ -1,10 +1,25 @@ | |||||||
| cmake_minimum_required(VERSION 3.16) | cmake_minimum_required(VERSION 3.16) | ||||||
|  |  | ||||||
| project(waylight LANGUAGES C CXX) | project(waylight LANGUAGES C CXX) | ||||||
|  |  | ||||||
| set(CMAKE_CXX_STANDARD 23) | set(CMAKE_CXX_STANDARD 23) | ||||||
| set(CMAKE_CXX_STANDARD_REQUIRED ON) | set(CMAKE_CXX_STANDARD_REQUIRED ON) | ||||||
| set(CMAKE_POSITION_INDEPENDENT_CODE 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) | find_package(PkgConfig REQUIRED) | ||||||
| pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client) | pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client) | ||||||
| pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl) | pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl) | ||||||
| @@ -44,6 +59,48 @@ set(MSDFGEN_DISABLE_SVG ON) | |||||||
| set(MSDFGEN_DISABLE_PNG ON) | set(MSDFGEN_DISABLE_PNG ON) | ||||||
| FetchContent_MakeAvailable(msdfgen) | 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) | find_program(WAYLAND_SCANNER wayland-scanner REQUIRED) | ||||||
|  |  | ||||||
| pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) | pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) | ||||||
| @@ -93,17 +150,23 @@ set(WLR_LAYER_SHELL_XML | |||||||
| 	"${WLR_PROTOCOLS_DIR}/unstable/wlr-layer-shell-unstable-v1.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 | set(GEN_C_HEADERS | ||||||
| 	"${GEN_DIR}/xdg-shell-client-protocol.h" | 	"${GEN_DIR}/xdg-shell-client-protocol.h" | ||||||
| 	"${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h" | 	"${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h" | ||||||
| 	"${GEN_DIR}/ext-background-effect-v1-client-protocol.h" | 	"${GEN_DIR}/ext-background-effect-v1-client-protocol.h" | ||||||
| 	"${GEN_DIR}/blur-client-protocol.h" | 	"${GEN_DIR}/blur-client-protocol.h" | ||||||
|  | 	"${GEN_DIR}/text-input-unstable-v3-client-protocol.h" | ||||||
| ) | ) | ||||||
| set(GEN_C_PRIVATES | set(GEN_C_PRIVATES | ||||||
| 	"${GEN_DIR}/xdg-shell-protocol.c" | 	"${GEN_DIR}/xdg-shell-protocol.c" | ||||||
| 	"${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c" | 	"${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c" | ||||||
| 	"${GEN_DIR}/ext-background-effect-v1-protocol.c" | 	"${GEN_DIR}/ext-background-effect-v1-protocol.c" | ||||||
| 	"${GEN_DIR}/blur-protocol.c" | 	"${GEN_DIR}/blur-protocol.c" | ||||||
|  | 	"${GEN_DIR}/text-input-unstable-v3-protocol.c" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| add_custom_command( | add_custom_command( | ||||||
| @@ -116,10 +179,13 @@ add_custom_command( | |||||||
| 	# wlr-layer-shell | 	# 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}" 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" | 	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 | 	# org-kde-win-blur | ||||||
| 	COMMAND "${WAYLAND_SCANNER}" client-header "${CMAKE_CURRENT_SOURCE_DIR}/blur.xml" "${GEN_DIR}/blur-client-protocol.h" | 	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" | 	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" | 	COMMENT "Generating Wayland + wlr-layer-shell client headers and private code" | ||||||
| 	VERBATIM | 	VERBATIM | ||||||
| ) | ) | ||||||
| @@ -131,8 +197,13 @@ add_custom_target(generate_protocols ALL | |||||||
| add_executable(waylight | add_executable(waylight | ||||||
| 	${GEN_C_PRIVATES} | 	${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/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/Tick.cpp | ||||||
| 	${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp | 	${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp | ||||||
| ) | ) | ||||||
| @@ -153,9 +224,15 @@ target_link_libraries(waylight PRIVATE | |||||||
| 	PkgConfig::FONTCONFIG | 	PkgConfig::FONTCONFIG | ||||||
| 	PkgConfig::HARFBUZZ | 	PkgConfig::HARFBUZZ | ||||||
|  |  | ||||||
|  | 	tomlplusplus::tomlplusplus | ||||||
|  | 	cpptrace::cpptrace | ||||||
|  | 	tinyfiledialogs | ||||||
|  | 	mINI | ||||||
| 	raylib | 	raylib | ||||||
| 	msdfgen::msdfgen-core | 	msdfgen::msdfgen-core | ||||||
| 	msdfgen::msdfgen-ext | 	msdfgen::msdfgen-ext | ||||||
|  | 	lunasvg::lunasvg | ||||||
|  | 	SQLiteCpp | ||||||
|  |  | ||||||
| 	m | 	m | ||||||
| 	dl | 	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: |       system: | ||||||
|       let |       let | ||||||
|         pkgs = import nixpkgs { inherit system; }; |         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; [ |         buildInputs = with pkgs; [ | ||||||
|           cmake |           cmake | ||||||
|           ninja |           ninja | ||||||
| @@ -31,23 +46,12 @@ | |||||||
|                 [ |                 [ | ||||||
|                   llvmPackages_21.clang-tools |                   llvmPackages_21.clang-tools | ||||||
|                   lldb |                   lldb | ||||||
|  |                   gdb | ||||||
|                   codespell |                   codespell | ||||||
|                   doxygen |                   doxygen | ||||||
|                   gtest |                   gtest | ||||||
|                   cppcheck |                   cppcheck | ||||||
|                   inotify-tools |                   inotify-tools | ||||||
|  |  | ||||||
|                   pkg-config |  | ||||||
|                   wayland |  | ||||||
|                   wayland-protocols |  | ||||||
|                   wlr-protocols |  | ||||||
|                   wayland-scanner |  | ||||||
|                   libGL |  | ||||||
|                   libportal |  | ||||||
|                   glib |  | ||||||
|                   libxkbcommon |  | ||||||
|                   fontconfig |  | ||||||
|                   harfbuzz |  | ||||||
|                 ] |                 ] | ||||||
|                 ++ buildInputs |                 ++ buildInputs | ||||||
|                 ++ nativeBuildInputs |                 ++ nativeBuildInputs | ||||||
|   | |||||||
							
								
								
									
										811
									
								
								src/App.cpp
									
									
									
									
									
								
							
							
						
						
									
										811
									
								
								src/App.cpp
									
									
									
									
									
								
							| @@ -1,17 +1,24 @@ | |||||||
| #include "App.hpp" | #include "App.hpp" | ||||||
|  |  | ||||||
|  | #include <algorithm> | ||||||
| #include <cassert> | #include <cassert> | ||||||
| #include <chrono> | #include <chrono> | ||||||
|  | #include <cmath> | ||||||
| #include <cstdio> | #include <cstdio> | ||||||
| #include <cstdlib> | #include <cstdlib> | ||||||
| #include <cstring> | #include <cstring> | ||||||
| #include <poll.h> | #include <poll.h> | ||||||
|  | #include <print> | ||||||
| #include <pthread.h> | #include <pthread.h> | ||||||
|  | #include <ranges> | ||||||
| #include <signal.h> | #include <signal.h> | ||||||
|  | #include <span> | ||||||
| #include <sys/mman.h> | #include <sys/mman.h> | ||||||
| #include <sys/signalfd.h> | #include <sys/signalfd.h> | ||||||
| #include <thread> | #include <thread> | ||||||
| #include <unistd.h> | #include <unistd.h> | ||||||
|  | #include <unordered_set> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| #include <GLES3/gl3.h> | #include <GLES3/gl3.h> | ||||||
| #include <fontconfig/fontconfig.h> | #include <fontconfig/fontconfig.h> | ||||||
| @@ -27,6 +34,84 @@ | |||||||
| #include "blur-client-protocol.h" | #include "blur-client-protocol.h" | ||||||
| #include "ext-background-effect-v1-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 | auto TypingBuffer::push_utf8(char const *s) -> void | ||||||
| { | { | ||||||
| 	for (unsigned char const *p = reinterpret_cast<unsigned char const *>(s); | 	for (unsigned char const *p = reinterpret_cast<unsigned char const *>(s); | ||||||
| @@ -58,6 +143,73 @@ App::App() | |||||||
| 	init_egl(); | 	init_egl(); | ||||||
| 	init_signal(); | 	init_signal(); | ||||||
| 	init_theme_portal(); | 	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() | App::~App() | ||||||
| @@ -65,6 +217,9 @@ App::~App() | |||||||
| 	if (m_sfd != -1) | 	if (m_sfd != -1) | ||||||
| 		close(m_sfd); | 		close(m_sfd); | ||||||
|  |  | ||||||
|  | 	for (auto &[_, tex] : m_textures) | ||||||
|  | 		UnloadTexture(tex); | ||||||
|  |  | ||||||
| 	destroy_layer_surface(); | 	destroy_layer_surface(); | ||||||
|  |  | ||||||
| 	if (m_gl.edpy != EGL_NO_DISPLAY) { | 	if (m_gl.edpy != EGL_NO_DISPLAY) { | ||||||
| @@ -79,6 +234,14 @@ App::~App() | |||||||
| 		xkb_keymap_unref(m_kbd.xkb_keymap_v); | 		xkb_keymap_unref(m_kbd.xkb_keymap_v); | ||||||
| 	if (m_kbd.xkb_ctx_v) | 	if (m_kbd.xkb_ctx_v) | ||||||
| 		xkb_context_unref(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) | 	if (m_wayland.kbd) | ||||||
| 		wl_keyboard_destroy(m_wayland.kbd); | 		wl_keyboard_destroy(m_wayland.kbd); | ||||||
| 	if (m_wayland.seat) | 	if (m_wayland.seat) | ||||||
| @@ -101,7 +264,10 @@ auto App::run() -> void | |||||||
| 	SetWindowSize(m_win_w, m_win_h); | 	SetWindowSize(m_win_w, m_win_h); | ||||||
| 	while (m_running) { | 	while (m_running) { | ||||||
| 		pump_events(); | 		pump_events(); | ||||||
|  | 		m_ir.color(m_accent_color); | ||||||
| 		tick(); | 		tick(); | ||||||
|  | 		m_kbd.typing.clear(); | ||||||
|  | 		m_kbd.clear_transients(); | ||||||
| 		std::this_thread::sleep_for(std::chrono::milliseconds(16)); | 		std::this_thread::sleep_for(std::chrono::milliseconds(16)); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -132,9 +298,9 @@ auto App::init_wayland() -> void | |||||||
|  |  | ||||||
| 	static wl_keyboard_listener keyboard_listener {}; | 	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 { | 			                 u32 size) -> void { | ||||||
| 			auto *app = static_cast<App *>(data); | 			auto *app { static_cast<App *>(data) }; | ||||||
| 			if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { | 			if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { | ||||||
| 				close(fd); | 				close(fd); | ||||||
| 				return; | 				return; | ||||||
| @@ -158,21 +324,25 @@ auto App::init_wayland() -> void | |||||||
| 			    : nullptr; | 			    : nullptr; | ||||||
| 			munmap(map, size); | 			munmap(map, size); | ||||||
| 			close(fd); | 			close(fd); | ||||||
| 		}; | 		} }; | ||||||
|  |  | ||||||
| 		auto kb_enter = [](void *, wl_keyboard *, u32, wl_surface *, | 		auto kb_enter { [](void *data, wl_keyboard *, u32 serial, wl_surface *, | ||||||
| 		                    wl_array *) -> void { }; | 			                wl_array *) -> void { | ||||||
|  | 			static_cast<App *>(data)->m_last_serial = serial; | ||||||
|  | 		} }; | ||||||
| 		auto kb_leave | 		auto kb_leave | ||||||
| 		    = [](void *data, wl_keyboard *, u32, wl_surface *) -> void { | 		    = [](void *data, wl_keyboard *, u32, wl_surface *) -> void { | ||||||
| 			static_cast<App *>(data)->m_kbd.held.clear(); | 			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 { | 			              u32 state) -> void { | ||||||
| 			auto *app = static_cast<App *>(data); | 			auto *app { static_cast<App *>(data) }; | ||||||
| 			if (!app->m_kbd.xkb_state_v) | 			if (!app->m_kbd.xkb_state_v) | ||||||
| 				return; | 				return; | ||||||
|  |  | ||||||
|  | 			app->m_last_serial = serial; | ||||||
|  |  | ||||||
| 			xkb_keycode_t kc = key + 8; | 			xkb_keycode_t kc = key + 8; | ||||||
|  |  | ||||||
| 			if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { | 			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 ctrl = app->m_kbd.mod_active("Control"); | ||||||
| 				bool alt = app->m_kbd.mod_active("Mod1"); | 				bool alt = app->m_kbd.mod_active("Mod1"); | ||||||
| 				bool meta = app->m_kbd.mod_active("Mod4"); | 				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); | 					u32 cp = xkb_keysym_to_utf32(sym); | ||||||
| 					if (cp >= 0x20) { | 					if (cp >= 0x20) { | ||||||
| 						char buf[8]; | 						char buf[8]; | ||||||
| @@ -203,27 +434,117 @@ auto App::init_wayland() -> void | |||||||
| 				app->m_kbd.held.erase(key); | 				app->m_kbd.held.erase(key); | ||||||
| 				xkb_state_update_key(app->m_kbd.xkb_state_v, kc, XKB_KEY_UP); | 				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 { | 			               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) | 			if (!app->m_kbd.xkb_state_v) | ||||||
| 				return; | 				return; | ||||||
| 			xkb_state_update_mask(app->m_kbd.xkb_state_v, depressed, latched, | 			xkb_state_update_mask(app->m_kbd.xkb_state_v, depressed, latched, | ||||||
| 			    locked, 0, 0, group); | 			    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, | 		keyboard_listener = { kb_keymap, kb_enter, kb_leave, kb_key, kb_mods, | ||||||
| 			kb_repeat_info }; | 			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 | 	auto handle_registry_global | ||||||
| 	    = [](void *data, wl_registry *registry, u32 name, char const *interface, | 	    = [](void *data, wl_registry *registry, u32 name, char const *interface, | ||||||
| 	          u32 version) -> void { | 	          u32 version) -> void { | ||||||
| 		auto *app = static_cast<App *>(data); | 		auto *app { static_cast<App *>(data) }; | ||||||
| 		if (std::strcmp(interface, wl_compositor_interface.name) == 0) { | 		if (std::strcmp(interface, wl_compositor_interface.name) == 0) { | ||||||
| 			app->m_wayland.compositor = static_cast<wl_compositor *>( | 			app->m_wayland.compositor = static_cast<wl_compositor *>( | ||||||
| 			    wl_registry_bind(registry, name, &wl_compositor_interface, 4)); | 			    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 = { | 			static struct wl_seat_listener const seat_listener = { | ||||||
| 				.capabilities = | 				.capabilities = | ||||||
| 				    [](void *data, struct wl_seat *seat, u32 caps) { | 				    [](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) { | 				        if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { | ||||||
| 					        app->m_wayland.kbd = wl_seat_get_keyboard(seat); | 					        app->m_wayland.kbd = wl_seat_get_keyboard(seat); | ||||||
| 					        wl_keyboard_add_listener( | 					        wl_keyboard_add_listener( | ||||||
| 					            app->m_wayland.kbd, &keyboard_listener, data); | 					            app->m_wayland.kbd, &keyboard_listener, data); | ||||||
|  | 					        app->m_ime.seat_focus = false; | ||||||
| 				        } | 				        } | ||||||
|  | 				        ensure_text_input(app); | ||||||
| 				    }, | 				    }, | ||||||
| 				.name = [](void *, struct wl_seat *, char const *) {}, | 				.name = [](void *, struct wl_seat *, char const *) {}, | ||||||
| 			}; | 			}; | ||||||
| @@ -259,6 +582,120 @@ auto App::init_wayland() -> void | |||||||
| 			app->m_wayland.kde_blur_mgr | 			app->m_wayland.kde_blur_mgr | ||||||
| 			    = static_cast<org_kde_kwin_blur_manager *>(wl_registry_bind( | 			    = static_cast<org_kde_kwin_blur_manager *>(wl_registry_bind( | ||||||
| 			        registry, name, &org_kde_kwin_blur_manager_interface, 1)); | 			        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(); | 	ensure_egl_surface(); | ||||||
|  |  | ||||||
|  | 	{ | ||||||
|  | 		auto const *env = getenv("WAYLIGHT_DEBUG"); | ||||||
|  | 		if (env && *env) { | ||||||
|  | 			SetTraceLogLevel(LOG_DEBUG); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	InitWindow(m_win_w, m_win_h, ""); | 	InitWindow(m_win_w, m_win_h, ""); | ||||||
|  |  | ||||||
| 	m_tr = TextRenderer(); | 	m_tr = std::make_shared<TextRenderer>(); | ||||||
| 	auto const font = find_font_path(); | 	m_gui = std::make_shared<ImGui>(m_tr); | ||||||
|  | 	auto const font { find_font_path() }; | ||||||
| 	assert(font && "Could not find font"); | 	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"); | 	assert(font_handle && "Could not load font"); | ||||||
| 	m_font = *font_handle; | 	m_font = *font_handle; | ||||||
|  | 	m_gui->set_font(m_font); | ||||||
| } | } | ||||||
|  |  | ||||||
| auto App::init_signal() -> void | auto App::init_signal() -> void | ||||||
| @@ -326,9 +803,9 @@ auto App::init_signal() -> void | |||||||
| void App::on_settings_changed(XdpSettings * /*self*/, char const *ns, | void App::on_settings_changed(XdpSettings * /*self*/, char const *ns, | ||||||
|     char const *key, GVariant * /*value*/, gpointer data) |     char const *key, GVariant * /*value*/, gpointer data) | ||||||
| { | { | ||||||
| 	auto *app = static_cast<App *>(data); | 	auto *app { static_cast<App *>(data) }; | ||||||
| 	if (g_strcmp0(ns, "org.freedesktop.appearance") == 0 | 	if (g_strcmp0(ns, "org.freedesktop.appearance") == 0) { | ||||||
| 	    && g_strcmp0(key, "color-scheme") == 0) { | 		if (g_strcmp0(key, "color-scheme") == 0) { | ||||||
| 			guint v = xdp_settings_read_uint(app->m_xdp.settings, | 			guint v = xdp_settings_read_uint(app->m_xdp.settings, | ||||||
| 			    "org.freedesktop.appearance", "color-scheme", NULL, NULL); | 			    "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; | 				app->m_active_theme = Theme::Dark; | ||||||
| 			else | 			else | ||||||
| 				app->m_active_theme = Theme::Light; | 				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 | 	else | ||||||
| 		m_active_theme = Theme::Light; | 		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( | 	g_signal_connect( | ||||||
| 	    m_xdp.settings, "changed", G_CALLBACK(on_settings_changed), this); | 	    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); | 		    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 { | 		                              u32 serial, u32 w, u32 h) -> void { | ||||||
| 		auto *app = static_cast<App *>(data); | 		auto *app { static_cast<App *>(data) }; | ||||||
| 		if (w) | 		if (w) | ||||||
| 			app->m_win_w = static_cast<int>(w); | 			app->m_win_w = static_cast<int>(w); | ||||||
| 		if (h) | 		if (h) | ||||||
| @@ -429,11 +929,11 @@ auto App::create_layer_surface() -> void | |||||||
|  |  | ||||||
| 		if (app->m_wayland.surface) | 		if (app->m_wayland.surface) | ||||||
| 			wl_surface_commit(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_cast<App *>(data)->m_running = false; | ||||||
| 	}; | 	} }; | ||||||
|  |  | ||||||
| 	static zwlr_layer_surface_v1_listener const lsl = { | 	static zwlr_layer_surface_v1_listener const lsl = { | ||||||
| 		.configure = handle_layer_configure, | 		.configure = handle_layer_configure, | ||||||
| @@ -541,6 +1041,127 @@ auto App::update_blur_region() -> void | |||||||
| 	wl_region_destroy(region); | 	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 | auto App::pump_events() -> void | ||||||
| { | { | ||||||
| 	while (g_main_context_iteration(nullptr, false)) | 	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 }, | 	pollfd fds[2] { { wl_display_get_fd(m_wayland.display), POLLIN, 0 }, | ||||||
| 		{ m_sfd, POLLIN, 0 } }; | 		{ m_sfd, POLLIN, 0 } }; | ||||||
|  |  | ||||||
| 	auto prepared = (wl_display_prepare_read(m_wayland.display) == 0); | 	auto prepared { (wl_display_prepare_read(m_wayland.display) == 0) }; | ||||||
| 	auto ret = poll(fds, 2, 0); | 	auto ret { poll(fds, 2, 0) }; | ||||||
|  |  | ||||||
| 	if (ret > 0 && (fds[0].revents & POLLIN)) { | 	if (ret > 0 && (fds[0].revents & POLLIN)) { | ||||||
| 		if (prepared) { | 		if (prepared) { | ||||||
| 			wl_display_read_events(m_wayland.display); | 			wl_display_read_events(m_wayland.display); | ||||||
| 			prepared = false; | 			prepared = false; // cppcheck-suppress unreadVariable | ||||||
| 		} | 		} | ||||||
| 	} else if (prepared) { | 	} else if (prepared) { | ||||||
| 		wl_display_cancel_read(m_wayland.display); | 		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 | #pragma once | ||||||
|  |  | ||||||
|  | #include <cassert> | ||||||
|  | #include <filesystem> | ||||||
|  | #include <string> | ||||||
| #include <unordered_set> | #include <unordered_set> | ||||||
| #include <vector> | #include <vector> | ||||||
|  |  | ||||||
| #include <EGL/egl.h> | #include <EGL/egl.h> | ||||||
| #include <libportal/portal.h> | #include <libportal/portal.h> | ||||||
|  | #include <wayland-client-protocol.h> | ||||||
| extern "C" { | extern "C" { | ||||||
| #include "blur-client-protocol.h" | #include "blur-client-protocol.h" | ||||||
| #define namespace namespace_ | #define namespace namespace_ | ||||||
| #include "ext-background-effect-v1-client-protocol.h" | #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 "wlr-layer-shell-unstable-v1-client-protocol.h" | ||||||
| #include <libportal/settings.h> | #include <libportal/settings.h> | ||||||
| #undef namespace | #undef namespace | ||||||
| } | } | ||||||
|  | #include <SQLiteCpp/SQLiteCpp.h> | ||||||
| #include <wayland-client.h> | #include <wayland-client.h> | ||||||
| #include <wayland-egl.h> | #include <wayland-egl.h> | ||||||
| #include <xkbcommon/xkbcommon.h> | #include <xkbcommon/xkbcommon.h> | ||||||
|  |  | ||||||
|  | #include "Cache.hpp" | ||||||
|  | #include "Config.hpp" | ||||||
|  | #include "IconRegistry.hpp" | ||||||
|  | #include "ImGui.hpp" | ||||||
| #include "TextRenderer.hpp" | #include "TextRenderer.hpp" | ||||||
| #include "Theme.hpp" | #include "Theme.hpp" | ||||||
| #include "common.hpp" | #include "common.hpp" | ||||||
|  |  | ||||||
|  | namespace Waylight { | ||||||
|  |  | ||||||
| struct TypingBuffer : std::pmr::vector<u32> { | struct TypingBuffer : std::pmr::vector<u32> { | ||||||
| 	void push_utf8(char const *s); | 	void push_utf8(char const *s); | ||||||
| }; | }; | ||||||
| @@ -46,11 +58,20 @@ private: | |||||||
| 	auto destroy_layer_surface() -> void; | 	auto destroy_layer_surface() -> void; | ||||||
| 	auto ensure_egl_surface() -> void; | 	auto ensure_egl_surface() -> void; | ||||||
| 	auto update_blur_region() -> 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 & | 	auto theme() const -> ColorScheme const & | ||||||
| 	{ | 	{ | ||||||
| 		return m_themes[m_active_theme]; | 		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, | 	static void on_settings_changed(XdpSettings * /*self*/, char const *ns, | ||||||
| 	    char const *key, GVariant * /*value*/, gpointer data); | 	    char const *key, GVariant * /*value*/, gpointer data); | ||||||
|  |  | ||||||
| @@ -67,7 +88,15 @@ private: | |||||||
| 		ext_background_effect_surface_v1 *eff {}; | 		ext_background_effect_surface_v1 *eff {}; | ||||||
| 		org_kde_kwin_blur_manager *kde_blur_mgr {}; | 		org_kde_kwin_blur_manager *kde_blur_mgr {}; | ||||||
| 		org_kde_kwin_blur *kde_blur {}; | 		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; | 	} m_wayland; | ||||||
|  | 	std::pmr::string m_clipboard_cache; | ||||||
|  | 	u32 m_last_serial { 0 }; | ||||||
|  |  | ||||||
| 	struct { | 	struct { | ||||||
| 		EGLDisplay edpy { EGL_NO_DISPLAY }; | 		EGLDisplay edpy { EGL_NO_DISPLAY }; | ||||||
| @@ -102,13 +131,11 @@ private: | |||||||
| 		{ | 		{ | ||||||
| 			if (!xkb_state_v) | 			if (!xkb_state_v) | ||||||
| 				return false; | 				return false; | ||||||
| 			for (auto k : held) { | 			return std::any_of(held.begin(), held.end(), [&](u32 const k) { | ||||||
| 				if (xkb_state_key_get_one_sym( | 				return (xkb_state_key_get_one_sym( | ||||||
| 				            xkb_state_v, static_cast<xkb_keycode_t>(k + 8)) | 				            xkb_state_v, static_cast<xkb_keycode_t>(k + 8)) | ||||||
| 				    == sym) | 				    == sym); | ||||||
| 					return true; | 			}); | ||||||
| 			} |  | ||||||
| 			return false; |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		auto is_sym_pressed(xkb_keysym_t sym) const -> bool | 		auto is_sym_pressed(xkb_keysym_t sym) const -> bool | ||||||
| @@ -139,16 +166,78 @@ private: | |||||||
| 		} | 		} | ||||||
| 	} m_kbd; | 	} m_kbd; | ||||||
|  |  | ||||||
| 	std::optional<TextRenderer> m_tr { std::nullopt }; | 	std::shared_ptr<TextRenderer> m_tr { nullptr }; | ||||||
| 	FontHandle m_font; | 	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() }; | 	enum_array<Theme, ColorScheme> m_themes { make_default_themes() }; | ||||||
| 	Theme m_active_theme { Theme::Light }; | 	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_w { 800 }; | ||||||
| 	int m_win_h { 600 }; | 	int m_win_h { 600 }; | ||||||
| 	bool m_running { true }; | 	bool m_running { true }; | ||||||
| 	bool m_visible { 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 }; | 	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,12 +1,12 @@ | |||||||
| #include "TextRenderer.hpp" | #include "TextRenderer.hpp" | ||||||
|  |  | ||||||
| #include <algorithm> | #include <algorithm> | ||||||
| #include <array> |  | ||||||
| #include <cassert> | #include <cassert> | ||||||
| #include <chrono> | #include <chrono> | ||||||
| #include <cmath> | #include <cmath> | ||||||
| #include <cstdlib> | #include <cstdlib> | ||||||
| #include <cstring> | #include <cstring> | ||||||
|  | #include <limits> | ||||||
| #include <mutex> | #include <mutex> | ||||||
| #include <optional> | #include <optional> | ||||||
| #include <string> | #include <string> | ||||||
| @@ -37,12 +37,13 @@ | |||||||
| #include <ext/import-font.h> | #include <ext/import-font.h> | ||||||
| #include <msdfgen.h> | #include <msdfgen.h> | ||||||
|  |  | ||||||
|  | namespace Waylight { | ||||||
|  |  | ||||||
| namespace { | namespace { | ||||||
|  |  | ||||||
| constexpr int kAtlasDimension = 1024; | constexpr int ATLAS_DIMENSION = 1024; | ||||||
| constexpr int kAtlasPadding = 2; | constexpr int ATLAS_PADDING = 2; | ||||||
| constexpr float kDefaultPxRange = 4.0f; | constexpr float DEFAULT_EM_SCALE = 48.0f; | ||||||
| constexpr float kDefaultEmScale = 48.0f; |  | ||||||
|  |  | ||||||
| constexpr float hb_to_em(hb_position_t value, unsigned upem) | constexpr float hb_to_em(hb_position_t value, unsigned upem) | ||||||
| { | { | ||||||
| @@ -66,14 +67,86 @@ auto ft_library() -> FT_Library | |||||||
| 	return 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 | } // namespace | ||||||
|  |  | ||||||
| auto TextRenderer::flush_font(FontRuntime &rt, FontData &fd) -> void | auto TextRenderer::flush_font(FontRuntime &rt, FontData &fd) -> void | ||||||
| { | { | ||||||
| 	rt.glyph_cache.clear(); | 	rt.glyph_cache.clear(); | ||||||
| 	fd.glyphs.clear(); | 	fd.glyphs.clear(); | ||||||
| 	rt.pen_x = kAtlasPadding; | 	rt.pen_x = ATLAS_PADDING; | ||||||
| 	rt.pen_y = kAtlasPadding; | 	rt.pen_y = ATLAS_PADDING; | ||||||
| 	rt.row_height = 0; | 	rt.row_height = 0; | ||||||
| 	if (fd.atlas_img.data) | 	if (fd.atlas_img.data) | ||||||
| 		ImageClearBackground(&fd.atlas_img, BLANK); | 		ImageClearBackground(&fd.atlas_img, BLANK); | ||||||
| @@ -85,20 +158,20 @@ auto TextRenderer::allocate_region(FontRuntime &rt, FontData &fd, int width, | |||||||
|     int height) -> std::optional<std::pair<int, int>> |     int height) -> std::optional<std::pair<int, int>> | ||||||
| { | { | ||||||
| 	(void)fd; | 	(void)fd; | ||||||
| 	int padded_w = width + kAtlasPadding; | 	int padded_w = width + ATLAS_PADDING; | ||||||
| 	if (padded_w > rt.atlas_width || height + kAtlasPadding > rt.atlas_height) | 	if (padded_w > rt.atlas_width || height + ATLAS_PADDING > rt.atlas_height) | ||||||
| 		return std::nullopt; | 		return std::nullopt; | ||||||
| 	if (rt.pen_x + padded_w > rt.atlas_width) { | 	if (rt.pen_x + padded_w > rt.atlas_width) { | ||||||
| 		rt.pen_x = kAtlasPadding; | 		rt.pen_x = ATLAS_PADDING; | ||||||
| 		rt.pen_y += rt.row_height; | 		rt.pen_y += rt.row_height; | ||||||
| 		rt.row_height = 0; | 		rt.row_height = 0; | ||||||
| 	} | 	} | ||||||
| 	if (rt.pen_y + height + kAtlasPadding > rt.atlas_height) | 	if (rt.pen_y + height + ATLAS_PADDING > rt.atlas_height) | ||||||
| 		return std::nullopt; | 		return std::nullopt; | ||||||
| 	int x = rt.pen_x; | 	int x = rt.pen_x; | ||||||
| 	int y = rt.pen_y; | 	int y = rt.pen_y; | ||||||
| 	rt.pen_x += padded_w; | 	rt.pen_x += padded_w; | ||||||
| 	rt.row_height = std::max(rt.row_height, height + kAtlasPadding); | 	rt.row_height = std::max(rt.row_height, height + ATLAS_PADDING); | ||||||
| 	return std::pair { x, y }; | 	return std::pair { x, y }; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -130,7 +203,8 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd, | |||||||
| 	        msdfgen::FONT_SCALING_EM_NORMALIZED, &advance_em)) | 	        msdfgen::FONT_SCALING_EM_NORMALIZED, &advance_em)) | ||||||
| 		return std::nullopt; | 		return std::nullopt; | ||||||
| 	shape.normalize(); | 	shape.normalize(); | ||||||
| 	msdfgen::edgeColoringInkTrap(shape, 3.0); | 	// FIXME: Figure out shader | ||||||
|  | 	// msdfgen::edgeColoringInkTrap(shape, 3.0); | ||||||
| 	auto bounds = shape.getBounds(); | 	auto bounds = shape.getBounds(); | ||||||
| 	float const width_em = static_cast<float>(bounds.r - bounds.l); | 	float const width_em = static_cast<float>(bounds.r - bounds.l); | ||||||
| 	float const height_em = static_cast<float>(bounds.t - bounds.b); | 	float const height_em = static_cast<float>(bounds.t - bounds.b); | ||||||
| @@ -140,8 +214,8 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd, | |||||||
| 	int bmp_h = std::max( | 	int bmp_h = std::max( | ||||||
| 	    1, static_cast<int>(std::ceil(height_em * scale + 2.0 * rt.px_range))); | 	    1, static_cast<int>(std::ceil(height_em * scale + 2.0 * rt.px_range))); | ||||||
|  |  | ||||||
| 	if (bmp_w + kAtlasPadding > rt.atlas_width | 	if (bmp_w + ATLAS_PADDING > rt.atlas_width | ||||||
| 	    || bmp_h + kAtlasPadding > rt.atlas_height) { | 	    || bmp_h + ATLAS_PADDING > rt.atlas_height) { | ||||||
| 		TraceLog(LOG_WARNING, "Glyph %u bitmap %dx%d exceeds atlas %dx%d", | 		TraceLog(LOG_WARNING, "Glyph %u bitmap %dx%d exceeds atlas %dx%d", | ||||||
| 		    glyph_index, bmp_w, bmp_h, rt.atlas_width, rt.atlas_height); | 		    glyph_index, bmp_w, bmp_h, rt.atlas_width, rt.atlas_height); | ||||||
| 		GlyphCacheEntry too_large {}; | 		GlyphCacheEntry too_large {}; | ||||||
| @@ -167,16 +241,55 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd, | |||||||
| 	msdfgen::generateMSDF( | 	msdfgen::generateMSDF( | ||||||
| 	    msdf_bitmap, shape, rt.px_range, scale_vec, translate); | 	    msdf_bitmap, shape, rt.px_range, scale_vec, translate); | ||||||
|  |  | ||||||
| 	std::vector<Color> buffer(static_cast<size_t>(bmp_w) * bmp_h); | 	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) { | 	for (int y = 0; y < bmp_h; ++y) { | ||||||
| 		int const dst_y = bmp_h - 1 - y; | 		int const dst_y = bmp_h - 1 - y; | ||||||
| 		for (int x = 0; x < bmp_w; ++x) { | 		for (int x = 0; x < bmp_w; ++x) { | ||||||
| 			float const *px = msdf_bitmap(x, y); | 			float const *px = msdf_bitmap(x, y); | ||||||
| 			auto const r = msdfgen::pixelFloatToByte(px[0]); | 			auto const r = msdfgen::pixelFloatToByte(px[0]); | ||||||
| 			auto const g = msdfgen::pixelFloatToByte(px[1]); | 			if (flip) { | ||||||
| 			auto const b = msdfgen::pixelFloatToByte(px[2]); | 				buffer[static_cast<usize>(dst_y) * bmp_w + x] = Color { 255, | ||||||
| 			buffer[static_cast<size_t>(dst_y) * bmp_w + x] | 					255, 255, static_cast<unsigned char>(255 - r) }; | ||||||
| 			    = Color { r, g, b, 255 }; | 			} else { | ||||||
|  | 				buffer[static_cast<usize>(dst_y) * bmp_w + x] | ||||||
|  | 				    = Color { 255, 255, 255, r }; | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -235,12 +348,12 @@ auto TextRenderer::ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index, | |||||||
| TextRenderer::TextRenderer() | TextRenderer::TextRenderer() | ||||||
| { | { | ||||||
| 	static char const msdf_vs_data[] { | 	static char const msdf_vs_data[] { | ||||||
| #embed "base.vs" | #embed "base.vert" | ||||||
| 		, 0 | 		, 0 // cppcheck-suppress syntaxError | ||||||
| 	}; | 	}; | ||||||
| 	static char const msdf_fs_data[] { | 	static char const msdf_fs_data[] { | ||||||
| #embed "msdf.fs" | #embed "msdf.frag" | ||||||
| 		, 0 | 		, 0 // cppcheck-suppress syntaxError | ||||||
| 	}; | 	}; | ||||||
| 	m_msdf_shader = LoadShaderFromMemory(msdf_vs_data, msdf_fs_data); | 	m_msdf_shader = LoadShaderFromMemory(msdf_vs_data, msdf_fs_data); | ||||||
| 	assert(IsShaderValid(m_msdf_shader)); | 	assert(IsShaderValid(m_msdf_shader)); | ||||||
| @@ -249,50 +362,56 @@ TextRenderer::TextRenderer() | |||||||
|  |  | ||||||
| TextRenderer::~TextRenderer() | TextRenderer::~TextRenderer() | ||||||
| { | { | ||||||
| 	for (usize i = 0; i < m_font_runtime.size(); ++i) { | 	for (usize i = 0; i < m_font_sets.size(); ++i) { | ||||||
| 		if (m_font_runtime[i]) { |  | ||||||
| 		FontHandle handle; | 		FontHandle handle; | ||||||
| 		handle.id = i; | 		handle.id = i; | ||||||
| 		unload_font(handle); | 		unload_font(handle); | ||||||
| 	} | 	} | ||||||
| 	} | 	// Not unloading the shader... I have no clue why, but there's some sort of | ||||||
| 	UnloadShader(m_msdf_shader); | 	// double free. I love C interop!!!! | ||||||
| } | } | ||||||
|  |  | ||||||
| auto TextRenderer::measure_text(FontHandle const font, | auto TextRenderer::measure_text(FontHandle const font, | ||||||
|     std::string_view const text, int const size) -> Vector2 |     std::string_view const text, int const size) -> Vector2 | ||||||
| { | { | ||||||
| 	usize const font_id = font(); | 	usize const handle_id = font(); | ||||||
| 	if (font_id >= m_font_runtime.size() || !m_font_runtime[font_id] | 	if (handle_id >= m_font_sets.size()) | ||||||
| 	    || !m_font_runtime[font_id]->hb_font) | 		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 }; | 		return Vector2 { 0.0f, 0.0f }; | ||||||
|  |  | ||||||
| 	auto &rt = *m_font_runtime[font_id]; | 	auto placements = shape_text(font, text); | ||||||
| 	auto &fd = m_font_data[font_id]; |  | ||||||
|  |  | ||||||
| 	hb_buffer_t *buffer = hb_buffer_create(); | 	auto primary_runtime_index = font_set.font_indices.front(); | ||||||
| 	hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, | 	if (placements.empty()) { | ||||||
| 	    static_cast<int>(text.size())); | 		if (primary_runtime_index >= m_font_runtime.size() | ||||||
| 	hb_buffer_guess_segment_properties(buffer); | 		    || !m_font_runtime[primary_runtime_index]) | ||||||
| 	hb_shape(rt.hb_font, buffer, nullptr, 0); | 			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) }; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	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); |  | ||||||
| 	float advance_em = 0.0f; | 	float advance_em = 0.0f; | ||||||
| 	float min_x_em = 0.0f; | 	float min_x_em = 0.0f; | ||||||
| 	float max_x_em = 0.0f; | 	float max_x_em = 0.0f; | ||||||
| 	bool first = true; | 	bool first = true; | ||||||
|  | 	bool have_metrics = false; | ||||||
|  | 	float max_ascent = 0.0f; | ||||||
|  | 	float min_descent = 0.0f; | ||||||
|  |  | ||||||
| 	for (unsigned i = 0; i < length; ++i) { | 	for (auto const &placement : placements) { | ||||||
| 		u32 glyph_index = infos[i].codepoint; | 		usize const runtime_index = placement.runtime_index; | ||||||
| 		if (glyph_index == 0) | 		if (runtime_index >= m_font_runtime.size() | ||||||
|  | 		    || !m_font_runtime[runtime_index]) | ||||||
| 			continue; | 			continue; | ||||||
| 		auto *entry = ensure_glyph(rt, fd, glyph_index, false); | 		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) | 		if (!entry || entry->width == 0 || entry->height == 0) | ||||||
| 			continue; | 			continue; | ||||||
| 		float const x_offset_em | 		float const x_offset_em = hb_to_em(placement.x_offset, rt.units_per_em); | ||||||
| 		    = hb_to_em(positions[i].x_offset, rt.units_per_em); |  | ||||||
| 		float const left | 		float const left | ||||||
| 		    = advance_em + x_offset_em + entry->glyph.plane_bounds.left; | 		    = advance_em + x_offset_em + entry->glyph.plane_bounds.left; | ||||||
| 		float const right | 		float const right | ||||||
| @@ -305,17 +424,36 @@ auto TextRenderer::measure_text(FontHandle const font, | |||||||
| 			min_x_em = std::min(min_x_em, left); | 			min_x_em = std::min(min_x_em, left); | ||||||
| 			max_x_em = std::max(max_x_em, right); | 			max_x_em = std::max(max_x_em, right); | ||||||
| 		} | 		} | ||||||
| 		advance_em += hb_to_em(positions[i].x_advance, rt.units_per_em); | 		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); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	hb_buffer_destroy(buffer); | 	if (first) { | ||||||
|  | 		if (primary_runtime_index >= m_font_runtime.size() | ||||||
| 	if (first) | 		    || !m_font_runtime[primary_runtime_index]) | ||||||
| 		return Vector2 { 0.0f, | 			return Vector2 { 0.0f, 0.0f }; | ||||||
| 			(rt.ascent - rt.descent) * static_cast<float>(size) }; | 		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 width_em = std::max(max_x_em, advance_em) - min_x_em; | ||||||
| 	float height_em = rt.ascent - rt.descent; | 	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), | 	return Vector2 { width_em * static_cast<float>(size), | ||||||
| 		height_em * static_cast<float>(size) }; | 		height_em * static_cast<float>(size) }; | ||||||
| } | } | ||||||
| @@ -326,50 +464,43 @@ auto TextRenderer::draw_text(FontHandle const font, std::string_view const text, | |||||||
| 	auto const draw_start = std::chrono::steady_clock::now(); | 	auto const draw_start = std::chrono::steady_clock::now(); | ||||||
| 	int const pos_x = pos.x; | 	int const pos_x = pos.x; | ||||||
| 	int const pos_y = pos.y; | 	int const pos_y = pos.y; | ||||||
| 	// Don't use pos from here on out! | 	usize const handle_id = font(); | ||||||
| 	usize const font_id = font(); | 	if (handle_id >= m_font_sets.size()) | ||||||
| 	if (font_id >= m_font_runtime.size() || !m_font_runtime[font_id] | 		return; | ||||||
| 	    || !m_font_runtime[font_id]->hb_font) | 	auto const &font_set = m_font_sets[handle_id]; | ||||||
|  | 	if (font_set.font_indices.empty()) | ||||||
| 		return; | 		return; | ||||||
| 	auto &rt = *m_font_runtime[font_id]; |  | ||||||
| 	auto &fd = m_font_data[font_id]; |  | ||||||
|  |  | ||||||
| 	hb_buffer_t *buffer = hb_buffer_create(); | 	auto placements = shape_text(font, text); | ||||||
| 	hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, | 	if (placements.empty()) | ||||||
| 	    static_cast<int>(text.size())); | 		return; | ||||||
| 	hb_buffer_guess_segment_properties(buffer); |  | ||||||
| 	hb_shape(rt.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); |  | ||||||
| 	float const size_f = static_cast<float>(size); | 	float const size_f = static_cast<float>(size); | ||||||
| 	float pen_x_em = 0.0f; | 	float pen_x_em = 0.0f; | ||||||
| 	float pen_y_em = 0.0f; | 	float pen_y_em = 0.0f; | ||||||
| 	rt.frame_stamp++; | 	std::vector<usize> updated_stamp; | ||||||
|  | 	updated_stamp.reserve(font_set.font_indices.size()); | ||||||
|  |  | ||||||
| 	BeginShaderMode(m_msdf_shader); | 	for (auto const &placement : placements) { | ||||||
| 	if (m_px_range_uniform >= 0) { | 		usize const runtime_index = placement.runtime_index; | ||||||
| 		float shader_px_range = rt.px_range; | 		if (runtime_index >= m_font_runtime.size() | ||||||
| 		SetShaderValue(m_msdf_shader, m_px_range_uniform, &shader_px_range, | 		    || !m_font_runtime[runtime_index]) | ||||||
| 		    SHADER_UNIFORM_FLOAT); | 			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); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	for (unsigned i = 0; i < length; ++i) { | 		auto *entry = ensure_glyph(rt, fd, placement.glyph_index, true); | ||||||
| 		u32 glyph_index = infos[i].codepoint; | 		if (!entry || entry->width == 0 || entry->height == 0) | ||||||
| 		if (glyph_index == 0) |  | ||||||
| 			continue; | 			continue; | ||||||
| 		auto *entry = ensure_glyph(rt, fd, glyph_index, true); |  | ||||||
| 		if (!entry) | 		float const advance_em = hb_to_em(placement.x_advance, rt.units_per_em); | ||||||
| 			continue; | 		float const x_offset_em = hb_to_em(placement.x_offset, rt.units_per_em); | ||||||
| 		if (entry->width == 0 || entry->height == 0) | 		float const y_offset_em = hb_to_em(placement.y_offset, rt.units_per_em); | ||||||
| 			continue; |  | ||||||
| 		float const advance_em |  | ||||||
| 		    = hb_to_em(positions[i].x_advance, rt.units_per_em); |  | ||||||
| 		float const x_offset_em |  | ||||||
| 		    = hb_to_em(positions[i].x_offset, rt.units_per_em); |  | ||||||
| 		float const y_offset_em |  | ||||||
| 		    = hb_to_em(positions[i].y_offset, rt.units_per_em); |  | ||||||
| 		float const x_base_em = pen_x_em + x_offset_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 y_base_em = pen_y_em + y_offset_em; | ||||||
| 		float const scale_px = size_f / static_cast<float>(rt.em_scale); | 		float const scale_px = size_f / static_cast<float>(rt.em_scale); | ||||||
| @@ -392,23 +523,20 @@ auto TextRenderer::draw_text(FontHandle const font, std::string_view const text, | |||||||
| 		    fd.atlas, source, dest, Vector2 { 0.0f, 0.0f }, 0.0f, color); | 		    fd.atlas, source, dest, Vector2 { 0.0f, 0.0f }, 0.0f, color); | ||||||
|  |  | ||||||
| 		pen_x_em += advance_em; | 		pen_x_em += advance_em; | ||||||
| 		pen_y_em += hb_to_em(positions[i].y_advance, rt.units_per_em); | 		pen_y_em += hb_to_em(placement.y_advance, rt.units_per_em); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	EndShaderMode(); |  | ||||||
| 	hb_buffer_destroy(buffer); |  | ||||||
|  |  | ||||||
| 	auto const draw_end = std::chrono::steady_clock::now(); | 	auto const draw_end = std::chrono::steady_clock::now(); | ||||||
| 	auto const draw_ms | 	auto const draw_ms | ||||||
| 	    = std::chrono::duration<double, std::milli>(draw_end - draw_start) | 	    = std::chrono::duration<double, std::milli>(draw_end - draw_start) | ||||||
| 	          .count(); | 	          .count(); | ||||||
| 	if (draw_ms > 5.0) | 	if (draw_ms > 5.0) | ||||||
| 		TraceLog(LOG_INFO, "draw_text took %.2f ms for %zu glyphs", draw_ms, | 		TraceLog(LOG_INFO, "draw_text took %.2f ms for %zu glyphs", draw_ms, | ||||||
| 		    static_cast<size_t>(length)); | 		    placements.size()); | ||||||
| } | } | ||||||
|  |  | ||||||
| auto TextRenderer::load_font(std::filesystem::path const &path) | auto TextRenderer::load_single_font(std::filesystem::path const &path) | ||||||
|     -> std::optional<FontHandle> |     -> std::optional<usize> | ||||||
| { | { | ||||||
| 	FT_Library const ft = ft_library(); | 	FT_Library const ft = ft_library(); | ||||||
| 	if (!ft) | 	if (!ft) | ||||||
| @@ -417,17 +545,20 @@ auto TextRenderer::load_font(std::filesystem::path const &path) | |||||||
| 	FT_Face face = nullptr; | 	FT_Face face = nullptr; | ||||||
| 	if (FT_New_Face(ft, path.string().c_str(), 0, &face) != 0) | 	if (FT_New_Face(ft, path.string().c_str(), 0, &face) != 0) | ||||||
| 		return std::nullopt; | 		return std::nullopt; | ||||||
| 	FT_Select_Charmap(face, FT_ENCODING_UNICODE); | 	if (FT_Select_Charmap(face, FT_ENCODING_UNICODE) != 0) { | ||||||
|  | 		FT_Done_Face(face); | ||||||
|  | 		return std::nullopt; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	auto runtime = std::make_unique<FontRuntime>(); | 	auto runtime = std::make_unique<FontRuntime>(); | ||||||
| 	runtime->face = face; | 	runtime->face = face; | ||||||
| 	runtime->atlas_width = kAtlasDimension; | 	runtime->atlas_width = ATLAS_DIMENSION; | ||||||
| 	runtime->atlas_height = kAtlasDimension; | 	runtime->atlas_height = ATLAS_DIMENSION; | ||||||
| 	runtime->pen_x = kAtlasPadding; | 	runtime->pen_x = ATLAS_PADDING; | ||||||
| 	runtime->pen_y = kAtlasPadding; | 	runtime->pen_y = ATLAS_PADDING; | ||||||
| 	runtime->row_height = 0; | 	runtime->row_height = 0; | ||||||
| 	runtime->px_range = 0.05; // kDefaultPxRange; | 	runtime->px_range = 0.05; // kDefaultPxRange; | ||||||
| 	runtime->em_scale = kDefaultEmScale; | 	runtime->em_scale = DEFAULT_EM_SCALE; | ||||||
| 	runtime->frame_stamp = 0; | 	runtime->frame_stamp = 0; | ||||||
| 	runtime->units_per_em | 	runtime->units_per_em | ||||||
| 	    = static_cast<unsigned>(face->units_per_EM ? face->units_per_EM : 2048); | 	    = static_cast<unsigned>(face->units_per_EM ? face->units_per_EM : 2048); | ||||||
| @@ -441,9 +572,12 @@ auto TextRenderer::load_font(std::filesystem::path const &path) | |||||||
| 	runtime->line_gap = std::max(0.0f, line_height - adv_height); | 	runtime->line_gap = std::max(0.0f, line_height - adv_height); | ||||||
|  |  | ||||||
| 	runtime->hb_face = hb_ft_face_create_referenced(face); | 	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); | 	runtime->hb_font = hb_ft_font_create_referenced(face); | ||||||
| 	if (!runtime->hb_font) { | 	if (!runtime->hb_font) { | ||||||
| 		if (runtime->hb_face) |  | ||||||
| 		hb_face_destroy(runtime->hb_face); | 		hb_face_destroy(runtime->hb_face); | ||||||
| 		FT_Done_Face(face); | 		FT_Done_Face(face); | ||||||
| 		return std::nullopt; | 		return std::nullopt; | ||||||
| @@ -467,18 +601,18 @@ auto TextRenderer::load_font(std::filesystem::path const &path) | |||||||
| 	    = GenImageColor(runtime->atlas_width, runtime->atlas_height, BLANK); | 	    = GenImageColor(runtime->atlas_width, runtime->atlas_height, BLANK); | ||||||
| 	if (!font_data.atlas_img.data) { | 	if (!font_data.atlas_img.data) { | ||||||
| 		msdfgen::destroyFont(runtime->msdf_font); | 		msdfgen::destroyFont(runtime->msdf_font); | ||||||
|  | 		runtime->msdf_font = nullptr; | ||||||
| 		hb_font_destroy(runtime->hb_font); | 		hb_font_destroy(runtime->hb_font); | ||||||
| 		hb_face_destroy(runtime->hb_face); | 		hb_face_destroy(runtime->hb_face); | ||||||
| 		FT_Done_Face(face); |  | ||||||
| 		return std::nullopt; | 		return std::nullopt; | ||||||
| 	} | 	} | ||||||
| 	font_data.atlas = LoadTextureFromImage(font_data.atlas_img); | 	font_data.atlas = LoadTextureFromImage(font_data.atlas_img); | ||||||
| 	if (font_data.atlas.id == 0) { | 	if (font_data.atlas.id == 0) { | ||||||
| 		UnloadImage(font_data.atlas_img); | 		UnloadImage(font_data.atlas_img); | ||||||
| 		msdfgen::destroyFont(runtime->msdf_font); | 		msdfgen::destroyFont(runtime->msdf_font); | ||||||
|  | 		runtime->msdf_font = nullptr; | ||||||
| 		hb_font_destroy(runtime->hb_font); | 		hb_font_destroy(runtime->hb_font); | ||||||
| 		hb_face_destroy(runtime->hb_face); | 		hb_face_destroy(runtime->hb_face); | ||||||
| 		FT_Done_Face(face); |  | ||||||
| 		return std::nullopt; | 		return std::nullopt; | ||||||
| 	} | 	} | ||||||
| 	SetTextureFilter(font_data.atlas, TEXTURE_FILTER_BILINEAR); | 	SetTextureFilter(font_data.atlas, TEXTURE_FILTER_BILINEAR); | ||||||
| @@ -487,40 +621,175 @@ auto TextRenderer::load_font(std::filesystem::path const &path) | |||||||
|  |  | ||||||
| 	m_font_data.emplace_back(std::move(font_data)); | 	m_font_data.emplace_back(std::move(font_data)); | ||||||
| 	m_font_runtime.emplace_back(std::move(runtime)); | 	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> | ||||||
|  | { | ||||||
|  | 	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; | 	FontHandle handle; | ||||||
| 	handle.id = m_font_data.size() - 1; | 	handle.id = m_font_sets.size() - 1; | ||||||
| 	return handle; | 	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 | auto TextRenderer::unload_font(FontHandle const font) -> void | ||||||
| { | { | ||||||
| 	usize const font_id = font(); | 	usize const handle_id = font(); | ||||||
| 	if (font_id >= m_font_runtime.size()) | 	if (handle_id >= m_font_sets.size()) | ||||||
| 		return; | 		return; | ||||||
|  |  | ||||||
| 	if (m_font_runtime[font_id]) { | 	auto &font_set = m_font_sets[handle_id]; | ||||||
| 		auto &rt = *m_font_runtime[font_id]; | 	for (usize runtime_index : font_set.font_indices) { | ||||||
| 		rt.glyph_cache.clear(); | 		if (runtime_index >= m_font_runtime.size()) | ||||||
| 		if (rt.msdf_font) | 			continue; | ||||||
| 			msdfgen::destroyFont(rt.msdf_font); |  | ||||||
| 		if (rt.hb_font) |  | ||||||
| 			hb_font_destroy(rt.hb_font); |  | ||||||
| 		if (rt.hb_face) |  | ||||||
| 			hb_face_destroy(rt.hb_face); |  | ||||||
| 		if (rt.face) |  | ||||||
| 			FT_Done_Face(rt.face); |  | ||||||
| 	} |  | ||||||
| 	m_font_runtime[font_id].reset(); |  | ||||||
|  |  | ||||||
| 	if (font_id < m_font_data.size()) { | 		if (auto &runtime_ptr = m_font_runtime[runtime_index]) { | ||||||
| 		auto &fd = m_font_data[font_id]; | 			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) | 			if (fd.atlas.id != 0) | ||||||
| 				UnloadTexture(fd.atlas); | 				UnloadTexture(fd.atlas); | ||||||
| 			if (fd.atlas_img.data) | 			if (fd.atlas_img.data) | ||||||
| 				UnloadImage(fd.atlas_img); | 				UnloadImage(fd.atlas_img); | ||||||
|  | 			fd.atlas = Texture2D {}; | ||||||
|  | 			fd.atlas_img = Image {}; | ||||||
| 			fd.glyphs.clear(); | 			fd.glyphs.clear(); | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  | 	font_set.font_indices.clear(); | ||||||
| } | } | ||||||
|  |  | ||||||
| auto find_font_path(std::string_view path) | auto find_font_path(std::string_view path) | ||||||
| @@ -572,3 +841,5 @@ auto find_font_path(std::string_view path) | |||||||
| 	} | 	} | ||||||
| 	return final_path; | 	return final_path; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | } // namespace Waylight | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| #include <filesystem> | #include <filesystem> | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <optional> | #include <optional> | ||||||
|  | #include <span> | ||||||
| #include <unordered_map> | #include <unordered_map> | ||||||
| #include <utility> | #include <utility> | ||||||
| #include <vector> | #include <vector> | ||||||
| @@ -14,18 +15,28 @@ | |||||||
| struct hb_face_t; | struct hb_face_t; | ||||||
| struct hb_font_t; | struct hb_font_t; | ||||||
| struct FT_FaceRec_; | struct FT_FaceRec_; | ||||||
| using FT_Face = FT_FaceRec_*; | using FT_Face = FT_FaceRec_ *; | ||||||
|  |  | ||||||
| namespace msdfgen { | namespace msdfgen { | ||||||
| class FontHandle; | class FontHandle; | ||||||
| } | } | ||||||
|  |  | ||||||
| struct FontHandle { | namespace Waylight { | ||||||
| 	auto operator()() const -> auto const & { return id; } |  | ||||||
|  | struct FontHandle { // cppcheck-supress noConstructor | ||||||
|  | 	FontHandle() = default; | ||||||
|  |  | ||||||
|  | 	auto operator()() const -> auto const & | ||||||
|  | 	{ | ||||||
|  | 		if (id == 0xffffffff) { | ||||||
|  | 			throw std::runtime_error("Uninitialized FontHandle"); | ||||||
|  | 		} | ||||||
|  | 		return id; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| private: | private: | ||||||
| 	friend struct TextRenderer; | 	friend struct TextRenderer; | ||||||
| 	usize id; | 	usize id { 0xffffffff }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct FontRuntime; | struct FontRuntime; | ||||||
| @@ -33,6 +44,7 @@ struct FontRuntime; | |||||||
| struct TextRenderer { | struct TextRenderer { | ||||||
| 	TextRenderer(); // Requires raylib to be initialized! | 	TextRenderer(); // Requires raylib to be initialized! | ||||||
| 	~TextRenderer(); | 	~TextRenderer(); | ||||||
|  |  | ||||||
| 	TextRenderer(TextRenderer const &) = delete; | 	TextRenderer(TextRenderer const &) = delete; | ||||||
| 	auto operator=(TextRenderer const &) -> TextRenderer & = delete; | 	auto operator=(TextRenderer const &) -> TextRenderer & = delete; | ||||||
| 	TextRenderer(TextRenderer &&) = default; | 	TextRenderer(TextRenderer &&) = default; | ||||||
| @@ -44,7 +56,8 @@ struct TextRenderer { | |||||||
| 	    Vector2 const pos, int const size = 16, Color const color = WHITE) | 	    Vector2 const pos, int const size = 16, Color const color = WHITE) | ||||||
| 	    -> void; | 	    -> 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>; | 	    -> std::optional<FontHandle>; | ||||||
| 	auto unload_font(FontHandle const font) -> void; | 	auto unload_font(FontHandle const font) -> void; | ||||||
|  |  | ||||||
| @@ -103,18 +116,36 @@ private: | |||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	std::vector<std::unique_ptr<FontRuntime>> m_font_runtime; | 	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 flush_font(FontRuntime &rt, FontData &fd) -> void; | ||||||
| 	static auto allocate_region( | 	static auto allocate_region(FontRuntime &rt, FontData &fd, int width, | ||||||
| 	    FontRuntime &rt, FontData &fd, int width, int height) | 	    int height) -> std::optional<std::pair<int, int>>; | ||||||
| 	    -> std::optional<std::pair<int, int>>; |  | ||||||
| 	static auto upload_region(FontData &fd, int dst_x, int dst_y, int width, | 	static auto upload_region(FontData &fd, int dst_x, int dst_y, int width, | ||||||
| 	    int height, std::vector<Color> const &buffer) -> void; | 	    int height, std::vector<Color> const &buffer) -> void; | ||||||
| 	static auto generate_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index) | 	static auto generate_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index) | ||||||
| 	    -> std::optional<GlyphCacheEntry>; | 	    -> std::optional<GlyphCacheEntry>; | ||||||
| 	static auto ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index, | 	static auto ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index, | ||||||
| 	    bool mark_usage) -> GlyphCacheEntry *; | 	    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") | auto find_font_path(std::string_view path = "sans-serif:style=Regular") | ||||||
|     -> std::optional<std::filesystem::path>; |     -> std::optional<std::filesystem::path>; | ||||||
|  |  | ||||||
|  | } // namespace Waylight | ||||||
|   | |||||||
| @@ -4,8 +4,11 @@ | |||||||
|  |  | ||||||
| #include "enum_array.hpp" | #include "enum_array.hpp" | ||||||
|  |  | ||||||
|  | namespace Waylight { | ||||||
|  |  | ||||||
| struct ColorScheme { | struct ColorScheme { | ||||||
| 	Color foreground; | 	Color foreground; | ||||||
|  | 	Color foreground_preedit; | ||||||
| 	struct { | 	struct { | ||||||
| 		Color background; | 		Color background; | ||||||
| 	} window; | 	} window; | ||||||
| @@ -23,6 +26,7 @@ constexpr auto make_default_themes() -> enum_array<Theme, ColorScheme> const | |||||||
| 	enum_array<Theme, ColorScheme> array; | 	enum_array<Theme, ColorScheme> array; | ||||||
| 	array[Theme::Light] = { | 	array[Theme::Light] = { | ||||||
| 	    .foreground = { 0, 0, 0, 255 }, | 	    .foreground = { 0, 0, 0, 255 }, | ||||||
|  | 	    .foreground_preedit = { 0, 0, 0, 255 }, | ||||||
| 	    .window = | 	    .window = | ||||||
| 	        { | 	        { | ||||||
| 	            .background = { 255, 255, 255, 100 }, | 	            .background = { 255, 255, 255, 100 }, | ||||||
| @@ -30,6 +34,7 @@ constexpr auto make_default_themes() -> enum_array<Theme, ColorScheme> const | |||||||
| 	}; | 	}; | ||||||
| 	array[Theme::Dark] = { | 	array[Theme::Dark] = { | ||||||
| 	    .foreground = { 255, 255, 255, 255 }, | 	    .foreground = { 255, 255, 255, 255 }, | ||||||
|  | 	    .foreground_preedit = { 255, 255, 255, 255 }, | ||||||
| 	    .window = | 	    .window = | ||||||
| 	        { | 	        { | ||||||
| 	            .background = { 0, 0, 0, 100 }, | 	            .background = { 0, 0, 0, 100 }, | ||||||
| @@ -37,3 +42,5 @@ constexpr auto make_default_themes() -> enum_array<Theme, ColorScheme> const | |||||||
| 	}; | 	}; | ||||||
| 	return array; | 	return array; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | } // namespace Waylight | ||||||
|   | |||||||
							
								
								
									
										73
									
								
								src/Tick.cpp
									
									
									
									
									
								
							
							
						
						
									
										73
									
								
								src/Tick.cpp
									
									
									
									
									
								
							| @@ -1,26 +1,31 @@ | |||||||
| #include "App.hpp" | #include "App.hpp" | ||||||
|  |  | ||||||
|  | #include <cassert> | ||||||
|  |  | ||||||
| #include <EGL/egl.h> | #include <EGL/egl.h> | ||||||
| #include <GLES3/gl3.h> | #include <GLES3/gl3.h> | ||||||
| #include <print> |  | ||||||
| #include <raylib.h> | #include <raylib.h> | ||||||
| #include <rlgl.h> | #include <rlgl.h> | ||||||
| #include <xkbcommon/xkbcommon.h> | #include <xkbcommon/xkbcommon.h> | ||||||
|  |  | ||||||
|  | #include <optional> | ||||||
|  |  | ||||||
|  | namespace Waylight { | ||||||
|  |  | ||||||
| auto App::tick() -> void | 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 | 	if (!m_visible || m_gl.edpy == EGL_NO_DISPLAY | ||||||
| 	    || m_gl.esurf == EGL_NO_SURFACE) | 	    || m_gl.esurf == EGL_NO_SURFACE) | ||||||
| 		return; | 		return; | ||||||
|  |  | ||||||
| 	glViewport(0, 0, m_win_w, m_win_h); | 	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)) { | 	if (m_kbd.is_sym_pressed(XKB_KEY_Escape)) { | ||||||
| 		set_visible(!visible()); | 		set_visible(!visible()); | ||||||
| 		if (m_kbd.ctrl() && m_kbd.shift()) { | 		if (m_kbd.ctrl() && m_kbd.shift()) { | ||||||
| @@ -30,20 +35,54 @@ auto App::tick() -> void | |||||||
|  |  | ||||||
| 	BeginDrawing(); | 	BeginDrawing(); | ||||||
|  |  | ||||||
| 	ClearBackground(BLANK); | 	ClearBackground(theme().window.background); | ||||||
|  |  | ||||||
| 	DrawFPS(10, 10); | 	{ | ||||||
| 	if (m_tr) { | 		assert(m_gui); | ||||||
| 		Color const fg = theme().foreground; |  | ||||||
| 		Vector2 const pos { 40.0f, 60.0f }; | 		m_gui->style().preedit_color = theme().foreground_preedit; | ||||||
| 		auto text = std::string_view("Hello from Waylight"); | 		m_gui->style().text_color = theme().foreground; | ||||||
| 		auto size = 48; | 		m_gui->style().selection_color = m_accent_color; | ||||||
| 		m_tr->draw_text(m_font, text, pos, size, fg); | 		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(); | 	EndDrawing(); | ||||||
|  |  | ||||||
| 	eglSwapBuffers(m_gl.edpy, m_gl.esurf); | 	eglSwapBuffers(m_gl.edpy, m_gl.esurf); | ||||||
| 	m_kbd.typing.clear(); |  | ||||||
| 	m_kbd.clear_transients(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | } // namespace Waylight | ||||||
|   | |||||||
| @@ -1,7 +1,11 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <algorithm> | ||||||
|  | #include <array> | ||||||
| #include <cstdint> | #include <cstdint> | ||||||
|  |  | ||||||
|  | namespace Waylight { | ||||||
|  |  | ||||||
| using u8 = std::uint8_t; | using u8 = std::uint8_t; | ||||||
| using i8 = std::int8_t; | using i8 = std::int8_t; | ||||||
| using u16 = std::uint16_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 * | [[maybe_unused]] static inline auto rune_to_string(uint32_t cp) -> char const * | ||||||
| { | { | ||||||
| 	static char utf8[5] = { 0 }; | 	static std::array<char, 5> utf8 {}; | ||||||
| 	for (auto &c : utf8) | 	std::fill(utf8.begin(), utf8.end(), 0); | ||||||
| 		c = 0; |  | ||||||
|  |  | ||||||
| 	if (cp < 0x80) { | 	if (cp < 0x80) { | ||||||
| 		utf8[0] = cp; | 		utf8[0] = cp; | ||||||
| @@ -35,5 +38,7 @@ using isize = std::intptr_t; | |||||||
| 		utf8[3] = 0x80 | (cp & 0x3F); | 		utf8[3] = 0x80 | (cp & 0x3F); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return utf8; | 	return utf8.data(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | } // namespace Waylight | ||||||
|   | |||||||
| @@ -5,28 +5,31 @@ | |||||||
| #include <stdexcept> | #include <stdexcept> | ||||||
| #include <type_traits> | #include <type_traits> | ||||||
|  |  | ||||||
|  | #include "common.hpp" | ||||||
|  |  | ||||||
|  | namespace Waylight { | ||||||
|  |  | ||||||
| template<class E> struct enum_traits; | template<class E> struct enum_traits; | ||||||
|  |  | ||||||
| template<class E> | template<class E> | ||||||
| concept EnumLike = std::is_enum_v<E>; | concept EnumLike = std::is_enum_v<E>; | ||||||
|  |  | ||||||
| template<EnumLike E> | template<EnumLike E> | ||||||
| constexpr std::size_t enum_count_v | constexpr usize enum_count_v = static_cast<usize>(enum_traits<E>::last) | ||||||
|     = static_cast<std::size_t>(enum_traits<E>::last) |     - static_cast<usize>(enum_traits<E>::first) + 1; | ||||||
|     - static_cast<std::size_t>(enum_traits<E>::first) + 1; |  | ||||||
|  |  | ||||||
| template<EnumLike E, class T> struct enum_array { | template<EnumLike E, class T> struct enum_array { | ||||||
| 	using value_type = T; | 	using value_type = T; | ||||||
| 	using enum_type = E; | 	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 first = enum_traits<E>::first; | ||||||
| 	static constexpr E last = enum_traits<E>::last; | 	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 {}; | 	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 *data() noexcept { return _data.data(); } | ||||||
| 	constexpr T const *data() const noexcept { return _data.data(); } | 	constexpr T const *data() const noexcept { return _data.data(); } | ||||||
| 	constexpr T *begin() noexcept { return _data.begin().operator->(); } | 	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); } | 	constexpr void fill(T const &v) { _data.fill(v); } | ||||||
|  |  | ||||||
| private: | 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)... }; | 	arr._data = { std::forward<T>(first_val), std::forward<U>(rest)... }; | ||||||
| 	return arr; | 	return arr; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | } // namespace Waylight | ||||||
|   | |||||||
							
								
								
									
										72
									
								
								src/main.cpp
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								src/main.cpp
									
									
									
									
									
								
							| @@ -1,35 +1,31 @@ | |||||||
|  | #include <algorithm> | ||||||
| #include <csignal> | #include <csignal> | ||||||
| #include <cstdio> | #include <cstdio> | ||||||
| #include <cstdlib> | #include <cstdlib> | ||||||
| #include <fcntl.h> | #include <fcntl.h> | ||||||
|  | #include <iostream> | ||||||
| #include <optional> | #include <optional> | ||||||
|  | #include <print> | ||||||
| #include <signal.h> | #include <signal.h> | ||||||
| #include <sys/file.h> | #include <sys/file.h> | ||||||
| #include <sys/types.h> | #include <sys/types.h> | ||||||
| #include <unistd.h> | #include <unistd.h> | ||||||
|  |  | ||||||
|  | #include <cpptrace/cpptrace.hpp> | ||||||
|  | #include <cpptrace/from_current.hpp> | ||||||
|  | #include <tinyfiledialogs.h> | ||||||
|  |  | ||||||
| #include "App.hpp" | #include "App.hpp" | ||||||
|  |  | ||||||
| bool check_or_signal_running(); | namespace Waylight { | ||||||
|  |  | ||||||
| std::optional<App> g_app{}; | bool signal_running(); | ||||||
|  |  | ||||||
| auto main() -> int { | std::optional<App> g_app {}; | ||||||
|   if (check_or_signal_running()) { |  | ||||||
|     return 0; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   std::signal(SIGINT, [](int) { | bool signal_running() | ||||||
|     if (g_app) | { | ||||||
|       g_app->stop(); | 	char const *lock_path = "/tmp/waylight.lock"; | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   g_app.emplace(); |  | ||||||
|   g_app->run(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool check_or_signal_running() { |  | ||||||
|   const char *lock_path = "/tmp/waylight.lock"; |  | ||||||
| 	int fd = open(lock_path, O_CREAT | O_RDWR, 0666); | 	int fd = open(lock_path, O_CREAT | O_RDWR, 0666); | ||||||
| 	if (fd == -1) | 	if (fd == -1) | ||||||
| 		return false; | 		return false; | ||||||
| @@ -46,7 +42,47 @@ bool check_or_signal_running() { | |||||||
| 		return true; | 		return true; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   ftruncate(fd, 0); | 	if (ftruncate(fd, 0) == -1) { | ||||||
|  | 		close(fd); | ||||||
|  | 		unlink(lock_path); | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	dprintf(fd, "%d\n", getpid()); | 	dprintf(fd, "%d\n", getpid()); | ||||||
| 	return false; | 	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