Compare commits
43 Commits
af1d54048e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8859504fed | |||
| 237208d972 | |||
| 42a9de3ba3 | |||
| 6f45a3bc70 | |||
| f61710010d | |||
| d368760f78 | |||
| a67b787386 | |||
| 86ecd128f8 | |||
| 06418b4cf4 | |||
| 81584c643e | |||
| aaf5dbb3b7 | |||
| 9f7365cbb6 | |||
| 58c5c2c6c4 | |||
| 92dbad27ee | |||
| d5af4c9baf | |||
| c4e13985ed | |||
| 86024e2c03 | |||
| e43799d11b | |||
| aeeae20aff | |||
| 465de1d0ea | |||
| f728d61f23 | |||
| 0629283aa5 | |||
| bd82b7a25c | |||
| cabf8b23df | |||
| acf480832a | |||
| 7834724e53 | |||
| 18614ccee9 | |||
| 30b558f649 | |||
| 804c420744 | |||
| 52fe62c3b1 | |||
| 47ffde7996 | |||
| 2b8bc31fc7 | |||
| 64979c6e5c | |||
| 278f4c6df3 | |||
| c377baf3c9 | |||
| 596f24becd | |||
| 4cbdc572a9 | |||
| 9268380fd9 | |||
| 4d8940812a | |||
| 6ec78cf752 | |||
| 59acba3264 | |||
| 1414a66e56 | |||
| 8653b02c44 |
26
.clang-format
Normal file
26
.clang-format
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
UseTab: ForIndentation
|
||||||
|
TabWidth: 4
|
||||||
|
IndentWidth: 4
|
||||||
|
ColumnLimit: 80
|
||||||
|
|
||||||
|
AlignEscapedNewlines: DontAlign
|
||||||
|
AlignTrailingComments:
|
||||||
|
Kind: Always
|
||||||
|
OverEmptyLines: 0
|
||||||
|
BasedOnStyle: WebKit
|
||||||
|
BraceWrapping:
|
||||||
|
AfterFunction: true
|
||||||
|
BreakBeforeBraces: Custom
|
||||||
|
BreakBeforeInheritanceComma: true
|
||||||
|
BreakConstructorInitializers: BeforeComma
|
||||||
|
IndentPPDirectives: AfterHash
|
||||||
|
IndentRequiresClause: false
|
||||||
|
InsertNewlineAtEOF: true
|
||||||
|
LineEnding: LF
|
||||||
|
NamespaceIndentation: None
|
||||||
|
PointerAlignment: Right # east pointer
|
||||||
|
QualifierAlignment: Right # east const
|
||||||
|
RemoveSemicolon: true
|
||||||
|
RequiresClausePosition: WithFollowing
|
||||||
|
RequiresExpressionIndentation: OuterScope
|
||||||
|
SpaceAfterTemplateKeyword: false
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
[Bb]uild*
|
[Bb]uild*
|
||||||
|
CMakeLists.txt.*
|
||||||
result
|
result
|
||||||
.cache
|
.cache
|
||||||
.direnv
|
.direnv
|
||||||
|
|||||||
101
CMakeLists.txt
101
CMakeLists.txt
@@ -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)
|
||||||
@@ -13,6 +28,8 @@ pkg_check_modules(GLES2 REQUIRED IMPORTED_TARGET glesv2)
|
|||||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||||
pkg_check_modules(LIBPORTAL REQUIRED IMPORTED_TARGET libportal)
|
pkg_check_modules(LIBPORTAL REQUIRED IMPORTED_TARGET libportal)
|
||||||
pkg_check_modules(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon)
|
pkg_check_modules(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon)
|
||||||
|
pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig)
|
||||||
|
pkg_check_modules(HARFBUZZ REQUIRED IMPORTED_TARGET harfbuzz)
|
||||||
pkg_check_modules(WAYLAND_PROTOCOLS REQUIRED wayland-protocols)
|
pkg_check_modules(WAYLAND_PROTOCOLS REQUIRED wayland-protocols)
|
||||||
pkg_check_modules(WLR_PROTOCOLS REQUIRED wlr-protocols)
|
pkg_check_modules(WLR_PROTOCOLS REQUIRED wlr-protocols)
|
||||||
|
|
||||||
@@ -29,6 +46,61 @@ set(PLATFORM DRM)
|
|||||||
set(BUILD_EXAMPLES OFF)
|
set(BUILD_EXAMPLES OFF)
|
||||||
FetchContent_MakeAvailable(raylib)
|
FetchContent_MakeAvailable(raylib)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
msdfgen
|
||||||
|
GIT_REPOSITORY https://github.com/Chlumsky/msdfgen.git
|
||||||
|
GIT_TAG "v1.12.1"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
set(MSDFGEN_BUILD_STANDALONE OFF)
|
||||||
|
set(MSDFGEN_USE_VCPKG OFF)
|
||||||
|
set(MSDFGEN_USE_SKIA OFF)
|
||||||
|
set(MSDFGEN_DISABLE_SVG ON)
|
||||||
|
set(MSDFGEN_DISABLE_PNG ON)
|
||||||
|
FetchContent_MakeAvailable(msdfgen)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
mINI
|
||||||
|
GIT_REPOSITORY https://github.com/metayeti/mINI.git
|
||||||
|
GIT_TAG "0.9.18"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(mINI)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
lunasvg
|
||||||
|
GIT_REPOSITORY https://github.com/sammycage/lunasvg.git
|
||||||
|
GIT_TAG "v3.5.0"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(lunasvg)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
SQLiteCpp
|
||||||
|
GIT_REPOSITORY https://github.com/SRombauts/SQLiteCpp.git
|
||||||
|
GIT_TAG "3.3.3"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(SQLiteCpp)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
cpptrace
|
||||||
|
GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
|
||||||
|
GIT_TAG "v1.0.1"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(cpptrace)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
tomlplusplus
|
||||||
|
GIT_REPOSITORY https://github.com/marzer/tomlplusplus
|
||||||
|
GIT_TAG "v3.4.0"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(tomlplusplus)
|
||||||
|
|
||||||
|
add_subdirectory(vendor)
|
||||||
|
|
||||||
find_program(WAYLAND_SCANNER wayland-scanner REQUIRED)
|
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)
|
||||||
@@ -78,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(
|
||||||
@@ -101,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
|
||||||
)
|
)
|
||||||
@@ -116,6 +197,14 @@ add_custom_target(generate_protocols ALL
|
|||||||
add_executable(waylight
|
add_executable(waylight
|
||||||
${GEN_C_PRIVATES}
|
${GEN_C_PRIVATES}
|
||||||
|
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Config.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/IconRegistry.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Cache.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/InotifyWatcher.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/TextRenderer.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/ImGui.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/App.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Tick.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp
|
||||||
)
|
)
|
||||||
add_dependencies(waylight generate_protocols)
|
add_dependencies(waylight generate_protocols)
|
||||||
@@ -132,8 +221,18 @@ target_link_libraries(waylight PRIVATE
|
|||||||
PkgConfig::GLIB
|
PkgConfig::GLIB
|
||||||
PkgConfig::LIBPORTAL
|
PkgConfig::LIBPORTAL
|
||||||
PkgConfig::XKBCOMMON
|
PkgConfig::XKBCOMMON
|
||||||
|
PkgConfig::FONTCONFIG
|
||||||
|
PkgConfig::HARFBUZZ
|
||||||
|
|
||||||
|
tomlplusplus::tomlplusplus
|
||||||
|
cpptrace::cpptrace
|
||||||
|
tinyfiledialogs
|
||||||
|
mINI
|
||||||
raylib
|
raylib
|
||||||
|
msdfgen::msdfgen-core
|
||||||
|
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/*
|
||||||
46
flake.nix
46
flake.nix
@@ -16,25 +16,7 @@
|
|||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
nativeBuildInputs = [ ];
|
nativeBuildInputs = with pkgs; [
|
||||||
buildInputs = with pkgs; [
|
|
||||||
cmake
|
|
||||||
ninja
|
|
||||||
];
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells.default = pkgs.mkShell.override { stdenv = pkgs.llvmPackages_21.libcxxStdenv; } {
|
|
||||||
packages =
|
|
||||||
with pkgs;
|
|
||||||
[
|
|
||||||
llvmPackages_21.clang-tools
|
|
||||||
lldb
|
|
||||||
codespell
|
|
||||||
doxygen
|
|
||||||
gtest
|
|
||||||
cppcheck
|
|
||||||
inotify-tools
|
|
||||||
|
|
||||||
pkg-config
|
pkg-config
|
||||||
wayland
|
wayland
|
||||||
wayland-protocols
|
wayland-protocols
|
||||||
@@ -44,6 +26,32 @@
|
|||||||
libportal
|
libportal
|
||||||
glib
|
glib
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
|
fontconfig
|
||||||
|
harfbuzz
|
||||||
|
sqlite
|
||||||
|
zenity
|
||||||
|
boost
|
||||||
|
];
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
cmake
|
||||||
|
ninja
|
||||||
|
];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default =
|
||||||
|
pkgs.mkShell.override { stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.gcc15Stdenv; }
|
||||||
|
{
|
||||||
|
packages =
|
||||||
|
with pkgs;
|
||||||
|
[
|
||||||
|
llvmPackages_21.clang-tools
|
||||||
|
lldb
|
||||||
|
gdb
|
||||||
|
codespell
|
||||||
|
doxygen
|
||||||
|
gtest
|
||||||
|
cppcheck
|
||||||
|
inotify-tools
|
||||||
]
|
]
|
||||||
++ buildInputs
|
++ buildInputs
|
||||||
++ nativeBuildInputs
|
++ nativeBuildInputs
|
||||||
|
|||||||
1328
src/App.cpp
Normal file
1328
src/App.cpp
Normal file
File diff suppressed because it is too large
Load Diff
243
src/App.hpp
Normal file
243
src/App.hpp
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <EGL/egl.h>
|
||||||
|
#include <libportal/portal.h>
|
||||||
|
#include <wayland-client-protocol.h>
|
||||||
|
extern "C" {
|
||||||
|
#include "blur-client-protocol.h"
|
||||||
|
#define namespace namespace_
|
||||||
|
#include "ext-background-effect-v1-client-protocol.h"
|
||||||
|
#include "text-input-unstable-v3-client-protocol.h"
|
||||||
|
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
|
||||||
|
#include <libportal/settings.h>
|
||||||
|
#undef namespace
|
||||||
|
}
|
||||||
|
#include <SQLiteCpp/SQLiteCpp.h>
|
||||||
|
#include <wayland-client.h>
|
||||||
|
#include <wayland-egl.h>
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
#include "Cache.hpp"
|
||||||
|
#include "Config.hpp"
|
||||||
|
#include "IconRegistry.hpp"
|
||||||
|
#include "ImGui.hpp"
|
||||||
|
#include "TextRenderer.hpp"
|
||||||
|
#include "Theme.hpp"
|
||||||
|
#include "common.hpp"
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
struct TypingBuffer : std::pmr::vector<u32> {
|
||||||
|
void push_utf8(char const *s);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
App();
|
||||||
|
~App();
|
||||||
|
|
||||||
|
auto run() -> void;
|
||||||
|
auto set_visible(bool visible) -> void;
|
||||||
|
auto visible() const -> bool { return m_visible; }
|
||||||
|
|
||||||
|
auto stop() -> void { m_running = false; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
auto init_wayland() -> void;
|
||||||
|
auto init_egl() -> void;
|
||||||
|
auto init_signal() -> void;
|
||||||
|
auto init_theme_portal() -> void;
|
||||||
|
auto pump_events() -> void;
|
||||||
|
auto tick() -> void;
|
||||||
|
auto create_layer_surface() -> void;
|
||||||
|
auto destroy_layer_surface() -> void;
|
||||||
|
auto ensure_egl_surface() -> void;
|
||||||
|
auto update_blur_region() -> void;
|
||||||
|
auto process_pending_text_input() -> void;
|
||||||
|
auto update_text_input_state(
|
||||||
|
std::pmr::string const &text, usize id, Rectangle field_rect) -> void;
|
||||||
|
auto theme() const -> ColorScheme const &
|
||||||
|
{
|
||||||
|
return m_themes[m_active_theme];
|
||||||
|
}
|
||||||
|
|
||||||
|
auto clipboard() const -> std::pmr::string const &
|
||||||
|
{
|
||||||
|
return m_clipboard_cache;
|
||||||
|
}
|
||||||
|
auto clipboard(std::string_view const &str) -> void;
|
||||||
|
|
||||||
|
static void on_settings_changed(XdpSettings * /*self*/, char const *ns,
|
||||||
|
char const *key, GVariant * /*value*/, gpointer data);
|
||||||
|
|
||||||
|
struct {
|
||||||
|
wl_display *display {};
|
||||||
|
wl_registry *registry {};
|
||||||
|
wl_compositor *compositor {};
|
||||||
|
wl_seat *seat {};
|
||||||
|
wl_keyboard *kbd {};
|
||||||
|
wl_surface *surface {};
|
||||||
|
zwlr_layer_shell_v1 *layer_shell {};
|
||||||
|
zwlr_layer_surface_v1 *layer_surface {};
|
||||||
|
ext_background_effect_manager_v1 *mgr {};
|
||||||
|
ext_background_effect_surface_v1 *eff {};
|
||||||
|
org_kde_kwin_blur_manager *kde_blur_mgr {};
|
||||||
|
org_kde_kwin_blur *kde_blur {};
|
||||||
|
zwp_text_input_manager_v3 *text_input_mgr {};
|
||||||
|
zwp_text_input_v3 *text_input {};
|
||||||
|
wl_data_device_manager *ddm {};
|
||||||
|
wl_data_device *ddev {};
|
||||||
|
wl_data_offer *curr_offer {};
|
||||||
|
wl_data_source *curr_source {};
|
||||||
|
} m_wayland;
|
||||||
|
std::pmr::string m_clipboard_cache;
|
||||||
|
u32 m_last_serial { 0 };
|
||||||
|
|
||||||
|
struct {
|
||||||
|
EGLDisplay edpy { EGL_NO_DISPLAY };
|
||||||
|
EGLConfig ecfg {};
|
||||||
|
EGLContext ectx { EGL_NO_CONTEXT };
|
||||||
|
EGLSurface esurf { EGL_NO_SURFACE };
|
||||||
|
wl_egl_window *wegl {};
|
||||||
|
} m_gl;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
XdpPortal *portal {};
|
||||||
|
XdpSettings *settings {};
|
||||||
|
} m_xdp;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
TypingBuffer typing {};
|
||||||
|
|
||||||
|
xkb_context *xkb_ctx_v {};
|
||||||
|
xkb_keymap *xkb_keymap_v {};
|
||||||
|
xkb_state *xkb_state_v {};
|
||||||
|
|
||||||
|
std::unordered_set<u32> held;
|
||||||
|
std::unordered_set<u32> pressed_syms;
|
||||||
|
std::unordered_set<u32> released_syms;
|
||||||
|
|
||||||
|
auto is_down_evdev(u32 evdev) const -> bool
|
||||||
|
{
|
||||||
|
return held.find(evdev) != held.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto is_down_sym(xkb_keysym_t sym) const -> bool
|
||||||
|
{
|
||||||
|
if (!xkb_state_v)
|
||||||
|
return false;
|
||||||
|
return std::any_of(held.begin(), held.end(), [&](u32 const k) {
|
||||||
|
return (xkb_state_key_get_one_sym(
|
||||||
|
xkb_state_v, static_cast<xkb_keycode_t>(k + 8))
|
||||||
|
== sym);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto is_sym_pressed(xkb_keysym_t sym) const -> bool
|
||||||
|
{
|
||||||
|
return pressed_syms.find(sym) != pressed_syms.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto is_sym_released(xkb_keysym_t sym) const -> bool
|
||||||
|
{
|
||||||
|
return released_syms.find(sym) != released_syms.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mod_active(char const *name) const -> bool
|
||||||
|
{
|
||||||
|
return xkb_state_v
|
||||||
|
&& xkb_state_mod_name_is_active(
|
||||||
|
xkb_state_v, name, XKB_STATE_MODS_EFFECTIVE)
|
||||||
|
> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ctrl() const -> bool { return mod_active("Control"); }
|
||||||
|
auto shift() const -> bool { return mod_active("Shift"); }
|
||||||
|
|
||||||
|
void clear_transients()
|
||||||
|
{
|
||||||
|
pressed_syms.clear();
|
||||||
|
released_syms.clear();
|
||||||
|
}
|
||||||
|
} m_kbd;
|
||||||
|
|
||||||
|
std::shared_ptr<TextRenderer> m_tr { nullptr };
|
||||||
|
FontHandle m_font;
|
||||||
|
std::shared_ptr<ImGui> m_gui { nullptr };
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool supported { false };
|
||||||
|
bool seat_focus { false };
|
||||||
|
bool enabled { false };
|
||||||
|
bool pending_done { false };
|
||||||
|
uint32_t pending_serial { 0 };
|
||||||
|
uint32_t sent_serial { 0 };
|
||||||
|
|
||||||
|
std::pmr::string *bound_text { nullptr };
|
||||||
|
usize bound_id { 0 };
|
||||||
|
Rectangle bound_rect {};
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool has_preedit { false };
|
||||||
|
std::string preedit_text;
|
||||||
|
int cursor_begin { 0 };
|
||||||
|
int cursor_end { 0 };
|
||||||
|
bool has_commit { false };
|
||||||
|
std::string commit_text;
|
||||||
|
bool has_delete { false };
|
||||||
|
uint32_t before { 0 };
|
||||||
|
uint32_t after { 0 };
|
||||||
|
} pending;
|
||||||
|
|
||||||
|
std::string last_surrounding;
|
||||||
|
int last_cursor { 0 };
|
||||||
|
int last_anchor { 0 };
|
||||||
|
Rectangle last_cursor_rect {};
|
||||||
|
bool last_cursor_visible { false };
|
||||||
|
bool surrounding_dirty { false };
|
||||||
|
} m_ime;
|
||||||
|
|
||||||
|
auto get_texture(std::filesystem::path const &path) -> Texture2D const &
|
||||||
|
{
|
||||||
|
if (m_textures.contains(path)) {
|
||||||
|
return m_textures[path];
|
||||||
|
}
|
||||||
|
auto fname = path.c_str();
|
||||||
|
assert(fname);
|
||||||
|
TraceLog(LOG_INFO, std::format("loading texture at {}", fname).c_str());
|
||||||
|
auto const tex = LoadTexture(fname);
|
||||||
|
assert(IsTextureValid(tex));
|
||||||
|
m_textures[path] = tex;
|
||||||
|
return m_textures[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
void execute_command(bool terminal, std::string_view const command);
|
||||||
|
|
||||||
|
// NOTE: Canonicalize first!
|
||||||
|
std::unordered_map<std::filesystem::path, Texture2D> m_textures;
|
||||||
|
|
||||||
|
enum_array<Theme, ColorScheme> m_themes { make_default_themes() };
|
||||||
|
Theme m_active_theme { Theme::Light };
|
||||||
|
IconRegistry m_ir;
|
||||||
|
std::optional<Cache> m_cache;
|
||||||
|
Config m_config;
|
||||||
|
|
||||||
|
int m_win_w { 800 };
|
||||||
|
int m_win_h { 600 };
|
||||||
|
bool m_running { true };
|
||||||
|
bool m_visible { true };
|
||||||
|
|
||||||
|
Color m_accent_color { 127, 127, 255, 255 };
|
||||||
|
|
||||||
|
std::filesystem::path m_data_home_dir {};
|
||||||
|
std::filesystem::path m_config_home_dir {};
|
||||||
|
std::shared_ptr<SQLite::Database> m_db {};
|
||||||
|
int m_sfd { -1 };
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
435
src/Cache.cpp
Normal file
435
src/Cache.cpp
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
#include "Cache.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <numeric>
|
||||||
|
#include <ranges>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <SQLiteCpp/Statement.h>
|
||||||
|
#include <SQLiteCpp/Transaction.h>
|
||||||
|
#include <mini/ini.h>
|
||||||
|
#include <raylib.h>
|
||||||
|
|
||||||
|
#include "common.hpp"
|
||||||
|
#include <sys/inotify.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
void replace_all(
|
||||||
|
std::string &str, std::string const &from, std::string const &to)
|
||||||
|
{
|
||||||
|
if (from.empty())
|
||||||
|
return;
|
||||||
|
size_t pos = 0;
|
||||||
|
while ((pos = str.find(from, pos)) != std::string::npos) {
|
||||||
|
str.replace(pos, from.size(), to);
|
||||||
|
pos += to.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::Cache(std::shared_ptr<SQLite::Database> db)
|
||||||
|
: m_db(db)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto const *env { getenv("XDG_DATA_DIRS") };
|
||||||
|
if (env && *env) {
|
||||||
|
std::ranges::copy(std::string_view(env) | std::views::split(':')
|
||||||
|
| std::views::transform([](auto &&s) {
|
||||||
|
return std::filesystem::path(s.begin(), s.end())
|
||||||
|
/ "applications";
|
||||||
|
})
|
||||||
|
| std::views::filter([](auto &&p) {
|
||||||
|
if (!std::filesystem::is_directory(p))
|
||||||
|
return false;
|
||||||
|
if (std::filesystem::directory_iterator(p)
|
||||||
|
== std::filesystem::directory_iterator {})
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
std::back_inserter(m_app_dirs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
auto total = std::accumulate(m_app_dirs.begin(), m_app_dirs.end(),
|
||||||
|
static_cast<usize>(0), [](usize acc, auto &&dir) {
|
||||||
|
return acc
|
||||||
|
+ std::count_if(std::filesystem::directory_iterator(dir),
|
||||||
|
std::filesystem::directory_iterator {}, [](auto &&entry) {
|
||||||
|
return entry.is_regular_file()
|
||||||
|
&& entry.path().extension() == ".desktop";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (total != m_apps.size()) {
|
||||||
|
rescan();
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceLog(LOG_DEBUG, std::format("Applications in cache:").c_str());
|
||||||
|
for (auto const &app : m_apps) {
|
||||||
|
TraceLog(LOG_DEBUG,
|
||||||
|
std::format("{}:", app.desktop_entry_path.string()).c_str());
|
||||||
|
if (app.comment)
|
||||||
|
TraceLog(
|
||||||
|
LOG_DEBUG, std::format(" - Comment: {}", *app.comment).c_str());
|
||||||
|
if (app.path)
|
||||||
|
TraceLog(LOG_DEBUG, std::format(" - Path: {}", *app.path).c_str());
|
||||||
|
TraceLog(
|
||||||
|
LOG_DEBUG, std::format(" - Terminal: {}", app.terminal).c_str());
|
||||||
|
TraceLog(
|
||||||
|
LOG_DEBUG, std::format(" - NoDisplay: {}", app.no_display).c_str());
|
||||||
|
TraceLog(LOG_DEBUG, std::format(" - Actions:").c_str());
|
||||||
|
for (auto const &action : app.actions) {
|
||||||
|
TraceLog(
|
||||||
|
LOG_DEBUG, std::format(" - Name: {}", action.name).c_str());
|
||||||
|
if (action.exec)
|
||||||
|
TraceLog(LOG_DEBUG,
|
||||||
|
std::format(" Exec: {}", *action.exec).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto const &dir : m_app_dirs) {
|
||||||
|
m_inotify.watch_path_recursively(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_inotify.set_callback([this](FileWatchEvent const &event) {
|
||||||
|
auto const mask = event.mask;
|
||||||
|
if (mask & IN_Q_OVERFLOW) {
|
||||||
|
rescan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask & IN_ISDIR) {
|
||||||
|
rescan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask & (IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM
|
||||||
|
| IN_MOVED_TO | IN_CLOSE_WRITE)) {
|
||||||
|
if (event.path.extension() == ".desktop") {
|
||||||
|
rescan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_inotify_thread = std::thread([this]() { m_inotify.run(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::~Cache()
|
||||||
|
{
|
||||||
|
m_inotify.stop();
|
||||||
|
if (m_inotify_thread.joinable())
|
||||||
|
m_inotify_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cache::rescan()
|
||||||
|
{
|
||||||
|
m_apps.clear();
|
||||||
|
|
||||||
|
int id = 0;
|
||||||
|
for (auto const &dir : m_app_dirs) {
|
||||||
|
for (auto const &file : std::filesystem::directory_iterator(dir)) {
|
||||||
|
if (!file.is_regular_file())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (file.path().extension() != ".desktop")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
mINI::INIFile ini_file(file.path());
|
||||||
|
mINI::INIStructure ini;
|
||||||
|
ini_file.read(ini);
|
||||||
|
|
||||||
|
constexpr auto read_action = [&](std::string const
|
||||||
|
&desktop_file_uri,
|
||||||
|
mINI::INIMap<std::string> const
|
||||||
|
§ion) {
|
||||||
|
auto const name = section.get("Name");
|
||||||
|
auto const icon = [&]()
|
||||||
|
-> std::optional<
|
||||||
|
std::variant<std::filesystem::path, std::string>> {
|
||||||
|
if (section.has("Icon")) {
|
||||||
|
auto const icon_name = section.get("Icon");
|
||||||
|
if (!icon_name.empty()) {
|
||||||
|
if (icon_name[0] == '/') {
|
||||||
|
return std::filesystem::path(icon_name);
|
||||||
|
} else {
|
||||||
|
return icon_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return ApplicationCache::Action {
|
||||||
|
.name = name,
|
||||||
|
.exec = [&]() -> std::optional<std::string> {
|
||||||
|
if (section.has("Exec")) {
|
||||||
|
auto s = section.get("Exec");
|
||||||
|
if (!s.empty()) {
|
||||||
|
// Either deprecated or not used...
|
||||||
|
replace_all(s, "%f", "");
|
||||||
|
replace_all(s, "%F", "");
|
||||||
|
replace_all(s, "%u", "");
|
||||||
|
replace_all(s, "%U", "");
|
||||||
|
replace_all(s, "%d", "");
|
||||||
|
replace_all(s, "%D", "");
|
||||||
|
replace_all(s, "%n", "");
|
||||||
|
replace_all(s, "%N", "");
|
||||||
|
replace_all(s, "%v", "");
|
||||||
|
replace_all(s, "%M", "");
|
||||||
|
|
||||||
|
replace_all(s, "%c", name);
|
||||||
|
if (icon) {
|
||||||
|
if (auto const p
|
||||||
|
= std::get_if<std::filesystem::path>(
|
||||||
|
&*icon)) {
|
||||||
|
replace_all(s, "%i",
|
||||||
|
"--icon '" + p->string() + "'");
|
||||||
|
} else if (auto const n
|
||||||
|
= std::get_if<std::string>(&*icon)) {
|
||||||
|
replace_all(
|
||||||
|
s, "%i", "--icon '" + *n + "'");
|
||||||
|
} else {
|
||||||
|
replace_all(s, "%i", "");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
replace_all(s, "%i", "");
|
||||||
|
}
|
||||||
|
replace_all(s, "%k", "'" + desktop_file_uri);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}(),
|
||||||
|
.icon = icon,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
m_apps.push_back({
|
||||||
|
.id = id++,
|
||||||
|
.desktop_entry_path = file.path(),
|
||||||
|
.type =
|
||||||
|
[&]() {
|
||||||
|
auto const type_str { ini["Desktop Entry"].get(
|
||||||
|
"Type") };
|
||||||
|
auto type { ApplicationCache::Type::Application };
|
||||||
|
if (type_str == "Application")
|
||||||
|
type = ApplicationCache::Type::Application;
|
||||||
|
else if (type_str == "Link")
|
||||||
|
type = ApplicationCache::Type::Link;
|
||||||
|
else if (type_str == "Directory")
|
||||||
|
type = ApplicationCache::Type::Directory;
|
||||||
|
return type;
|
||||||
|
}(),
|
||||||
|
.terminal =
|
||||||
|
[&]() {
|
||||||
|
if (ini["Desktop Entry"].has("Terminal")) {
|
||||||
|
return ini["Desktop Entry"]["Terminal"] == "true"
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}(),
|
||||||
|
.no_display =
|
||||||
|
[&]() {
|
||||||
|
if (ini["Desktop Entry"].has("NoDisplay")) {
|
||||||
|
return ini["Desktop Entry"]["NoDisplay"] == "true"
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}(),
|
||||||
|
.path = [&]() -> std::optional<std::string> {
|
||||||
|
if (ini["Desktop Entry"].has("Path")) {
|
||||||
|
return ini["Desktop Entry"]["Path"];
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}(),
|
||||||
|
.comment = [&]() -> std::optional<std::string> {
|
||||||
|
if (ini["Desktop Entry"].has("Comment")) {
|
||||||
|
return ini["Desktop Entry"]["Comment"];
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}(),
|
||||||
|
.actions =
|
||||||
|
[&]() {
|
||||||
|
std::vector<ApplicationCache::Action> actions;
|
||||||
|
for (auto const &[_, v] : ini) {
|
||||||
|
try {
|
||||||
|
auto const action = read_action(
|
||||||
|
std::filesystem::canonical(file.path())
|
||||||
|
.string(),
|
||||||
|
v);
|
||||||
|
actions.push_back(action);
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
}(),
|
||||||
|
.dbus_activatable =
|
||||||
|
[&]() {
|
||||||
|
if (ini["Desktop Entry"].has("DBusActivatable")) {
|
||||||
|
return ini["Desktop Entry"]["DBusActivatable"]
|
||||||
|
== "true"
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cache::dump()
|
||||||
|
{
|
||||||
|
SQLite::Transaction tx(*m_db);
|
||||||
|
|
||||||
|
SQLite::Statement(*m_db, "DELETE FROM ApplicationCache").exec();
|
||||||
|
SQLite::Statement(*m_db, "DELETE FROM ApplicationActionCache").exec();
|
||||||
|
|
||||||
|
try {
|
||||||
|
SQLite::Statement(
|
||||||
|
*m_db, "DELETE FROM sqlite_sequence WHERE name='ApplicationCache'")
|
||||||
|
.exec();
|
||||||
|
SQLite::Statement(*m_db,
|
||||||
|
"DELETE FROM sqlite_sequence WHERE name='ApplicationActionCache'")
|
||||||
|
.exec();
|
||||||
|
} catch (std::exception const &) {
|
||||||
|
}
|
||||||
|
|
||||||
|
SQLite::Statement ins_app(*m_db,
|
||||||
|
"INSERT INTO ApplicationCache(type, desktop_entry_path, terminal, "
|
||||||
|
"no_display, path, comment, dbus_activatable) VALUES (?,?,?,?,?,?,?)");
|
||||||
|
|
||||||
|
SQLite::Statement ins_act(*m_db,
|
||||||
|
"INSERT INTO ApplicationActionCache(id_app, name, exec, icon) VALUES "
|
||||||
|
"(?,?,?,?)");
|
||||||
|
|
||||||
|
for (auto &app : m_apps) {
|
||||||
|
ins_app.reset();
|
||||||
|
ins_app.clearBindings();
|
||||||
|
ins_app.bind(1, static_cast<int>(app.type));
|
||||||
|
ins_app.bind(2, app.desktop_entry_path.string());
|
||||||
|
ins_app.bind(3, app.terminal ? 1 : 0);
|
||||||
|
ins_app.bind(4, app.no_display ? 1 : 0);
|
||||||
|
if (app.path)
|
||||||
|
ins_app.bind(5, *app.path);
|
||||||
|
else
|
||||||
|
ins_app.bind(5);
|
||||||
|
if (app.comment)
|
||||||
|
ins_app.bind(6, *app.comment);
|
||||||
|
else
|
||||||
|
ins_app.bind(6);
|
||||||
|
ins_app.bind(7, app.dbus_activatable ? 1 : 0);
|
||||||
|
ins_app.exec();
|
||||||
|
|
||||||
|
app.id = m_db->getLastInsertRowid();
|
||||||
|
|
||||||
|
for (auto const &action : app.actions) {
|
||||||
|
ins_act.reset();
|
||||||
|
ins_act.clearBindings();
|
||||||
|
ins_act.bind(1, app.id);
|
||||||
|
ins_act.bind(2, action.name);
|
||||||
|
if (action.exec)
|
||||||
|
ins_act.bind(3, *action.exec);
|
||||||
|
else
|
||||||
|
ins_act.bind(3);
|
||||||
|
|
||||||
|
if (action.icon) {
|
||||||
|
std::string str;
|
||||||
|
if (auto const *s = std::get_if<std::string>(&*action.icon)) {
|
||||||
|
str = *s;
|
||||||
|
} else if (auto const *p
|
||||||
|
= std::get_if<std::filesystem::path>(&*action.icon)) {
|
||||||
|
str = std::filesystem::canonical(*p).string();
|
||||||
|
}
|
||||||
|
ins_act.bind(4, str);
|
||||||
|
} else {
|
||||||
|
ins_act.bind(4);
|
||||||
|
}
|
||||||
|
ins_act.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cache::load()
|
||||||
|
{
|
||||||
|
m_apps.clear();
|
||||||
|
|
||||||
|
SQLite::Statement get_apps(*m_db,
|
||||||
|
"SELECT id, type, desktop_entry_path, terminal, no_display, path, "
|
||||||
|
"comment, dbus_activatable "
|
||||||
|
"FROM ApplicationCache");
|
||||||
|
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> id_to_index;
|
||||||
|
|
||||||
|
while (get_apps.executeStep()) {
|
||||||
|
ApplicationCache app {};
|
||||||
|
app.id = get_apps.getColumn(0).getInt64();
|
||||||
|
app.type = static_cast<ApplicationCache::Type>(
|
||||||
|
get_apps.getColumn(1).getInt());
|
||||||
|
app.desktop_entry_path
|
||||||
|
= std::filesystem::path(get_apps.getColumn(2).getString());
|
||||||
|
app.terminal = get_apps.getColumn(3).getInt() != 0;
|
||||||
|
app.no_display = get_apps.getColumn(4).getInt() != 0;
|
||||||
|
|
||||||
|
if (!get_apps.getColumn(5).isNull())
|
||||||
|
app.path = std::string(get_apps.getColumn(5).getString());
|
||||||
|
else
|
||||||
|
app.path.reset();
|
||||||
|
|
||||||
|
if (!get_apps.getColumn(6).isNull())
|
||||||
|
app.comment = std::string(get_apps.getColumn(6).getString());
|
||||||
|
else
|
||||||
|
app.comment.reset();
|
||||||
|
|
||||||
|
app.dbus_activatable = get_apps.getColumn(7).getInt() != 0;
|
||||||
|
|
||||||
|
id_to_index.emplace(app.id, m_apps.size());
|
||||||
|
m_apps.push_back(std::move(app));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_apps.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
SQLite::Statement get_actions(*m_db,
|
||||||
|
"SELECT id_app, name, exec, icon "
|
||||||
|
"FROM ApplicationActionCache "
|
||||||
|
"ORDER BY id_app");
|
||||||
|
|
||||||
|
while (get_actions.executeStep()) {
|
||||||
|
auto id_app = get_actions.getColumn(0).getInt64();
|
||||||
|
auto it = id_to_index.find(id_app);
|
||||||
|
if (it == id_to_index.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ApplicationCache::Action action {};
|
||||||
|
action.name = std::string(get_actions.getColumn(1).getString());
|
||||||
|
|
||||||
|
if (!get_actions.getColumn(2).isNull())
|
||||||
|
action.exec = std::string(get_actions.getColumn(2).getString());
|
||||||
|
else
|
||||||
|
action.exec.reset();
|
||||||
|
|
||||||
|
if (!get_actions.getColumn(3).isNull()) {
|
||||||
|
auto const str = get_actions.getColumn(3).getString();
|
||||||
|
if (str.at(0) == '/') {
|
||||||
|
action.icon
|
||||||
|
= std::filesystem::canonical(std::filesystem::path(str));
|
||||||
|
} else {
|
||||||
|
action.icon = str;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
action.icon.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_apps[it->second].actions.push_back(std::move(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
66
src/Cache.hpp
Normal file
66
src/Cache.hpp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "InotifyWatcher.hpp"
|
||||||
|
#include <SQLiteCpp/Database.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
struct ApplicationCache {
|
||||||
|
enum class Type {
|
||||||
|
Application,
|
||||||
|
Link,
|
||||||
|
Directory,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Action {
|
||||||
|
std::string name;
|
||||||
|
// May not exist if DBusActivable=true
|
||||||
|
std::optional<std::string> exec;
|
||||||
|
// Freedesktop Desktop Entry Spec 11.2 Table 3 says:
|
||||||
|
//
|
||||||
|
// If the name is an absolute path, the given file will be used.
|
||||||
|
// If the name is not an absolute path, the algorithm described in
|
||||||
|
// the Icon Theme Specification will be used to locate the icon.
|
||||||
|
//
|
||||||
|
// Thus, when deserializing, we will just check if it starts with /
|
||||||
|
// to determine type.
|
||||||
|
std::optional<std::variant<std::filesystem::path, std::string>> icon;
|
||||||
|
};
|
||||||
|
|
||||||
|
int id;
|
||||||
|
std::filesystem::path desktop_entry_path;
|
||||||
|
|
||||||
|
Type type { Type::Application };
|
||||||
|
bool terminal { false };
|
||||||
|
bool no_display { false };
|
||||||
|
std::optional<std::string> path;
|
||||||
|
std::optional<std::string> comment;
|
||||||
|
std::vector<Action> actions; // There should always be at least 1.
|
||||||
|
bool dbus_activatable {}; // Unimplemented for now.
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Cache {
|
||||||
|
explicit Cache(std::shared_ptr<SQLite::Database> db);
|
||||||
|
~Cache();
|
||||||
|
|
||||||
|
void rescan();
|
||||||
|
void dump();
|
||||||
|
void load();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<ApplicationCache> m_apps;
|
||||||
|
std::vector<std::filesystem::path> m_app_dirs;
|
||||||
|
std::shared_ptr<SQLite::Database> m_db;
|
||||||
|
|
||||||
|
InotifyWatcher m_inotify;
|
||||||
|
std::thread m_inotify_thread;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
47
src/Config.cpp
Normal file
47
src/Config.cpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#include "Config.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <print>
|
||||||
|
|
||||||
|
#include <toml++/toml.hpp>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
void Config::write(std::filesystem::path const &path)
|
||||||
|
{
|
||||||
|
std::ofstream f(path);
|
||||||
|
if (!f) {
|
||||||
|
throw std::runtime_error("Failed to open config file for writing");
|
||||||
|
}
|
||||||
|
std::println(f, "[settings]");
|
||||||
|
std::println(f, "terminal_cmdline=\"{}\"", terminal_cmdline);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Config::load(std::filesystem::path const &config_dir_path) -> Config const
|
||||||
|
{
|
||||||
|
if (!std::filesystem::is_directory(config_dir_path))
|
||||||
|
throw std::runtime_error("Provided path is not a directory!");
|
||||||
|
|
||||||
|
Config cfg {};
|
||||||
|
|
||||||
|
std::filesystem::path path_config { config_dir_path / "config.toml" };
|
||||||
|
if (!std::filesystem::is_regular_file(path_config)) {
|
||||||
|
try {
|
||||||
|
std::filesystem::remove_all(path_config);
|
||||||
|
} catch (std::exception const &e) {
|
||||||
|
}
|
||||||
|
cfg.write(path_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::println("Config file: {}", path_config.string());
|
||||||
|
auto const tbl { toml::parse_file(path_config.string()) };
|
||||||
|
auto const terminal_cmdline { tbl["settings"]["terminal_cmdline"].value_or(
|
||||||
|
"kitty -c") };
|
||||||
|
|
||||||
|
cfg.terminal_cmdline = terminal_cmdline;
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
17
src/Config.hpp
Normal file
17
src/Config.hpp
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
std::string terminal_cmdline { "kitty" };
|
||||||
|
|
||||||
|
void write(std::filesystem::path const &path);
|
||||||
|
|
||||||
|
static auto load(std::filesystem::path const &config_dir_path)
|
||||||
|
-> Config const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
449
src/IconRegistry.cpp
Normal file
449
src/IconRegistry.cpp
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
#include "IconRegistry.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <ranges>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include <gio/gio.h>
|
||||||
|
#include <lunasvg.h>
|
||||||
|
#include <mini/ini.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
static inline auto color_to_string(Color const &c) -> std::string
|
||||||
|
{
|
||||||
|
auto const r { c.r / 255.0 }, g { c.g / 255.0 }, b { c.b / 255.0 };
|
||||||
|
|
||||||
|
auto const maxv { std::fmax(r, std::fmax(g, b)) };
|
||||||
|
auto const minv { std::fmin(r, std::fmin(g, b)) };
|
||||||
|
auto const d { maxv - minv };
|
||||||
|
|
||||||
|
double h = 0.0;
|
||||||
|
if (d > 1e-6) {
|
||||||
|
if (maxv == r)
|
||||||
|
h = 60.0 * std::fmod(((g - b) / d), 6.0);
|
||||||
|
else if (maxv == g)
|
||||||
|
h = 60.0 * (((b - r) / d) + 2.0);
|
||||||
|
else
|
||||||
|
h = 60.0 * (((r - g) / d) + 4.0);
|
||||||
|
}
|
||||||
|
if (h < 0.0)
|
||||||
|
h += 360.0;
|
||||||
|
|
||||||
|
if (h >= 345 || h < 15)
|
||||||
|
return "red";
|
||||||
|
if (h < 45)
|
||||||
|
return "orange";
|
||||||
|
if (h < 70)
|
||||||
|
return "yellow";
|
||||||
|
if (h < 170)
|
||||||
|
return "green";
|
||||||
|
if (h < 200)
|
||||||
|
return "teal";
|
||||||
|
if (h < 250)
|
||||||
|
return "cyan";
|
||||||
|
if (h < 290)
|
||||||
|
return "blue";
|
||||||
|
if (h < 330)
|
||||||
|
return "purple";
|
||||||
|
return "pink";
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto detect_desktop_environment() -> std::string const
|
||||||
|
{
|
||||||
|
if (auto const de { getenv("XDG_CURRENT_DESKTOP") })
|
||||||
|
return de;
|
||||||
|
if (auto const sess { getenv("DESKTOP_SESSION") })
|
||||||
|
return sess;
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto kde_get_theme() -> std::string const
|
||||||
|
{
|
||||||
|
std::string home { getenv("HOME") ? getenv("HOME") : "" };
|
||||||
|
|
||||||
|
std::string const paths[] {
|
||||||
|
home + "/.config/kdeglobals",
|
||||||
|
home + "/.config/kdedefaults/kdeglobals",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto const &p : paths) {
|
||||||
|
std::ifstream f(p);
|
||||||
|
if (!f)
|
||||||
|
continue;
|
||||||
|
std::string line;
|
||||||
|
auto in_icons { false };
|
||||||
|
while (std::getline(f, line)) {
|
||||||
|
if (line == "[Icons]") {
|
||||||
|
in_icons = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.starts_with("["))
|
||||||
|
in_icons = false;
|
||||||
|
if (in_icons && line.starts_with("Theme="))
|
||||||
|
return line.substr(strlen("Theme="));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto other_get_theme() -> std::string
|
||||||
|
{
|
||||||
|
char const *schema_id { "org.gnome.desktop.interface" };
|
||||||
|
char const *key { "icon-theme" };
|
||||||
|
|
||||||
|
GSettingsSchemaSource *src { g_settings_schema_source_get_default() };
|
||||||
|
if (!src)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
GSettingsSchema *schema { g_settings_schema_source_lookup(
|
||||||
|
src, schema_id, TRUE) };
|
||||||
|
if (!schema)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
GSettings *settings { g_settings_new_full(schema, nullptr, nullptr) };
|
||||||
|
g_settings_schema_unref(schema);
|
||||||
|
if (!settings)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
gchar *cstr { g_settings_get_string(settings, key) };
|
||||||
|
g_object_unref(settings);
|
||||||
|
if (!cstr)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
std::string theme { cstr };
|
||||||
|
g_free(cstr);
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto get_current_icon_theme() -> std::optional<std::string> const
|
||||||
|
{
|
||||||
|
auto de { detect_desktop_environment() };
|
||||||
|
std::transform(de.begin(), de.end(), de.begin(), ::tolower);
|
||||||
|
|
||||||
|
if (de.find("kde") != std::string::npos
|
||||||
|
|| de.find("plasma") != std::string::npos) {
|
||||||
|
if (auto const t = kde_get_theme(); !t.empty()) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (auto const t = other_get_theme(); !t.empty()) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto IconTheme::lookup(std::string_view const name,
|
||||||
|
std::optional<int> optimal_size) const -> Icon const
|
||||||
|
{
|
||||||
|
for (auto const &dir : m_directories) {
|
||||||
|
if (optimal_size && *optimal_size < dir.size)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (auto const &dir_entry :
|
||||||
|
std::filesystem::recursive_directory_iterator(dir.path)) {
|
||||||
|
if (!dir_entry.is_regular_file())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (dir_entry.path().stem() != name)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// This can be derived from the image filename.
|
||||||
|
// But we probably won't need it either way...
|
||||||
|
if (dir_entry.path().extension() == ".icon")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (dir_entry.path().extension() == ".svg") {
|
||||||
|
auto const document { lunasvg::Document::loadFromFile(
|
||||||
|
dir_entry.path()) };
|
||||||
|
if (!document) {
|
||||||
|
throw std::runtime_error("Failed to load SVG file");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const bitmap { document->renderToBitmap() };
|
||||||
|
if (bitmap.width() == 0 || bitmap.height() == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
std::vector<unsigned char> rgba(
|
||||||
|
bitmap.width() * bitmap.height() * 4);
|
||||||
|
for (size_t i = 0, px = bitmap.width() * bitmap.height();
|
||||||
|
i < px; ++i) {
|
||||||
|
auto *src { bitmap.data() };
|
||||||
|
|
||||||
|
uint8_t b { src[i * 4 + 0] };
|
||||||
|
uint8_t g { src[i * 4 + 1] };
|
||||||
|
uint8_t r { src[i * 4 + 2] };
|
||||||
|
uint8_t a { src[i * 4 + 3] };
|
||||||
|
|
||||||
|
if (a != 0) {
|
||||||
|
r = (uint8_t)std::min(
|
||||||
|
255, (int)((r * 255 + a / 2) / a));
|
||||||
|
g = (uint8_t)std::min(
|
||||||
|
255, (int)((g * 255 + a / 2) / a));
|
||||||
|
b = (uint8_t)std::min(
|
||||||
|
255, (int)((b * 255 + a / 2) / a));
|
||||||
|
}
|
||||||
|
|
||||||
|
rgba[i * 4 + 0] = r;
|
||||||
|
rgba[i * 4 + 1] = g;
|
||||||
|
rgba[i * 4 + 2] = b;
|
||||||
|
rgba[i * 4 + 3] = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image const img {
|
||||||
|
.data = rgba.data(),
|
||||||
|
.width = bitmap.width(),
|
||||||
|
.height = bitmap.height(),
|
||||||
|
.mipmaps = 1,
|
||||||
|
.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
|
||||||
|
};
|
||||||
|
|
||||||
|
auto const tex { LoadTextureFromImage(img) };
|
||||||
|
if (!IsTextureValid(tex)) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Failed to load texture from image");
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon const icon(dir_entry.path(), tex, dir.size);
|
||||||
|
return icon;
|
||||||
|
} else {
|
||||||
|
auto const tex { LoadTexture(dir_entry.path().c_str()) };
|
||||||
|
if (!IsTextureValid(tex)) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Failed to load texture from file");
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon const icon(dir_entry.path(), tex, dir.size);
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optimal_size) {
|
||||||
|
// We failed to find a icon big enough, try again with smaller sizes
|
||||||
|
// than our optimal.
|
||||||
|
return lookup(name, std::nullopt);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error(
|
||||||
|
std::format("Failed to find icon `{}` in theme!", name));
|
||||||
|
}
|
||||||
|
|
||||||
|
IconTheme::IconTheme(std::filesystem::path const &themes_directory_path)
|
||||||
|
{
|
||||||
|
for (auto const &dir :
|
||||||
|
std::filesystem::directory_iterator(themes_directory_path)) {
|
||||||
|
if (!dir.is_directory())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto const index_path = dir.path() / "index.theme";
|
||||||
|
if (!std::filesystem::is_regular_file(index_path))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
m_names.push_back(dir.path().filename().string());
|
||||||
|
|
||||||
|
mINI::INIFile ini_file(index_path);
|
||||||
|
mINI::INIStructure ini;
|
||||||
|
ini_file.read(ini);
|
||||||
|
|
||||||
|
auto const &inherits { ini["Icon Theme"]["Inherits"] };
|
||||||
|
std::ranges::copy(std::string_view(inherits) | std::views::split(',')
|
||||||
|
| std::views::transform(
|
||||||
|
[](auto &&s) { return std::string(s.begin(), s.end()); }),
|
||||||
|
std::back_inserter(m_inherits));
|
||||||
|
|
||||||
|
auto const &directories { ini["Icon Theme"]["Directories"] };
|
||||||
|
for (auto const &&dir_entry : directories | std::views::split(',')) {
|
||||||
|
auto const dir_entry_str { std::string(
|
||||||
|
dir_entry.begin(), dir_entry.end()) };
|
||||||
|
auto const path { std::filesystem::path(dir_entry_str) };
|
||||||
|
auto const path_actual { dir.path() / path };
|
||||||
|
|
||||||
|
if (!std::filesystem::is_directory(path_actual))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto const &type_raw { ini[dir_entry_str]["Type"] };
|
||||||
|
DirectoryEntry::Type type;
|
||||||
|
if (type_raw == "Fixed") {
|
||||||
|
type = DirectoryEntry::Type::Fixed;
|
||||||
|
} else if (type_raw == "Scalable") {
|
||||||
|
type = DirectoryEntry::Type::Scalable;
|
||||||
|
} else if (type_raw == "Threshold") {
|
||||||
|
type = DirectoryEntry::Type::Threshold;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const &context_raw { ini[dir_entry_str]["Context"] };
|
||||||
|
DirectoryEntry::Context context;
|
||||||
|
if (context_raw == "Actions") {
|
||||||
|
context = DirectoryEntry::Context::Actions;
|
||||||
|
} else if (context_raw == "Devices") {
|
||||||
|
context = DirectoryEntry::Context::Devices;
|
||||||
|
} else if (context_raw == "FileSystems") {
|
||||||
|
context = DirectoryEntry::Context::FileSystems;
|
||||||
|
} else if (context_raw == "MimeTypes") {
|
||||||
|
context = DirectoryEntry::Context::MimeTypes;
|
||||||
|
} else if (context_raw == "Places") {
|
||||||
|
context = DirectoryEntry::Context::Places;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size { std::atoi(ini[dir_entry_str]["Size"].c_str()) };
|
||||||
|
if (size == 0) {
|
||||||
|
if (type == DirectoryEntry::Type::Scalable) {
|
||||||
|
int minSize
|
||||||
|
= std::atoi(ini[dir_entry_str]["MinSize"].c_str());
|
||||||
|
int maxSize
|
||||||
|
= std::atoi(ini[dir_entry_str]["MaxSize"].c_str());
|
||||||
|
size = std::max(minSize, maxSize);
|
||||||
|
}
|
||||||
|
if (size == 0)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_directories.push_back({
|
||||||
|
.path = path_actual,
|
||||||
|
.size = size,
|
||||||
|
.type = type,
|
||||||
|
.context = context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by biggest sizes first. This is important for the lookup
|
||||||
|
// algorithm. Mess with this, change that.
|
||||||
|
std::sort(m_directories.begin(), m_directories.end(),
|
||||||
|
[](DirectoryEntry const &a, DirectoryEntry const &b) {
|
||||||
|
return a.size > b.size;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconRegistry::IconRegistry()
|
||||||
|
: m_preferred_theme(get_current_icon_theme())
|
||||||
|
{
|
||||||
|
std::vector<std::filesystem::path> theme_directory_paths;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto const *env { getenv("HOME") };
|
||||||
|
if (env && *env) {
|
||||||
|
theme_directory_paths.push_back(
|
||||||
|
std::filesystem::path(env) / ".icons");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto const *env { getenv("XDG_DATA_DIRS") };
|
||||||
|
if (env && *env) {
|
||||||
|
std::ranges::copy(std::string_view(env) | std::views::split(':')
|
||||||
|
| std::views::transform([](auto &&s) {
|
||||||
|
return std::filesystem::path(s.begin(), s.end())
|
||||||
|
/ "icons";
|
||||||
|
})
|
||||||
|
| std::views::filter([](auto &&p) {
|
||||||
|
if (!std::filesystem::is_directory(p))
|
||||||
|
return false;
|
||||||
|
if (std::filesystem::directory_iterator(p)
|
||||||
|
== std::filesystem::directory_iterator {})
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
std::back_inserter(theme_directory_paths));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::array<std::filesystem::path, 3> const paths {
|
||||||
|
"/usr/share/pixmaps",
|
||||||
|
"/usr/local/share/icons",
|
||||||
|
"/usr/share/icons",
|
||||||
|
};
|
||||||
|
std::copy_if(paths.begin(), paths.end(), theme_directory_paths.begin(),
|
||||||
|
[](auto const path) { return std::filesystem::exists(path); });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &&path : theme_directory_paths
|
||||||
|
| std::views::filter([](std::filesystem::path const &path) {
|
||||||
|
return std::filesystem::is_directory(path);
|
||||||
|
})) {
|
||||||
|
try {
|
||||||
|
m_themes.push_back(IconTheme(path));
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_themes.empty()) {
|
||||||
|
throw std::runtime_error("Could not find any icon themes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_preferred_theme) {
|
||||||
|
TraceLog(LOG_INFO,
|
||||||
|
std::format("Preferred theme: {}", *m_preferred_theme).c_str());
|
||||||
|
|
||||||
|
std::stable_partition(
|
||||||
|
m_themes.begin(), m_themes.end(), [&](auto const &t) {
|
||||||
|
return std::any_of(t.names().begin(), t.names().end(),
|
||||||
|
[&](auto const &e) { return e == *m_preferred_theme; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto IconRegistry::lookup(std::string_view const name,
|
||||||
|
std::optional<int> optimal_size, bool symbolic, std::optional<Color> color)
|
||||||
|
-> Icon const &
|
||||||
|
{
|
||||||
|
if (!color && m_color)
|
||||||
|
color = m_color;
|
||||||
|
|
||||||
|
std::string color_name {};
|
||||||
|
if (color) {
|
||||||
|
auto const col { color_to_string(*color) };
|
||||||
|
if (!col.empty()) {
|
||||||
|
color_name = "-" + col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (symbolic) {
|
||||||
|
try {
|
||||||
|
auto const n { std::format("{}{}-symbolic", color_name, name) };
|
||||||
|
return lookup_cached(n, optimal_size);
|
||||||
|
} catch (...) {
|
||||||
|
return lookup(name, optimal_size, false, color);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return lookup_cached(
|
||||||
|
std::string_view(std::format("{}{}", name, color_name)),
|
||||||
|
optimal_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto IconRegistry::lookup_cached(std::string_view const name,
|
||||||
|
std::optional<int> optimal_size) -> Icon const &
|
||||||
|
{
|
||||||
|
std::string name_s(name);
|
||||||
|
if (m_cached_icons.contains(name_s)) {
|
||||||
|
auto const &icon = m_cached_icons.at(name_s);
|
||||||
|
if (optimal_size && icon.size() >= *optimal_size)
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto const &theme : m_themes) {
|
||||||
|
try {
|
||||||
|
auto const icon = theme.lookup(name, optimal_size);
|
||||||
|
m_cached_icons.insert_or_assign(name_s, icon);
|
||||||
|
return m_cached_icons.at(name_s);
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::runtime_error(std::format("Failed to find icon `{}`!", name));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
93
src/IconRegistry.hpp
Normal file
93
src/IconRegistry.hpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <raylib.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
struct Icon {
|
||||||
|
Icon(std::filesystem::path path, Texture2D texture, int size)
|
||||||
|
: m_path(path)
|
||||||
|
, m_texture(texture)
|
||||||
|
, m_size(size)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto path() const -> std::filesystem::path const &
|
||||||
|
{
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
constexpr auto texture() const -> Texture2D const & { return m_texture; }
|
||||||
|
constexpr auto size() const -> int const & { return m_size; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path m_path;
|
||||||
|
Texture2D m_texture;
|
||||||
|
int m_size { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IconTheme {
|
||||||
|
explicit IconTheme(std::filesystem::path const &themes_directory_path);
|
||||||
|
~IconTheme() = default;
|
||||||
|
|
||||||
|
constexpr auto inherits() const -> std::span<std::string const>
|
||||||
|
{
|
||||||
|
return std::span { m_inherits };
|
||||||
|
}
|
||||||
|
auto lookup(std::string_view const name,
|
||||||
|
std::optional<int> optimal_size = std::nullopt) const -> Icon const;
|
||||||
|
auto names() const -> std::vector<std::string> const & { return m_names; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct DirectoryEntry {
|
||||||
|
enum class Type {
|
||||||
|
Fixed,
|
||||||
|
Scalable,
|
||||||
|
Threshold,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Context {
|
||||||
|
Actions,
|
||||||
|
Devices,
|
||||||
|
FileSystems,
|
||||||
|
MimeTypes,
|
||||||
|
Places,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path path;
|
||||||
|
int size;
|
||||||
|
Type type;
|
||||||
|
Context context;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::string> m_names;
|
||||||
|
std::vector<std::string> m_inherits;
|
||||||
|
std::vector<DirectoryEntry> m_directories;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IconRegistry {
|
||||||
|
IconRegistry();
|
||||||
|
~IconRegistry() = default;
|
||||||
|
|
||||||
|
auto lookup(std::string_view const name,
|
||||||
|
std::optional<int> optimal_size = std::nullopt, bool symbolic = false,
|
||||||
|
std::optional<Color> color = std::nullopt) -> Icon const &;
|
||||||
|
auto color(std::optional<Color> const &color) { m_color = color; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::optional<Color> m_color { std::nullopt };
|
||||||
|
|
||||||
|
auto lookup_cached(std::string_view const name,
|
||||||
|
std::optional<int> optimal_size) -> Icon const &;
|
||||||
|
|
||||||
|
std::vector<IconTheme> m_themes;
|
||||||
|
std::unordered_map<std::string, Icon> m_cached_icons;
|
||||||
|
std::optional<std::string> m_preferred_theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
792
src/ImGui.cpp
Normal file
792
src/ImGui.cpp
Normal file
@@ -0,0 +1,792 @@
|
|||||||
|
#include "ImGui.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <format>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <raylib.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct CodepointSpan {
|
||||||
|
u32 codepoint {};
|
||||||
|
usize start {};
|
||||||
|
usize end {};
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr inline float px_pos(float x) { return std::floor(x + 0.5f); }
|
||||||
|
constexpr inline float px_w(float w) { return std::ceil(w); }
|
||||||
|
|
||||||
|
constexpr auto utf8_rune_from_first(char const *s) -> u32
|
||||||
|
{
|
||||||
|
u8 b0 = static_cast<u8>(s[0]);
|
||||||
|
if (b0 < 0x80)
|
||||||
|
return b0;
|
||||||
|
|
||||||
|
if ((b0 & 0xE0) == 0xC0)
|
||||||
|
return ((b0 & 0x1F) << 6) | (static_cast<u8>(s[1]) & 0x3F);
|
||||||
|
|
||||||
|
if ((b0 & 0xF0) == 0xE0)
|
||||||
|
return ((b0 & 0x0F) << 12) | ((static_cast<u8>(s[1]) & 0x3F) << 6)
|
||||||
|
| (static_cast<u8>(s[2]) & 0x3F);
|
||||||
|
|
||||||
|
if ((b0 & 0xF8) == 0xF0)
|
||||||
|
return ((b0 & 0x07) << 18) | ((static_cast<u8>(s[1]) & 0x3F) << 12)
|
||||||
|
| ((static_cast<u8>(s[2]) & 0x3F) << 6)
|
||||||
|
| (static_cast<u8>(s[3]) & 0x3F);
|
||||||
|
|
||||||
|
return 0xFFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto decode_utf8(std::string_view text) -> std::vector<CodepointSpan>
|
||||||
|
{
|
||||||
|
std::vector<CodepointSpan> spans;
|
||||||
|
usize i = 0;
|
||||||
|
spans.reserve(text.size());
|
||||||
|
|
||||||
|
while (i < text.size()) {
|
||||||
|
u8 b = static_cast<u8>(text[i]);
|
||||||
|
usize len = 1;
|
||||||
|
|
||||||
|
if (b < 0x80)
|
||||||
|
len = 1;
|
||||||
|
else if ((b & 0xE0) == 0xC0)
|
||||||
|
len = 2;
|
||||||
|
else if ((b & 0xF0) == 0xE0)
|
||||||
|
len = 3;
|
||||||
|
else if ((b & 0xF8) == 0xF0)
|
||||||
|
len = 4;
|
||||||
|
|
||||||
|
if (i + len > text.size())
|
||||||
|
len = 1;
|
||||||
|
|
||||||
|
u32 cp = utf8_rune_from_first(text.data() + i);
|
||||||
|
spans.push_back({ cp, i, i + len });
|
||||||
|
i += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto encode_utf8(u32 cp) -> std::string
|
||||||
|
{
|
||||||
|
char buf[5] = { 0, 0, 0, 0, 0 };
|
||||||
|
int len = 0;
|
||||||
|
if (cp <= 0x7F) {
|
||||||
|
buf[len++] = static_cast<char>(cp);
|
||||||
|
} else if (cp <= 0x7FF) {
|
||||||
|
buf[len++] = static_cast<char>(0xC0 | (cp >> 6));
|
||||||
|
buf[len++] = static_cast<char>(0x80 | (cp & 0x3F));
|
||||||
|
} else if (cp <= 0xFFFF) {
|
||||||
|
if (cp >= 0xD800 && cp <= 0xDFFF)
|
||||||
|
return {};
|
||||||
|
buf[len++] = static_cast<char>(0xE0 | (cp >> 12));
|
||||||
|
buf[len++] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
buf[len++] = static_cast<char>(0x80 | (cp & 0x3F));
|
||||||
|
} else if (cp <= 0x10FFFF) {
|
||||||
|
buf[len++] = static_cast<char>(0xF0 | (cp >> 18));
|
||||||
|
buf[len++] = static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||||
|
buf[len++] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
buf[len++] = static_cast<char>(0x80 | (cp & 0x3F));
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return std::string(buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto rune_index_for_byte(std::string_view text, usize byte_offset) -> int
|
||||||
|
{
|
||||||
|
auto spans { decode_utf8(text) };
|
||||||
|
int idx = 0;
|
||||||
|
for (auto const &span : spans) {
|
||||||
|
if (span.start >= byte_offset)
|
||||||
|
break;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
if (byte_offset >= text.size())
|
||||||
|
idx = static_cast<int>(spans.size());
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto clamp_preedit_index(int value, usize text_size) -> usize
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
return 0;
|
||||||
|
auto const as_size { static_cast<usize>(value) };
|
||||||
|
return std::min(as_size, text_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr float HORIZONTAL_PADDING = 6.0f;
|
||||||
|
constexpr float VERTICAL_PADDING = 4.0f;
|
||||||
|
constexpr double CARET_BLINK_INTERVAL = 0.5;
|
||||||
|
constexpr float CARET_DESCENT_FRACTION = 0.25f;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ImGui::ImGui(std::shared_ptr<TextRenderer> text_renderer)
|
||||||
|
: m_text_renderer(text_renderer)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui::begin(u32 const rune, bool ctrl, bool shift,
|
||||||
|
std::string_view const clipboard,
|
||||||
|
std::function<void(std::string_view const &)> clipboard_set)
|
||||||
|
{
|
||||||
|
m_rune = rune;
|
||||||
|
m_ctrl = ctrl;
|
||||||
|
m_shift = shift;
|
||||||
|
m_clipboard = clipboard;
|
||||||
|
m_clipboard_set = clipboard_set;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui::end()
|
||||||
|
{
|
||||||
|
m_rune = false;
|
||||||
|
m_ctrl = false;
|
||||||
|
m_shift = false;
|
||||||
|
m_clipboard = {};
|
||||||
|
m_clipboard_set = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui::set_font(FontHandle font) { m_font = font; }
|
||||||
|
|
||||||
|
auto ImGui::focused_text_input() const -> std::optional<usize>
|
||||||
|
{
|
||||||
|
if (m_focused_id == 0)
|
||||||
|
return std::nullopt;
|
||||||
|
return m_focused_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ImGui::text_input_surrounding(usize id, std::pmr::string const &str) const
|
||||||
|
-> std::optional<TextInputSurrounding>
|
||||||
|
{
|
||||||
|
auto it { m_ti_states.find(id) };
|
||||||
|
if (it == m_ti_states.end())
|
||||||
|
return std::nullopt;
|
||||||
|
TextInputSurrounding info;
|
||||||
|
info.text.assign(str.data(), str.size());
|
||||||
|
info.caret_byte = std::min(it->second.caret_byte, str.size());
|
||||||
|
info.cursor = static_cast<int>(info.caret_byte);
|
||||||
|
info.anchor = static_cast<int>(info.caret_byte);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ImGui::text_input_cursor(usize id) const -> std::optional<TextInputCursor>
|
||||||
|
{
|
||||||
|
auto it { m_ti_states.find(id) };
|
||||||
|
if (it == m_ti_states.end())
|
||||||
|
return std::nullopt;
|
||||||
|
TextInputCursor cursor;
|
||||||
|
cursor.rect = it->second.caret_rect;
|
||||||
|
cursor.visible
|
||||||
|
= it->second.caret_visible && !it->second.preedit_cursor_hidden;
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui::ime_commit_text(std::pmr::string &str, std::string_view text)
|
||||||
|
{
|
||||||
|
if (m_focused_id == 0)
|
||||||
|
return;
|
||||||
|
auto it { m_ti_states.find(m_focused_id) };
|
||||||
|
if (it == m_ti_states.end())
|
||||||
|
return;
|
||||||
|
auto &state { it->second };
|
||||||
|
usize insert_pos = std::min(state.caret_byte, str.size());
|
||||||
|
if (!text.empty())
|
||||||
|
str.insert(insert_pos, text);
|
||||||
|
state.caret_byte = insert_pos + text.size();
|
||||||
|
std::string_view const view(str.data(), str.size());
|
||||||
|
state.current_rune_idx = rune_index_for_byte(view, state.caret_byte);
|
||||||
|
state.caret_timer = 0.0;
|
||||||
|
state.caret_visible = true;
|
||||||
|
state.external_change = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui::ime_delete_surrounding(
|
||||||
|
std::pmr::string &str, usize before, usize after)
|
||||||
|
{
|
||||||
|
if (m_focused_id == 0)
|
||||||
|
return;
|
||||||
|
auto it { m_ti_states.find(m_focused_id) };
|
||||||
|
if (it == m_ti_states.end())
|
||||||
|
return;
|
||||||
|
auto &state { it->second };
|
||||||
|
usize caret_byte { std::min(state.caret_byte, str.size()) };
|
||||||
|
usize start { before > caret_byte ? 0 : caret_byte - before };
|
||||||
|
usize end { std::min(caret_byte + after, str.size()) };
|
||||||
|
if (end > start) {
|
||||||
|
str.erase(start, end - start);
|
||||||
|
state.caret_byte = start;
|
||||||
|
std::string_view const view(str.data(), str.size());
|
||||||
|
state.current_rune_idx = rune_index_for_byte(view, state.caret_byte);
|
||||||
|
state.caret_timer = 0.0;
|
||||||
|
state.caret_visible = true;
|
||||||
|
state.external_change = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui::ime_set_preedit(std::string text, int cursor_begin, int cursor_end)
|
||||||
|
{
|
||||||
|
if (m_focused_id == 0)
|
||||||
|
return;
|
||||||
|
auto it { m_ti_states.find(m_focused_id) };
|
||||||
|
if (it == m_ti_states.end())
|
||||||
|
return;
|
||||||
|
auto &state { it->second };
|
||||||
|
state.preedit_text = std::move(text);
|
||||||
|
state.preedit_cursor_hidden = (cursor_begin == -1 && cursor_end == -1);
|
||||||
|
usize const size = state.preedit_text.size();
|
||||||
|
if (state.preedit_cursor_hidden) {
|
||||||
|
state.preedit_cursor_begin = 0;
|
||||||
|
state.preedit_cursor_end = 0;
|
||||||
|
} else {
|
||||||
|
auto begin_clamped { clamp_preedit_index(cursor_begin, size) };
|
||||||
|
auto end_clamped { clamp_preedit_index(cursor_end, size) };
|
||||||
|
state.preedit_cursor_begin = static_cast<int>(begin_clamped);
|
||||||
|
state.preedit_cursor_end = static_cast<int>(end_clamped);
|
||||||
|
}
|
||||||
|
state.preedit_active
|
||||||
|
= !state.preedit_text.empty() || !state.preedit_cursor_hidden;
|
||||||
|
if (state.preedit_active) {
|
||||||
|
state.caret_timer = 0.0;
|
||||||
|
state.caret_visible = !state.preedit_cursor_hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui::ime_clear_preedit()
|
||||||
|
{
|
||||||
|
if (m_focused_id == 0)
|
||||||
|
return;
|
||||||
|
auto it { m_ti_states.find(m_focused_id) };
|
||||||
|
if (it == m_ti_states.end())
|
||||||
|
return;
|
||||||
|
auto &state { it->second };
|
||||||
|
state.preedit_text.clear();
|
||||||
|
state.preedit_cursor_begin = 0;
|
||||||
|
state.preedit_cursor_end = 0;
|
||||||
|
state.preedit_active = false;
|
||||||
|
state.preedit_cursor_hidden = false;
|
||||||
|
state.caret_visible = true;
|
||||||
|
state.caret_timer = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t utf8_length(std::string_view const &s)
|
||||||
|
{
|
||||||
|
size_t count = std::count_if(
|
||||||
|
s.begin(), s.end(), [](auto const &c) { return (c & 0xC0) != 0x80; });
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec,
|
||||||
|
TextInputOptions options) -> std::bitset<2>
|
||||||
|
{
|
||||||
|
assert(id != 0);
|
||||||
|
assert(
|
||||||
|
m_font.has_value() && "ImGui font must be set before using text input");
|
||||||
|
|
||||||
|
bool submitted { false };
|
||||||
|
bool changed { false };
|
||||||
|
|
||||||
|
auto &state { m_ti_states[id] };
|
||||||
|
assert(!options.multiline && "Multiline not yet implemented.");
|
||||||
|
|
||||||
|
if (m_focused_id == 0)
|
||||||
|
m_focused_id = id;
|
||||||
|
|
||||||
|
if (style().font_size > rec.height) {
|
||||||
|
TraceLog(LOG_WARNING,
|
||||||
|
std::format("Text size for text input {} is bigger than height ({} "
|
||||||
|
"> {}). Clipping will occur.",
|
||||||
|
id, style().font_size, rec.height)
|
||||||
|
.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view str_view(str.data(), str.size());
|
||||||
|
auto spans { decode_utf8(str_view) };
|
||||||
|
|
||||||
|
auto is_space = [](u32 cp) -> bool {
|
||||||
|
if (cp == '\n' || cp == '\r' || cp == '\t' || cp == '\v' || cp == '\f')
|
||||||
|
return true;
|
||||||
|
if (cp <= 0x7F)
|
||||||
|
return std::isspace(static_cast<unsigned char>(cp)) != 0;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto clamp_cursor = [&]() -> usize {
|
||||||
|
int const max_idx = static_cast<int>(spans.size());
|
||||||
|
state.current_rune_idx = std::clamp(state.current_rune_idx, 0, max_idx);
|
||||||
|
if (state.current_rune_idx == max_idx)
|
||||||
|
return str.size();
|
||||||
|
return spans[(usize)state.current_rune_idx].start;
|
||||||
|
};
|
||||||
|
|
||||||
|
usize caret_byte = clamp_cursor();
|
||||||
|
|
||||||
|
auto refresh_spans = [&]() {
|
||||||
|
str_view = std::string_view(str.data(), str.size());
|
||||||
|
spans = decode_utf8(str_view);
|
||||||
|
caret_byte = clamp_cursor();
|
||||||
|
};
|
||||||
|
|
||||||
|
auto erase_range = [&](usize byte_begin, usize byte_end) {
|
||||||
|
if (byte_end > byte_begin && byte_begin < str.size()) {
|
||||||
|
str.erase(byte_begin, byte_end - byte_begin);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto selection_range_bytes
|
||||||
|
= [&](int a_idx, int b_idx) -> std::pair<usize, usize> {
|
||||||
|
int lo = std::max(0, std::min(a_idx, b_idx));
|
||||||
|
int hi = std::max(0, std::max(a_idx, b_idx));
|
||||||
|
usize byte_begin
|
||||||
|
= (lo >= (int)spans.size()) ? str.size() : spans[(usize)lo].start;
|
||||||
|
usize byte_end
|
||||||
|
= (hi >= (int)spans.size()) ? str.size() : spans[(usize)hi].start;
|
||||||
|
return { byte_begin, byte_end };
|
||||||
|
};
|
||||||
|
|
||||||
|
auto erase_selection_if_any = [&]() -> bool {
|
||||||
|
if (!state.has_selection(state.current_rune_idx))
|
||||||
|
return false;
|
||||||
|
auto [b, e] = selection_range_bytes(
|
||||||
|
state.sel_anchor_idx, state.current_rune_idx);
|
||||||
|
if (e > b) {
|
||||||
|
str.erase(b, e - b);
|
||||||
|
changed = true;
|
||||||
|
refresh_spans();
|
||||||
|
state.current_rune_idx = rune_index_for_byte(str_view, b);
|
||||||
|
state.clear_selection();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto move_left_word = [&]() {
|
||||||
|
while (state.current_rune_idx > 0
|
||||||
|
&& is_space(spans[(usize)(state.current_rune_idx - 1)].codepoint))
|
||||||
|
state.current_rune_idx--;
|
||||||
|
while (state.current_rune_idx > 0
|
||||||
|
&& !is_space(spans[(usize)(state.current_rune_idx - 1)].codepoint))
|
||||||
|
state.current_rune_idx--;
|
||||||
|
};
|
||||||
|
auto move_right_word = [&]() {
|
||||||
|
while (state.current_rune_idx < (int)spans.size()
|
||||||
|
&& is_space(spans[(usize)state.current_rune_idx].codepoint))
|
||||||
|
state.current_rune_idx++;
|
||||||
|
while (state.current_rune_idx < (int)spans.size()
|
||||||
|
&& !is_space(spans[(usize)state.current_rune_idx].codepoint))
|
||||||
|
state.current_rune_idx++;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool caret_activity = false;
|
||||||
|
|
||||||
|
if (m_focused_id == id && m_rune != 0) {
|
||||||
|
bool request_refresh = false;
|
||||||
|
|
||||||
|
auto handle_backspace = [&]() {
|
||||||
|
if (state.current_rune_idx <= 0
|
||||||
|
|| state.current_rune_idx > (int)spans.size())
|
||||||
|
return;
|
||||||
|
if (m_ctrl) {
|
||||||
|
int idx = state.current_rune_idx, scan = idx - 1;
|
||||||
|
while (scan >= 0 && is_space(spans[(usize)scan].codepoint))
|
||||||
|
scan--;
|
||||||
|
while (scan >= 0 && !is_space(spans[(usize)scan].codepoint))
|
||||||
|
scan--;
|
||||||
|
int start_idx = std::max(scan + 1, 0);
|
||||||
|
usize b = spans[(usize)start_idx].start;
|
||||||
|
usize e = (idx >= (int)spans.size()) ? str.size()
|
||||||
|
: spans[(usize)idx].start;
|
||||||
|
erase_range(b, e);
|
||||||
|
state.current_rune_idx = start_idx;
|
||||||
|
} else {
|
||||||
|
auto const &prev = spans[(usize)(state.current_rune_idx - 1)];
|
||||||
|
erase_range(prev.start, prev.end);
|
||||||
|
state.current_rune_idx--;
|
||||||
|
}
|
||||||
|
request_refresh = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto handle_delete = [&]() {
|
||||||
|
if (state.current_rune_idx < 0
|
||||||
|
|| state.current_rune_idx >= (int)spans.size()) {
|
||||||
|
if (!m_ctrl)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int idx = state.current_rune_idx;
|
||||||
|
if (m_ctrl) {
|
||||||
|
int scan = idx;
|
||||||
|
while (scan < (int)spans.size()
|
||||||
|
&& is_space(spans[(usize)scan].codepoint))
|
||||||
|
scan++;
|
||||||
|
while (scan < (int)spans.size()
|
||||||
|
&& !is_space(spans[(usize)scan].codepoint))
|
||||||
|
scan++;
|
||||||
|
usize b = (idx < (int)spans.size()) ? spans[(usize)idx].start
|
||||||
|
: str.size();
|
||||||
|
usize e = (scan < (int)spans.size()) ? spans[(usize)scan].start
|
||||||
|
: str.size();
|
||||||
|
erase_range(b, e);
|
||||||
|
} else if (idx < (int)spans.size()) {
|
||||||
|
auto const &curr = spans[(usize)idx];
|
||||||
|
erase_range(curr.start, curr.end);
|
||||||
|
}
|
||||||
|
request_refresh = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool extend = m_shift;
|
||||||
|
|
||||||
|
switch (m_rune) {
|
||||||
|
case 1: // Left
|
||||||
|
if (!extend)
|
||||||
|
state.clear_selection();
|
||||||
|
if (state.current_rune_idx > 0) {
|
||||||
|
if (extend && state.sel_anchor_idx == -1)
|
||||||
|
state.sel_anchor_idx = state.current_rune_idx;
|
||||||
|
if (m_ctrl)
|
||||||
|
move_left_word();
|
||||||
|
else
|
||||||
|
state.current_rune_idx--;
|
||||||
|
caret_byte = clamp_cursor();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4: // Right
|
||||||
|
if (!extend)
|
||||||
|
state.clear_selection();
|
||||||
|
if (state.current_rune_idx < (int)spans.size()) {
|
||||||
|
if (extend && state.sel_anchor_idx == -1)
|
||||||
|
state.sel_anchor_idx = state.current_rune_idx;
|
||||||
|
if (m_ctrl)
|
||||||
|
move_right_word();
|
||||||
|
else
|
||||||
|
state.current_rune_idx++;
|
||||||
|
caret_byte = clamp_cursor();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3: // Up -> home
|
||||||
|
if (!extend)
|
||||||
|
state.clear_selection();
|
||||||
|
if (extend && state.sel_anchor_idx == -1)
|
||||||
|
state.sel_anchor_idx = state.current_rune_idx;
|
||||||
|
state.current_rune_idx = 0;
|
||||||
|
caret_byte = clamp_cursor();
|
||||||
|
break;
|
||||||
|
case 2: // Down -> end
|
||||||
|
if (!extend)
|
||||||
|
state.clear_selection();
|
||||||
|
if (extend && state.sel_anchor_idx == -1)
|
||||||
|
state.sel_anchor_idx = state.current_rune_idx;
|
||||||
|
state.current_rune_idx = (int)spans.size();
|
||||||
|
caret_byte = clamp_cursor();
|
||||||
|
break;
|
||||||
|
case 8: // Backspace
|
||||||
|
if (erase_selection_if_any()) {
|
||||||
|
request_refresh = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
handle_backspace();
|
||||||
|
break;
|
||||||
|
case 0x7F: // Delete
|
||||||
|
if (erase_selection_if_any()) {
|
||||||
|
request_refresh = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
handle_delete();
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
if (m_ctrl) {
|
||||||
|
state.sel_anchor_idx = 0;
|
||||||
|
state.current_rune_idx = (int)spans.size();
|
||||||
|
request_refresh = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
[[fallthrough]];
|
||||||
|
case 'c':
|
||||||
|
if (m_ctrl) {
|
||||||
|
if (state.has_selection(state.current_rune_idx)
|
||||||
|
&& m_clipboard_set) {
|
||||||
|
auto [b, e] = selection_range_bytes(
|
||||||
|
state.sel_anchor_idx, state.current_rune_idx);
|
||||||
|
m_clipboard_set(std::string_view(str.data() + b, e - b));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
[[fallthrough]];
|
||||||
|
case 'x':
|
||||||
|
if (m_ctrl) {
|
||||||
|
if (state.has_selection(state.current_rune_idx)
|
||||||
|
&& m_clipboard_set) {
|
||||||
|
auto [b, e] = selection_range_bytes(
|
||||||
|
state.sel_anchor_idx, state.current_rune_idx);
|
||||||
|
m_clipboard_set(std::string_view(str.data() + b, e - b));
|
||||||
|
str.erase(b, e - b);
|
||||||
|
changed = true;
|
||||||
|
request_refresh = true;
|
||||||
|
refresh_spans();
|
||||||
|
state.current_rune_idx = rune_index_for_byte(str_view, b);
|
||||||
|
state.clear_selection();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
[[fallthrough]];
|
||||||
|
case 'v':
|
||||||
|
if (m_ctrl && !m_clipboard.empty()) {
|
||||||
|
erase_selection_if_any();
|
||||||
|
if (!options.multiline) {
|
||||||
|
std::string clip2;
|
||||||
|
clip2.reserve(m_clipboard.size());
|
||||||
|
std::copy_if(m_clipboard.begin(), m_clipboard.end(),
|
||||||
|
clip2.begin(),
|
||||||
|
[](char ch) { return ch != '\n' && ch != '\r'; });
|
||||||
|
str.insert(caret_byte, clip2);
|
||||||
|
state.current_rune_idx += (int)utf8_length(clip2);
|
||||||
|
} else {
|
||||||
|
str.insert(caret_byte, m_clipboard);
|
||||||
|
state.current_rune_idx += (int)utf8_length(m_clipboard);
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
request_refresh = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
goto insert_printable;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
case '\n':
|
||||||
|
if (options.multiline) {
|
||||||
|
erase_selection_if_any();
|
||||||
|
auto encoded { encode_utf8('\n') };
|
||||||
|
if (!encoded.empty()) {
|
||||||
|
str.insert(caret_byte, encoded);
|
||||||
|
state.current_rune_idx++;
|
||||||
|
changed = true;
|
||||||
|
request_refresh = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
submitted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
insert_printable:
|
||||||
|
if (m_rune >= 0x20) {
|
||||||
|
erase_selection_if_any();
|
||||||
|
auto encoded { encode_utf8(m_rune) };
|
||||||
|
if (!encoded.empty()) {
|
||||||
|
str.insert(caret_byte, encoded);
|
||||||
|
state.current_rune_idx++;
|
||||||
|
changed = true;
|
||||||
|
request_refresh = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request_refresh)
|
||||||
|
refresh_spans();
|
||||||
|
else
|
||||||
|
caret_byte = clamp_cursor();
|
||||||
|
caret_activity = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.caret_byte = caret_byte;
|
||||||
|
|
||||||
|
double const dt = (double)GetFrameTime();
|
||||||
|
if (m_focused_id == id) {
|
||||||
|
if (state.preedit_active && state.preedit_cursor_hidden) {
|
||||||
|
state.caret_visible = false;
|
||||||
|
state.caret_timer = 0.0;
|
||||||
|
} else if (caret_activity) {
|
||||||
|
state.caret_timer = 0.0;
|
||||||
|
state.caret_visible = true;
|
||||||
|
} else {
|
||||||
|
if (state.caret_timer == 0.0)
|
||||||
|
state.caret_visible = true;
|
||||||
|
state.caret_timer += dt;
|
||||||
|
if (state.caret_timer >= CARET_BLINK_INTERVAL) {
|
||||||
|
int toggles = (int)(state.caret_timer / CARET_BLINK_INTERVAL);
|
||||||
|
state.caret_timer
|
||||||
|
= std::fmod(state.caret_timer, CARET_BLINK_INTERVAL);
|
||||||
|
if (toggles % 2 == 1)
|
||||||
|
state.caret_visible = !state.caret_visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.caret_visible = false;
|
||||||
|
state.caret_timer = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color const bg_col { 16, 16, 16, 100 };
|
||||||
|
Color const border_col { 220, 220, 220, 180 };
|
||||||
|
DrawRectangleRec(rec, bg_col);
|
||||||
|
DrawRectangleLinesEx(rec, 1.0f, border_col);
|
||||||
|
|
||||||
|
float const text_top = rec.y + VERTICAL_PADDING;
|
||||||
|
float const baseline_y = text_top + style().font_size;
|
||||||
|
float const max_caret_h
|
||||||
|
= std::max(0.0f, rec.height - 2.0f * VERTICAL_PADDING);
|
||||||
|
float caret_height = style().font_size * (1.0f + CARET_DESCENT_FRACTION);
|
||||||
|
caret_height = std::min(
|
||||||
|
caret_height, max_caret_h > 0.0f ? max_caret_h : caret_height);
|
||||||
|
if (caret_height <= 0.0f)
|
||||||
|
caret_height = style().font_size;
|
||||||
|
float caret_top = baseline_y - style().font_size;
|
||||||
|
float const min_top = rec.y + VERTICAL_PADDING;
|
||||||
|
float const max_top = rec.y + rec.height - VERTICAL_PADDING - caret_height;
|
||||||
|
caret_top = std::clamp(caret_top, min_top, max_top);
|
||||||
|
float caret_bottom = caret_top + caret_height;
|
||||||
|
float const desired_bottom
|
||||||
|
= baseline_y + style().font_size * CARET_DESCENT_FRACTION;
|
||||||
|
if (caret_bottom < desired_bottom) {
|
||||||
|
float const adjust = desired_bottom - caret_bottom;
|
||||||
|
caret_top = std::min(caret_top + adjust, max_top);
|
||||||
|
caret_bottom = caret_top + caret_height;
|
||||||
|
}
|
||||||
|
state.cursor_position.y = caret_top;
|
||||||
|
|
||||||
|
float const available_width
|
||||||
|
= std::max(0.0f, rec.width - 2.0f * HORIZONTAL_PADDING);
|
||||||
|
float const base_y = px_pos(baseline_y);
|
||||||
|
|
||||||
|
int const font_px = (int)style().font_size;
|
||||||
|
|
||||||
|
Vector2 prefix_metrics { 0.0f, 0.0f };
|
||||||
|
{
|
||||||
|
std::string_view prefix(str.data(), caret_byte);
|
||||||
|
if (m_text_renderer)
|
||||||
|
prefix_metrics
|
||||||
|
= m_text_renderer->measure_text(*m_font, prefix, font_px);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 caret_preedit_metrics { 0.0f, 0.0f };
|
||||||
|
bool const has_preedit = state.preedit_active
|
||||||
|
&& (!state.preedit_text.empty() || !state.preedit_cursor_hidden);
|
||||||
|
if (has_preedit && m_text_renderer) {
|
||||||
|
auto pe_end = clamp_preedit_index(
|
||||||
|
state.preedit_cursor_end, state.preedit_text.size());
|
||||||
|
caret_preedit_metrics = m_text_renderer->measure_text(*m_font,
|
||||||
|
std::string_view(state.preedit_text.data(), (usize)pe_end),
|
||||||
|
font_px);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 full_metrics { 0.0f, 0.0f };
|
||||||
|
{
|
||||||
|
std::string display;
|
||||||
|
display.reserve(
|
||||||
|
str.size() + (has_preedit ? state.preedit_text.size() : 0));
|
||||||
|
display.append(std::string_view(str.data(), caret_byte));
|
||||||
|
if (has_preedit)
|
||||||
|
display.append(state.preedit_text);
|
||||||
|
display.append(
|
||||||
|
std::string_view(str.data() + caret_byte, str.size() - caret_byte));
|
||||||
|
if (m_text_renderer)
|
||||||
|
full_metrics = m_text_renderer->measure_text(*m_font,
|
||||||
|
std::string_view(display.data(), display.size()), font_px);
|
||||||
|
}
|
||||||
|
|
||||||
|
float caret_offset = prefix_metrics.x + caret_preedit_metrics.x;
|
||||||
|
state.cursor_position.x = caret_offset;
|
||||||
|
|
||||||
|
if (full_metrics.x <= available_width) {
|
||||||
|
state.scroll_offset.x = 0.0f;
|
||||||
|
} else {
|
||||||
|
float &scroll = state.scroll_offset.x;
|
||||||
|
float caret_local = caret_offset - scroll;
|
||||||
|
float const pad = 8.0f;
|
||||||
|
if (caret_local > available_width - pad)
|
||||||
|
scroll = caret_offset - (available_width - pad);
|
||||||
|
else if (caret_local < pad)
|
||||||
|
scroll = caret_offset - pad;
|
||||||
|
scroll = std::clamp(
|
||||||
|
scroll, 0.0f, std::max(0.0f, full_metrics.x - available_width));
|
||||||
|
}
|
||||||
|
state.scroll_offset.y = 0.0f;
|
||||||
|
|
||||||
|
float const origin = rec.x + HORIZONTAL_PADDING - state.scroll_offset.x;
|
||||||
|
|
||||||
|
BeginScissorMode(rec.x, rec.y, rec.width, rec.height);
|
||||||
|
{
|
||||||
|
if (m_font.has_value() && m_text_renderer) {
|
||||||
|
Color const &text_color = style().text_color;
|
||||||
|
|
||||||
|
std::string display;
|
||||||
|
display.reserve(
|
||||||
|
str.size() + (has_preedit ? state.preedit_text.size() : 0));
|
||||||
|
display.append(std::string_view(str.data(), caret_byte));
|
||||||
|
if (has_preedit)
|
||||||
|
display.append(state.preedit_text);
|
||||||
|
display.append(std::string_view(
|
||||||
|
str.data() + caret_byte, str.size() - caret_byte));
|
||||||
|
|
||||||
|
m_text_renderer->draw_text(*m_font,
|
||||||
|
std::string_view(display.data(), display.size()),
|
||||||
|
{ origin, base_y }, font_px, text_color);
|
||||||
|
|
||||||
|
if (state.has_selection(state.current_rune_idx)) {
|
||||||
|
auto [sb, se] = selection_range_bytes(
|
||||||
|
state.sel_anchor_idx, state.current_rune_idx);
|
||||||
|
|
||||||
|
Vector2 sel_prefix = m_text_renderer->measure_text(
|
||||||
|
*m_font, std::string_view(str.data(), sb), font_px);
|
||||||
|
Vector2 sel_width = m_text_renderer->measure_text(*m_font,
|
||||||
|
std::string_view(str.data() + sb, se - sb), font_px);
|
||||||
|
|
||||||
|
Rectangle sel_rect { std::floor(origin + sel_prefix.x + 0.5f),
|
||||||
|
std::floor(caret_top + 0.5f),
|
||||||
|
std::max(1.0f, sel_width.x) + 1,
|
||||||
|
std::max(1.0f, std::round(caret_height)) };
|
||||||
|
DrawRectangleRec(sel_rect, style().selection_color);
|
||||||
|
|
||||||
|
m_text_renderer->draw_text(*m_font,
|
||||||
|
std::string_view(str.data() + sb, se - sb),
|
||||||
|
{ origin + sel_prefix.x, base_y }, font_px,
|
||||||
|
style().selection_text_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_focused_id == id && state.caret_visible) {
|
||||||
|
float const caret_x = std::floor(origin + caret_offset + 0.5f);
|
||||||
|
Vector2 const p0 { caret_x, std::floor(caret_top + 0.5f) };
|
||||||
|
Vector2 const p1 { caret_x,
|
||||||
|
std::floor((caret_top + caret_height) + 0.5f) };
|
||||||
|
DrawLineV(p0, p1, text_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EndScissorMode();
|
||||||
|
|
||||||
|
if (state.external_change) {
|
||||||
|
changed = true;
|
||||||
|
state.external_change = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::bitset<2> { static_cast<unsigned long long>(
|
||||||
|
(submitted ? 1 : 0) | (changed ? 2 : 0)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ImGui::list_view(usize id, Rectangle bounds, usize elements,
|
||||||
|
std::function<Vector2(usize i)> draw_cb, ListViewOptions options) -> bool
|
||||||
|
{
|
||||||
|
auto const &state { m_lv_states[id] };
|
||||||
|
|
||||||
|
bool submitted { false };
|
||||||
|
|
||||||
|
m_next_lv_next = false;
|
||||||
|
m_next_lv_previous = false;
|
||||||
|
m_next_lv_clear = false;
|
||||||
|
|
||||||
|
BeginScissorMode(bounds.x, bounds.y, bounds.width, bounds.height);
|
||||||
|
|
||||||
|
EndScissorMode();
|
||||||
|
|
||||||
|
m_prev_lv_selected_item = state.selected_item;
|
||||||
|
|
||||||
|
return submitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
164
src/ImGui.hpp
Normal file
164
src/ImGui.hpp
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <bitset>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <queue>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <raylib.h>
|
||||||
|
|
||||||
|
#include "TextRenderer.hpp"
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
constexpr float DEFAULT_FONT_SIZE { 24 };
|
||||||
|
|
||||||
|
struct TextInputOptions {
|
||||||
|
bool multiline { false };
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ListViewOptions {
|
||||||
|
bool selectable { false };
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ImGui {
|
||||||
|
struct Style {
|
||||||
|
float font_size { DEFAULT_FONT_SIZE };
|
||||||
|
Color text_color { BLACK };
|
||||||
|
Color preedit_color { BLACK };
|
||||||
|
Color selection_color { 127, 127, 255, 255 };
|
||||||
|
Color selection_text_color { WHITE };
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit ImGui(std::shared_ptr<TextRenderer> text_renderer);
|
||||||
|
|
||||||
|
ImGui(ImGui const &) = delete;
|
||||||
|
auto operator=(ImGui const &) -> ImGui & = delete;
|
||||||
|
ImGui(ImGui &&) = default;
|
||||||
|
auto operator=(ImGui &&) -> ImGui & = default;
|
||||||
|
|
||||||
|
void begin(u32 const rune, bool ctrl, bool shift,
|
||||||
|
std::string_view const clipboard,
|
||||||
|
std::function<void(std::string_view const &)> clipboard_set);
|
||||||
|
void end();
|
||||||
|
|
||||||
|
// Bit 0 -> Submitted
|
||||||
|
// Bit 1 -> String changed
|
||||||
|
auto text_input(usize id, std::pmr::string &str, Rectangle rec,
|
||||||
|
TextInputOptions options = {}) -> std::bitset<2>;
|
||||||
|
|
||||||
|
struct TextInputSurrounding {
|
||||||
|
std::string text;
|
||||||
|
int cursor { 0 };
|
||||||
|
int anchor { 0 };
|
||||||
|
usize caret_byte { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TextInputCursor {
|
||||||
|
Rectangle rect {};
|
||||||
|
bool visible { true };
|
||||||
|
};
|
||||||
|
|
||||||
|
auto focused_text_input() const -> std::optional<usize>;
|
||||||
|
auto text_input_surrounding(usize id, std::pmr::string const &str) const
|
||||||
|
-> std::optional<TextInputSurrounding>;
|
||||||
|
auto text_input_cursor(usize id) const -> std::optional<TextInputCursor>;
|
||||||
|
void ime_commit_text(std::pmr::string &str, std::string_view text);
|
||||||
|
void ime_delete_surrounding(
|
||||||
|
std::pmr::string &str, usize before, usize after);
|
||||||
|
void ime_set_preedit(std::string text, int cursor_begin, int cursor_end);
|
||||||
|
void ime_clear_preedit();
|
||||||
|
|
||||||
|
auto list_view(usize id, Rectangle bounds, usize elements,
|
||||||
|
std::function<Vector2(usize i)> draw_cb, ListViewOptions options = {})
|
||||||
|
-> bool;
|
||||||
|
auto prev_list_view_selected_item() const -> std::optional<usize>
|
||||||
|
{
|
||||||
|
return m_prev_lv_selected_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_font(FontHandle font);
|
||||||
|
|
||||||
|
auto style() -> Style & { return m_styles.back(); }
|
||||||
|
auto push_style(Style const &style) -> Style &
|
||||||
|
{
|
||||||
|
m_styles.push(style);
|
||||||
|
return m_styles.back();
|
||||||
|
}
|
||||||
|
auto push_style() -> Style & { return push_style(style()); }
|
||||||
|
|
||||||
|
[[nodiscard]] inline auto id(std::string_view const str) -> usize
|
||||||
|
{
|
||||||
|
std::hash<std::string_view> hasher;
|
||||||
|
return hasher(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct TextInputState {
|
||||||
|
int current_rune_idx { 0 };
|
||||||
|
|
||||||
|
// y not used if multiline == false
|
||||||
|
Vector2 scroll_offset { 0, 0 };
|
||||||
|
Vector2 cursor_position { 0, 0 };
|
||||||
|
|
||||||
|
bool caret_visible { true };
|
||||||
|
double caret_timer { 0.0 };
|
||||||
|
std::string preedit_text;
|
||||||
|
int preedit_cursor_begin { 0 };
|
||||||
|
int preedit_cursor_end { 0 };
|
||||||
|
bool preedit_active { false };
|
||||||
|
bool preedit_cursor_hidden { false };
|
||||||
|
usize caret_byte { 0 };
|
||||||
|
Rectangle caret_rect {};
|
||||||
|
bool external_change { false };
|
||||||
|
int sel_anchor_idx = -1;
|
||||||
|
|
||||||
|
bool has_selection(int curr_idx) const
|
||||||
|
{
|
||||||
|
return sel_anchor_idx != -1 && sel_anchor_idx != curr_idx;
|
||||||
|
}
|
||||||
|
void clear_selection() { sel_anchor_idx = -1; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ListViewState {
|
||||||
|
float scroll_offset_y { 0 };
|
||||||
|
std::optional<usize> selected_item { std::nullopt };
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<usize, ListViewState> m_lv_states;
|
||||||
|
std::unordered_map<usize, TextInputState> m_ti_states;
|
||||||
|
bool m_next_lv_next { false };
|
||||||
|
bool m_next_lv_previous { false };
|
||||||
|
bool m_next_lv_clear { false };
|
||||||
|
std::optional<usize> m_prev_lv_selected_item { std::nullopt };
|
||||||
|
usize m_focused_id {};
|
||||||
|
u32 m_rune {}; // 1234 <-> hjkl arrow keys
|
||||||
|
bool m_ctrl {};
|
||||||
|
bool m_shift {};
|
||||||
|
std::string_view m_clipboard {};
|
||||||
|
std::function<void(std::string_view const &)> m_clipboard_set;
|
||||||
|
|
||||||
|
std::queue<Style> m_styles { { Style {} } };
|
||||||
|
|
||||||
|
std::optional<FontHandle> m_font {};
|
||||||
|
std::shared_ptr<TextRenderer> m_text_renderer {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ImGuiGuard {
|
||||||
|
ImGuiGuard(std::shared_ptr<ImGui> imgui, u32 const rune, bool const ctrl,
|
||||||
|
bool const shift, std::string_view const clipboard,
|
||||||
|
std::function<void(std::string_view const &)> clipboard_set)
|
||||||
|
: m_imgui(imgui)
|
||||||
|
{
|
||||||
|
m_imgui->begin(rune, ctrl, shift, clipboard, clipboard_set);
|
||||||
|
}
|
||||||
|
~ImGuiGuard() { m_imgui->end(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<ImGui> m_imgui { nullptr };
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
241
src/InotifyWatcher.cpp
Normal file
241
src/InotifyWatcher.cpp
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#include "InotifyWatcher.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <system_error>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <sys/inotify.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr std::uint32_t watch_mask { IN_ATTRIB | IN_CREATE | IN_DELETE
|
||||||
|
| IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO
|
||||||
|
| IN_CLOSE_WRITE };
|
||||||
|
constexpr int poll_timeout_ms { 250 };
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
InotifyWatcher::InotifyWatcher()
|
||||||
|
: m_fd(inotify_init1(IN_CLOEXEC))
|
||||||
|
{
|
||||||
|
if (m_fd < 0) {
|
||||||
|
throw std::system_error(
|
||||||
|
errno, std::generic_category(), "inotify_init1 failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InotifyWatcher::~InotifyWatcher()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
|
||||||
|
if (m_fd >= 0) {
|
||||||
|
::close(m_fd);
|
||||||
|
m_fd = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InotifyWatcher::watch_path_recursively(std::filesystem::path const &path)
|
||||||
|
{
|
||||||
|
add_watch_recursively(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InotifyWatcher::set_callback(callback_t cb)
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(m_watch_mutex);
|
||||||
|
m_callback = std::move(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InotifyWatcher::run()
|
||||||
|
{
|
||||||
|
if (m_running.exchange(true)) {
|
||||||
|
throw std::runtime_error("InotifyWatcher::run called while running");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<char, 4096> buffer {};
|
||||||
|
while (m_running.load()) {
|
||||||
|
pollfd fd_set {
|
||||||
|
.fd = m_fd,
|
||||||
|
.events = POLLIN,
|
||||||
|
.revents = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
int const poll_res { ::poll(&fd_set, 1, poll_timeout_ms) };
|
||||||
|
if (!m_running.load())
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (poll_res < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
if (errno == EBADF || errno == EINVAL)
|
||||||
|
break;
|
||||||
|
throw std::system_error(errno, std::generic_category(), "poll");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poll_res == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (fd_set.revents & (POLLERR | POLLHUP | POLLNVAL))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!(fd_set.revents & POLLIN))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ssize_t const bytes_read { ::read(m_fd, buffer.data(), buffer.size()) };
|
||||||
|
if (bytes_read < 0) {
|
||||||
|
if (errno == EINTR || errno == EAGAIN)
|
||||||
|
continue;
|
||||||
|
if (errno == EBADF || errno == EINVAL)
|
||||||
|
break;
|
||||||
|
throw std::system_error(errno, std::generic_category(), "read");
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t offset { 0 };
|
||||||
|
while (offset + static_cast<ssize_t>(sizeof(inotify_event))
|
||||||
|
<= bytes_read) {
|
||||||
|
auto const *event = reinterpret_cast<inotify_event const *>(
|
||||||
|
buffer.data() + offset);
|
||||||
|
std::string_view name;
|
||||||
|
if (event->len > 0) {
|
||||||
|
auto const *raw_name
|
||||||
|
= buffer.data() + offset + sizeof(inotify_event);
|
||||||
|
auto const *end
|
||||||
|
= std::find(raw_name, raw_name + event->len, '\0');
|
||||||
|
name = std::string_view(raw_name, end - raw_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch_event(event->wd, event->mask, name);
|
||||||
|
|
||||||
|
offset += sizeof(inotify_event) + event->len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_running.store(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InotifyWatcher::stop() { m_running.store(false); }
|
||||||
|
|
||||||
|
void InotifyWatcher::add_watch_for_path(std::filesystem::path const &path)
|
||||||
|
{
|
||||||
|
auto normalized { normalize_path(path) };
|
||||||
|
if (normalized.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto const key = normalized.string();
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(m_watch_mutex);
|
||||||
|
auto it = m_path_to_watch.find(key);
|
||||||
|
if (it != m_path_to_watch.end())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int const wd { ::inotify_add_watch(m_fd, normalized.c_str(), watch_mask) };
|
||||||
|
if (wd < 0) {
|
||||||
|
if (errno == ENOENT || errno == ENOTDIR)
|
||||||
|
return;
|
||||||
|
throw std::system_error(errno, std::generic_category(),
|
||||||
|
"inotify_add_watch failed for " + normalized.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::scoped_lock lock(m_watch_mutex);
|
||||||
|
m_watch_to_path.emplace(wd, normalized);
|
||||||
|
m_path_to_watch.emplace(key, wd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InotifyWatcher::add_watch_recursively(std::filesystem::path const &path)
|
||||||
|
{
|
||||||
|
auto normalized = normalize_path(path);
|
||||||
|
if (normalized.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
add_watch_for_path(normalized);
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::is_directory(normalized, ec))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto const options {
|
||||||
|
std::filesystem::directory_options::follow_directory_symlink
|
||||||
|
| std::filesystem::directory_options::skip_permission_denied
|
||||||
|
};
|
||||||
|
for (auto const &entry : std::filesystem::recursive_directory_iterator(
|
||||||
|
normalized, options)) {
|
||||||
|
std::error_code inner_ec;
|
||||||
|
if (!entry.is_directory(inner_ec) || inner_ec)
|
||||||
|
continue;
|
||||||
|
add_watch_for_path(entry.path());
|
||||||
|
}
|
||||||
|
} catch (std::filesystem::filesystem_error const &) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InotifyWatcher::remove_watch(int wd)
|
||||||
|
{
|
||||||
|
std::filesystem::path removed_path;
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(m_watch_mutex);
|
||||||
|
auto it { m_watch_to_path.find(wd) };
|
||||||
|
if (it == m_watch_to_path.end())
|
||||||
|
return;
|
||||||
|
removed_path = it->second;
|
||||||
|
m_watch_to_path.erase(it);
|
||||||
|
m_path_to_watch.erase(removed_path.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
::inotify_rm_watch(m_fd, wd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InotifyWatcher::dispatch_event(
|
||||||
|
int wd, std::uint32_t mask, std::string_view name)
|
||||||
|
{
|
||||||
|
std::filesystem::path base_path;
|
||||||
|
callback_t callback_copy;
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(m_watch_mutex);
|
||||||
|
auto it { m_watch_to_path.find(wd) };
|
||||||
|
if (it == m_watch_to_path.end())
|
||||||
|
return;
|
||||||
|
base_path = it->second;
|
||||||
|
callback_copy = m_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto event_path { base_path };
|
||||||
|
if (!name.empty())
|
||||||
|
event_path /= name;
|
||||||
|
|
||||||
|
if ((mask & IN_ISDIR)
|
||||||
|
&& (mask & (IN_CREATE | IN_MOVED_TO | IN_ATTRIB | IN_CLOSE_WRITE))) {
|
||||||
|
add_watch_recursively(event_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask & (IN_DELETE_SELF | IN_MOVE_SELF | IN_IGNORED)) {
|
||||||
|
remove_watch(wd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback_copy) {
|
||||||
|
callback_copy(FileWatchEvent {
|
||||||
|
.path = std::move(event_path),
|
||||||
|
.mask = mask,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path InotifyWatcher::normalize_path(
|
||||||
|
std::filesystem::path const &path)
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
auto normalized { std::filesystem::weakly_canonical(path, ec) };
|
||||||
|
if (ec)
|
||||||
|
normalized = std::filesystem::absolute(path, ec);
|
||||||
|
if (ec)
|
||||||
|
return {};
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
57
src/InotifyWatcher.hpp
Normal file
57
src/InotifyWatcher.hpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
struct FileWatchEvent {
|
||||||
|
std::filesystem::path path;
|
||||||
|
std::uint32_t mask;
|
||||||
|
};
|
||||||
|
|
||||||
|
class InotifyWatcher {
|
||||||
|
public:
|
||||||
|
using callback_t = std::function<void(FileWatchEvent const &)>;
|
||||||
|
|
||||||
|
InotifyWatcher();
|
||||||
|
~InotifyWatcher();
|
||||||
|
|
||||||
|
InotifyWatcher(InotifyWatcher const &) = delete;
|
||||||
|
InotifyWatcher &operator=(InotifyWatcher const &) = delete;
|
||||||
|
|
||||||
|
InotifyWatcher(InotifyWatcher &&) = delete;
|
||||||
|
InotifyWatcher &operator=(InotifyWatcher &&) = delete;
|
||||||
|
|
||||||
|
void watch_path_recursively(std::filesystem::path const &path);
|
||||||
|
|
||||||
|
void set_callback(callback_t cb);
|
||||||
|
|
||||||
|
void run();
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void add_watch_for_path(std::filesystem::path const &path);
|
||||||
|
void add_watch_recursively(std::filesystem::path const &path);
|
||||||
|
void remove_watch(int wd);
|
||||||
|
|
||||||
|
void dispatch_event(int wd, std::uint32_t mask, std::string_view name);
|
||||||
|
static std::filesystem::path normalize_path(
|
||||||
|
std::filesystem::path const &path);
|
||||||
|
|
||||||
|
int m_fd;
|
||||||
|
std::atomic<bool> m_running { false };
|
||||||
|
callback_t m_callback;
|
||||||
|
|
||||||
|
std::mutex m_watch_mutex;
|
||||||
|
std::unordered_map<int, std::filesystem::path> m_watch_to_path;
|
||||||
|
std::unordered_map<std::string, int> m_path_to_watch;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
845
src/TextRenderer.cpp
Normal file
845
src/TextRenderer.cpp
Normal file
@@ -0,0 +1,845 @@
|
|||||||
|
#include "TextRenderer.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <limits>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fontconfig/fontconfig.h>
|
||||||
|
|
||||||
|
#include <raylib.h>
|
||||||
|
#include <rlgl.h>
|
||||||
|
|
||||||
|
#undef BLACK
|
||||||
|
#undef WHITE
|
||||||
|
#undef RED
|
||||||
|
#undef GREEN
|
||||||
|
#undef BLUE
|
||||||
|
#undef YELLOW
|
||||||
|
#undef MAGENTA
|
||||||
|
|
||||||
|
#include <ft2build.h>
|
||||||
|
#include FT_FREETYPE_H
|
||||||
|
#include FT_GLYPH_H
|
||||||
|
#include <hb-ft.h>
|
||||||
|
#include <hb.h>
|
||||||
|
|
||||||
|
#include <ext/import-font.h>
|
||||||
|
#include <msdfgen.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int ATLAS_DIMENSION = 1024;
|
||||||
|
constexpr int ATLAS_PADDING = 2;
|
||||||
|
constexpr float DEFAULT_EM_SCALE = 48.0f;
|
||||||
|
|
||||||
|
constexpr float hb_to_em(hb_position_t value, unsigned upem)
|
||||||
|
{
|
||||||
|
return static_cast<float>(value)
|
||||||
|
/ (64.0f * static_cast<float>(upem ? upem : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ft_library() -> FT_Library
|
||||||
|
{
|
||||||
|
static FT_Library library = nullptr;
|
||||||
|
static std::once_flag once;
|
||||||
|
std::call_once(once, [] {
|
||||||
|
if (FT_Init_FreeType(&library) != 0)
|
||||||
|
library = nullptr;
|
||||||
|
else
|
||||||
|
std::atexit([] {
|
||||||
|
if (library)
|
||||||
|
FT_Done_FreeType(library);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return library;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CodepointSpan {
|
||||||
|
uint32_t codepoint {};
|
||||||
|
usize start {};
|
||||||
|
usize end {};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto decode_utf8(std::string_view text) -> std::vector<CodepointSpan>
|
||||||
|
{
|
||||||
|
std::vector<CodepointSpan> spans;
|
||||||
|
usize i = 0;
|
||||||
|
while (i < text.size()) {
|
||||||
|
u8 const byte = static_cast<u8>(text[i]);
|
||||||
|
usize const start = i;
|
||||||
|
usize length = 1;
|
||||||
|
uint32_t cp = 0xFFFD;
|
||||||
|
if (byte < 0x80) {
|
||||||
|
cp = byte;
|
||||||
|
} else if ((byte & 0xE0) == 0xC0) {
|
||||||
|
if (i + 1 < text.size()) {
|
||||||
|
u8 const b1 = static_cast<u8>(text[i + 1]);
|
||||||
|
if ((b1 & 0xC0) == 0x80) {
|
||||||
|
uint32_t t = ((byte & 0x1F) << 6)
|
||||||
|
| (static_cast<uint32_t>(b1) & 0x3F);
|
||||||
|
if (t >= 0x80) {
|
||||||
|
cp = t;
|
||||||
|
length = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((byte & 0xF0) == 0xE0) {
|
||||||
|
if (i + 2 < text.size()) {
|
||||||
|
u8 const b1 = static_cast<u8>(text[i + 1]);
|
||||||
|
u8 const b2 = static_cast<u8>(text[i + 2]);
|
||||||
|
if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80) {
|
||||||
|
uint32_t t = ((byte & 0x0F) << 12)
|
||||||
|
| ((static_cast<uint32_t>(b1) & 0x3F) << 6)
|
||||||
|
| (static_cast<uint32_t>(b2) & 0x3F);
|
||||||
|
if (t >= 0x800 && (t < 0xD800 || t > 0xDFFF)) {
|
||||||
|
cp = t;
|
||||||
|
length = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((byte & 0xF8) == 0xF0) {
|
||||||
|
if (i + 3 < text.size()) {
|
||||||
|
u8 const b1 = static_cast<u8>(text[i + 1]);
|
||||||
|
u8 const b2 = static_cast<u8>(text[i + 2]);
|
||||||
|
u8 const b3 = static_cast<u8>(text[i + 3]);
|
||||||
|
if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80
|
||||||
|
&& (b3 & 0xC0) == 0x80) {
|
||||||
|
uint32_t t = ((byte & 0x07) << 18)
|
||||||
|
| ((static_cast<uint32_t>(b1) & 0x3F) << 12)
|
||||||
|
| ((static_cast<uint32_t>(b2) & 0x3F) << 6)
|
||||||
|
| (static_cast<uint32_t>(b3) & 0x3F);
|
||||||
|
if (t >= 0x10000 && t <= 0x10FFFF) {
|
||||||
|
cp = t;
|
||||||
|
length = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spans.push_back(CodepointSpan {
|
||||||
|
.codepoint = cp,
|
||||||
|
.start = start,
|
||||||
|
.end = std::min(text.size(), start + length),
|
||||||
|
});
|
||||||
|
i += length;
|
||||||
|
}
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto TextRenderer::flush_font(FontRuntime &rt, FontData &fd) -> void
|
||||||
|
{
|
||||||
|
rt.glyph_cache.clear();
|
||||||
|
fd.glyphs.clear();
|
||||||
|
rt.pen_x = ATLAS_PADDING;
|
||||||
|
rt.pen_y = ATLAS_PADDING;
|
||||||
|
rt.row_height = 0;
|
||||||
|
if (fd.atlas_img.data)
|
||||||
|
ImageClearBackground(&fd.atlas_img, BLANK);
|
||||||
|
if (fd.atlas.id != 0 && fd.atlas_img.data)
|
||||||
|
UpdateTexture(fd.atlas, fd.atlas_img.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::allocate_region(FontRuntime &rt, FontData &fd, int width,
|
||||||
|
int height) -> std::optional<std::pair<int, int>>
|
||||||
|
{
|
||||||
|
(void)fd;
|
||||||
|
int padded_w = width + ATLAS_PADDING;
|
||||||
|
if (padded_w > rt.atlas_width || height + ATLAS_PADDING > rt.atlas_height)
|
||||||
|
return std::nullopt;
|
||||||
|
if (rt.pen_x + padded_w > rt.atlas_width) {
|
||||||
|
rt.pen_x = ATLAS_PADDING;
|
||||||
|
rt.pen_y += rt.row_height;
|
||||||
|
rt.row_height = 0;
|
||||||
|
}
|
||||||
|
if (rt.pen_y + height + ATLAS_PADDING > rt.atlas_height)
|
||||||
|
return std::nullopt;
|
||||||
|
int x = rt.pen_x;
|
||||||
|
int y = rt.pen_y;
|
||||||
|
rt.pen_x += padded_w;
|
||||||
|
rt.row_height = std::max(rt.row_height, height + ATLAS_PADDING);
|
||||||
|
return std::pair { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::upload_region(FontData &fd, int dst_x, int dst_y, int width,
|
||||||
|
int height, std::vector<Color> const &buffer) -> void
|
||||||
|
{
|
||||||
|
Rectangle rec { static_cast<float>(dst_x), static_cast<float>(dst_y),
|
||||||
|
static_cast<float>(width), static_cast<float>(height) };
|
||||||
|
if (fd.atlas.id != 0)
|
||||||
|
UpdateTextureRec(fd.atlas, rec, buffer.data());
|
||||||
|
if (!fd.atlas_img.data)
|
||||||
|
return;
|
||||||
|
auto *pixels = static_cast<Color *>(fd.atlas_img.data);
|
||||||
|
for (int row = 0; row < height; ++row) {
|
||||||
|
auto *dst = pixels + (dst_y + row) * fd.atlas_img.width + dst_x;
|
||||||
|
std::memcpy(dst, buffer.data() + row * width, sizeof(Color) * width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd,
|
||||||
|
u32 glyph_index) -> std::optional<GlyphCacheEntry>
|
||||||
|
{
|
||||||
|
auto const gen_start = std::chrono::steady_clock::now();
|
||||||
|
msdfgen::Shape shape;
|
||||||
|
double advance_em = 0.0;
|
||||||
|
msdfgen::GlyphIndex const index(glyph_index);
|
||||||
|
if (!rt.msdf_font
|
||||||
|
|| !msdfgen::loadGlyph(shape, rt.msdf_font, index,
|
||||||
|
msdfgen::FONT_SCALING_EM_NORMALIZED, &advance_em))
|
||||||
|
return std::nullopt;
|
||||||
|
shape.normalize();
|
||||||
|
// FIXME: Figure out shader
|
||||||
|
// msdfgen::edgeColoringInkTrap(shape, 3.0);
|
||||||
|
auto bounds = shape.getBounds();
|
||||||
|
float const width_em = static_cast<float>(bounds.r - bounds.l);
|
||||||
|
float const height_em = static_cast<float>(bounds.t - bounds.b);
|
||||||
|
double const scale = rt.em_scale;
|
||||||
|
int bmp_w = std::max(
|
||||||
|
1, static_cast<int>(std::ceil(width_em * scale + 2.0 * rt.px_range)));
|
||||||
|
int bmp_h = std::max(
|
||||||
|
1, static_cast<int>(std::ceil(height_em * scale + 2.0 * rt.px_range)));
|
||||||
|
|
||||||
|
if (bmp_w + ATLAS_PADDING > rt.atlas_width
|
||||||
|
|| bmp_h + ATLAS_PADDING > rt.atlas_height) {
|
||||||
|
TraceLog(LOG_WARNING, "Glyph %u bitmap %dx%d exceeds atlas %dx%d",
|
||||||
|
glyph_index, bmp_w, bmp_h, rt.atlas_width, rt.atlas_height);
|
||||||
|
GlyphCacheEntry too_large {};
|
||||||
|
too_large.width = 0;
|
||||||
|
too_large.height = 0;
|
||||||
|
return too_large;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto place = allocate_region(rt, fd, bmp_w, bmp_h);
|
||||||
|
if (!place) {
|
||||||
|
TraceLog(LOG_INFO, "Atlas full, flushing before glyph %u", glyph_index);
|
||||||
|
flush_font(rt, fd);
|
||||||
|
place = allocate_region(rt, fd, bmp_w, bmp_h);
|
||||||
|
if (!place)
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
msdfgen::Bitmap<float, 3> msdf_bitmap(bmp_w, bmp_h);
|
||||||
|
msdfgen::Vector2 scale_vec(scale, scale);
|
||||||
|
double const inv_scale = 1.0 / scale;
|
||||||
|
msdfgen::Vector2 translate(-bounds.l + rt.px_range * inv_scale,
|
||||||
|
-bounds.b + rt.px_range * inv_scale);
|
||||||
|
msdfgen::generateMSDF(
|
||||||
|
msdf_bitmap, shape, rt.px_range, scale_vec, translate);
|
||||||
|
|
||||||
|
std::vector<Color> buffer(static_cast<usize>(bmp_w) * bmp_h);
|
||||||
|
// FIXME: Figure out shader
|
||||||
|
// for (int y = 0; y < bmp_h; ++y) {
|
||||||
|
// int const dst_y = bmp_h - 1 - y;
|
||||||
|
// for (int x = 0; x < bmp_w; ++x) {
|
||||||
|
// float const *px = msdf_bitmap(x, y);
|
||||||
|
// auto const r = msdfgen::pixelFloatToByte(px[0]);
|
||||||
|
// auto const g = msdfgen::pixelFloatToByte(px[1]);
|
||||||
|
// auto const b = msdfgen::pixelFloatToByte(px[2]);
|
||||||
|
// buffer[static_cast<usize>(dst_y) * bmp_w + x]
|
||||||
|
// = Color { r, g, b, 255 };
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
auto c1 { (int)std::round(msdf_bitmap(0, 0)[3]) };
|
||||||
|
auto c4 { (int)std::round(msdf_bitmap(bmp_w - 1, bmp_h - 1)[3]) };
|
||||||
|
|
||||||
|
auto sum_white = 0;
|
||||||
|
auto sum_black = 0;
|
||||||
|
for (int y = 0; y < bmp_h; ++y) {
|
||||||
|
for (int x = 0; x < bmp_w; ++x) {
|
||||||
|
float const *px = msdf_bitmap(x, y);
|
||||||
|
auto const r = msdfgen::pixelFloatToByte(px[0]);
|
||||||
|
if (r > 127) {
|
||||||
|
sum_white++;
|
||||||
|
} else {
|
||||||
|
sum_black++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool flip { sum_white > sum_black && (float)bmp_w / (float)bmp_h > 0.6 };
|
||||||
|
if (c1 == c4) {
|
||||||
|
flip = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This really isn't the most accurate thing in the world but should work
|
||||||
|
// for now. Things like commas might be fucked.
|
||||||
|
for (int y = 0; y < bmp_h; ++y) {
|
||||||
|
int const dst_y = bmp_h - 1 - y;
|
||||||
|
for (int x = 0; x < bmp_w; ++x) {
|
||||||
|
float const *px = msdf_bitmap(x, y);
|
||||||
|
auto const r = msdfgen::pixelFloatToByte(px[0]);
|
||||||
|
if (flip) {
|
||||||
|
buffer[static_cast<usize>(dst_y) * bmp_w + x] = Color { 255,
|
||||||
|
255, 255, static_cast<unsigned char>(255 - r) };
|
||||||
|
} else {
|
||||||
|
buffer[static_cast<usize>(dst_y) * bmp_w + x]
|
||||||
|
= Color { 255, 255, 255, r };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_region(fd, place->first, place->second, bmp_w, bmp_h, buffer);
|
||||||
|
|
||||||
|
GlyphCacheEntry entry;
|
||||||
|
entry.atlas_x = place->first;
|
||||||
|
entry.atlas_y = place->second;
|
||||||
|
entry.width = bmp_w;
|
||||||
|
entry.height = bmp_h;
|
||||||
|
|
||||||
|
entry.glyph.advance = static_cast<float>(advance_em);
|
||||||
|
entry.glyph.plane_bounds.left = static_cast<float>(bounds.l);
|
||||||
|
entry.glyph.plane_bounds.right = static_cast<float>(bounds.r);
|
||||||
|
entry.glyph.plane_bounds.top = static_cast<float>(bounds.t);
|
||||||
|
entry.glyph.plane_bounds.bottom = static_cast<float>(bounds.b);
|
||||||
|
entry.glyph.glyph_bounds.left = static_cast<float>(entry.atlas_x);
|
||||||
|
entry.glyph.glyph_bounds.top = static_cast<float>(entry.atlas_y);
|
||||||
|
entry.glyph.glyph_bounds.right
|
||||||
|
= static_cast<float>(entry.atlas_x + entry.width);
|
||||||
|
entry.glyph.glyph_bounds.bottom
|
||||||
|
= static_cast<float>(entry.atlas_y + entry.height);
|
||||||
|
|
||||||
|
auto const gen_end = std::chrono::steady_clock::now();
|
||||||
|
auto const gen_ms
|
||||||
|
= std::chrono::duration<double, std::milli>(gen_end - gen_start)
|
||||||
|
.count();
|
||||||
|
if (gen_ms > 2.0)
|
||||||
|
TraceLog(LOG_INFO, "Generated glyph %u in %.2f ms (%dx%d texels)",
|
||||||
|
glyph_index, gen_ms, entry.width, entry.height);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index,
|
||||||
|
bool mark_usage) -> GlyphCacheEntry *
|
||||||
|
{
|
||||||
|
auto it = rt.glyph_cache.find(glyph_index);
|
||||||
|
if (it != rt.glyph_cache.end()) {
|
||||||
|
if (mark_usage)
|
||||||
|
it->second.stamp = rt.frame_stamp;
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
auto entry = generate_glyph(rt, fd, glyph_index);
|
||||||
|
if (!entry)
|
||||||
|
return nullptr;
|
||||||
|
auto [inserted_it, ok]
|
||||||
|
= rt.glyph_cache.emplace(glyph_index, std::move(*entry));
|
||||||
|
if (!ok)
|
||||||
|
return nullptr;
|
||||||
|
inserted_it->second.stamp
|
||||||
|
= mark_usage ? rt.frame_stamp : inserted_it->second.stamp;
|
||||||
|
fd.glyphs[glyph_index] = inserted_it->second.glyph;
|
||||||
|
return &inserted_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRenderer::TextRenderer()
|
||||||
|
{
|
||||||
|
static char const msdf_vs_data[] {
|
||||||
|
#embed "base.vert"
|
||||||
|
, 0 // cppcheck-suppress syntaxError
|
||||||
|
};
|
||||||
|
static char const msdf_fs_data[] {
|
||||||
|
#embed "msdf.frag"
|
||||||
|
, 0 // cppcheck-suppress syntaxError
|
||||||
|
};
|
||||||
|
m_msdf_shader = LoadShaderFromMemory(msdf_vs_data, msdf_fs_data);
|
||||||
|
assert(IsShaderValid(m_msdf_shader));
|
||||||
|
m_px_range_uniform = GetShaderLocation(m_msdf_shader, "pxRange");
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRenderer::~TextRenderer()
|
||||||
|
{
|
||||||
|
for (usize i = 0; i < m_font_sets.size(); ++i) {
|
||||||
|
FontHandle handle;
|
||||||
|
handle.id = i;
|
||||||
|
unload_font(handle);
|
||||||
|
}
|
||||||
|
// Not unloading the shader... I have no clue why, but there's some sort of
|
||||||
|
// double free. I love C interop!!!!
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::measure_text(FontHandle const font,
|
||||||
|
std::string_view const text, int const size) -> Vector2
|
||||||
|
{
|
||||||
|
usize const handle_id = font();
|
||||||
|
if (handle_id >= m_font_sets.size())
|
||||||
|
return Vector2 { 0.0f, 0.0f };
|
||||||
|
auto const &font_set = m_font_sets[handle_id];
|
||||||
|
if (font_set.font_indices.empty())
|
||||||
|
return Vector2 { 0.0f, 0.0f };
|
||||||
|
|
||||||
|
auto placements = shape_text(font, text);
|
||||||
|
|
||||||
|
auto primary_runtime_index = font_set.font_indices.front();
|
||||||
|
if (placements.empty()) {
|
||||||
|
if (primary_runtime_index >= m_font_runtime.size()
|
||||||
|
|| !m_font_runtime[primary_runtime_index])
|
||||||
|
return Vector2 { 0.0f, 0.0f };
|
||||||
|
auto const &rt_primary = *m_font_runtime[primary_runtime_index];
|
||||||
|
float height_em = rt_primary.ascent - rt_primary.descent;
|
||||||
|
return Vector2 { 0.0f, height_em * static_cast<float>(size) };
|
||||||
|
}
|
||||||
|
|
||||||
|
float advance_em = 0.0f;
|
||||||
|
float min_x_em = 0.0f;
|
||||||
|
float max_x_em = 0.0f;
|
||||||
|
bool first = true;
|
||||||
|
bool have_metrics = false;
|
||||||
|
float max_ascent = 0.0f;
|
||||||
|
float min_descent = 0.0f;
|
||||||
|
|
||||||
|
for (auto const &placement : placements) {
|
||||||
|
usize const runtime_index = placement.runtime_index;
|
||||||
|
if (runtime_index >= m_font_runtime.size()
|
||||||
|
|| !m_font_runtime[runtime_index])
|
||||||
|
continue;
|
||||||
|
auto &rt = *m_font_runtime[runtime_index];
|
||||||
|
auto &fd = m_font_data[runtime_index];
|
||||||
|
auto *entry = ensure_glyph(rt, fd, placement.glyph_index, false);
|
||||||
|
if (!entry || entry->width == 0 || entry->height == 0)
|
||||||
|
continue;
|
||||||
|
float const x_offset_em = hb_to_em(placement.x_offset, rt.units_per_em);
|
||||||
|
float const left
|
||||||
|
= advance_em + x_offset_em + entry->glyph.plane_bounds.left;
|
||||||
|
float const right
|
||||||
|
= advance_em + x_offset_em + entry->glyph.plane_bounds.right;
|
||||||
|
if (first) {
|
||||||
|
min_x_em = left;
|
||||||
|
max_x_em = right;
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
min_x_em = std::min(min_x_em, left);
|
||||||
|
max_x_em = std::max(max_x_em, right);
|
||||||
|
}
|
||||||
|
if (!have_metrics) {
|
||||||
|
max_ascent = rt.ascent;
|
||||||
|
min_descent = rt.descent;
|
||||||
|
have_metrics = true;
|
||||||
|
} else {
|
||||||
|
max_ascent = std::max(max_ascent, rt.ascent);
|
||||||
|
min_descent = std::min(min_descent, rt.descent);
|
||||||
|
}
|
||||||
|
advance_em += hb_to_em(placement.x_advance, rt.units_per_em);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
if (primary_runtime_index >= m_font_runtime.size()
|
||||||
|
|| !m_font_runtime[primary_runtime_index])
|
||||||
|
return Vector2 { 0.0f, 0.0f };
|
||||||
|
auto const &rt = *m_font_runtime[primary_runtime_index];
|
||||||
|
float height_em = rt.ascent - rt.descent;
|
||||||
|
return Vector2 { 0.0f, height_em * static_cast<float>(size) };
|
||||||
|
}
|
||||||
|
|
||||||
|
float width_em = std::max(max_x_em, advance_em) - min_x_em;
|
||||||
|
float height_em = 0.0f;
|
||||||
|
if (have_metrics) {
|
||||||
|
height_em = max_ascent - min_descent;
|
||||||
|
} else if (primary_runtime_index < m_font_runtime.size()
|
||||||
|
&& m_font_runtime[primary_runtime_index]) {
|
||||||
|
auto const &rt = *m_font_runtime[primary_runtime_index];
|
||||||
|
height_em = rt.ascent - rt.descent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Vector2 { width_em * static_cast<float>(size),
|
||||||
|
height_em * static_cast<float>(size) };
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::draw_text(FontHandle const font, std::string_view const text,
|
||||||
|
Vector2 const pos, int const size, Color const color) -> void
|
||||||
|
{
|
||||||
|
auto const draw_start = std::chrono::steady_clock::now();
|
||||||
|
int const pos_x = pos.x;
|
||||||
|
int const pos_y = pos.y;
|
||||||
|
usize const handle_id = font();
|
||||||
|
if (handle_id >= m_font_sets.size())
|
||||||
|
return;
|
||||||
|
auto const &font_set = m_font_sets[handle_id];
|
||||||
|
if (font_set.font_indices.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto placements = shape_text(font, text);
|
||||||
|
if (placements.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
float const size_f = static_cast<float>(size);
|
||||||
|
float pen_x_em = 0.0f;
|
||||||
|
float pen_y_em = 0.0f;
|
||||||
|
std::vector<usize> updated_stamp;
|
||||||
|
updated_stamp.reserve(font_set.font_indices.size());
|
||||||
|
|
||||||
|
for (auto const &placement : placements) {
|
||||||
|
usize const runtime_index = placement.runtime_index;
|
||||||
|
if (runtime_index >= m_font_runtime.size()
|
||||||
|
|| !m_font_runtime[runtime_index])
|
||||||
|
continue;
|
||||||
|
auto &rt = *m_font_runtime[runtime_index];
|
||||||
|
auto &fd = m_font_data[runtime_index];
|
||||||
|
if (std::find(updated_stamp.begin(), updated_stamp.end(), runtime_index)
|
||||||
|
== updated_stamp.end()) {
|
||||||
|
rt.frame_stamp++;
|
||||||
|
updated_stamp.push_back(runtime_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *entry = ensure_glyph(rt, fd, placement.glyph_index, true);
|
||||||
|
if (!entry || entry->width == 0 || entry->height == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
float const advance_em = hb_to_em(placement.x_advance, rt.units_per_em);
|
||||||
|
float const x_offset_em = hb_to_em(placement.x_offset, rt.units_per_em);
|
||||||
|
float const y_offset_em = hb_to_em(placement.y_offset, rt.units_per_em);
|
||||||
|
float const x_base_em = pen_x_em + x_offset_em;
|
||||||
|
float const y_base_em = pen_y_em + y_offset_em;
|
||||||
|
float const scale_px = size_f / static_cast<float>(rt.em_scale);
|
||||||
|
float const margin_px = static_cast<float>(rt.px_range) * scale_px;
|
||||||
|
float const dest_x = pos_x
|
||||||
|
+ (x_base_em + entry->glyph.plane_bounds.left) * size_f - margin_px;
|
||||||
|
float const dest_y = pos_y
|
||||||
|
- (y_base_em + entry->glyph.plane_bounds.top) * size_f - margin_px;
|
||||||
|
float const dest_w = static_cast<float>(entry->width) * scale_px;
|
||||||
|
float const dest_h = static_cast<float>(entry->height) * scale_px;
|
||||||
|
|
||||||
|
Rectangle source {
|
||||||
|
entry->glyph.glyph_bounds.left,
|
||||||
|
entry->glyph.glyph_bounds.top,
|
||||||
|
static_cast<float>(entry->width),
|
||||||
|
static_cast<float>(entry->height),
|
||||||
|
};
|
||||||
|
Rectangle dest { dest_x, dest_y, dest_w, dest_h };
|
||||||
|
DrawTexturePro(
|
||||||
|
fd.atlas, source, dest, Vector2 { 0.0f, 0.0f }, 0.0f, color);
|
||||||
|
|
||||||
|
pen_x_em += advance_em;
|
||||||
|
pen_y_em += hb_to_em(placement.y_advance, rt.units_per_em);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const draw_end = std::chrono::steady_clock::now();
|
||||||
|
auto const draw_ms
|
||||||
|
= std::chrono::duration<double, std::milli>(draw_end - draw_start)
|
||||||
|
.count();
|
||||||
|
if (draw_ms > 5.0)
|
||||||
|
TraceLog(LOG_INFO, "draw_text took %.2f ms for %zu glyphs", draw_ms,
|
||||||
|
placements.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::load_single_font(std::filesystem::path const &path)
|
||||||
|
-> std::optional<usize>
|
||||||
|
{
|
||||||
|
FT_Library const ft = ft_library();
|
||||||
|
if (!ft)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
FT_Face face = nullptr;
|
||||||
|
if (FT_New_Face(ft, path.string().c_str(), 0, &face) != 0)
|
||||||
|
return std::nullopt;
|
||||||
|
if (FT_Select_Charmap(face, FT_ENCODING_UNICODE) != 0) {
|
||||||
|
FT_Done_Face(face);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto runtime = std::make_unique<FontRuntime>();
|
||||||
|
runtime->face = face;
|
||||||
|
runtime->atlas_width = ATLAS_DIMENSION;
|
||||||
|
runtime->atlas_height = ATLAS_DIMENSION;
|
||||||
|
runtime->pen_x = ATLAS_PADDING;
|
||||||
|
runtime->pen_y = ATLAS_PADDING;
|
||||||
|
runtime->row_height = 0;
|
||||||
|
runtime->px_range = 0.05; // kDefaultPxRange;
|
||||||
|
runtime->em_scale = DEFAULT_EM_SCALE;
|
||||||
|
runtime->frame_stamp = 0;
|
||||||
|
runtime->units_per_em
|
||||||
|
= static_cast<unsigned>(face->units_per_EM ? face->units_per_EM : 2048);
|
||||||
|
runtime->ascent = static_cast<float>(face->ascender)
|
||||||
|
/ (64.0f * static_cast<float>(runtime->units_per_em));
|
||||||
|
runtime->descent = static_cast<float>(face->descender)
|
||||||
|
/ (64.0f * static_cast<float>(runtime->units_per_em));
|
||||||
|
float line_height = static_cast<float>(face->height)
|
||||||
|
/ (64.0f * static_cast<float>(runtime->units_per_em));
|
||||||
|
float adv_height = runtime->ascent - runtime->descent;
|
||||||
|
runtime->line_gap = std::max(0.0f, line_height - adv_height);
|
||||||
|
|
||||||
|
runtime->hb_face = hb_ft_face_create_referenced(face);
|
||||||
|
if (!runtime->hb_face) {
|
||||||
|
FT_Done_Face(face);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
runtime->hb_font = hb_ft_font_create_referenced(face);
|
||||||
|
if (!runtime->hb_font) {
|
||||||
|
hb_face_destroy(runtime->hb_face);
|
||||||
|
FT_Done_Face(face);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
hb_font_set_scale(runtime->hb_font,
|
||||||
|
static_cast<int>(runtime->units_per_em) << 6,
|
||||||
|
static_cast<int>(runtime->units_per_em) << 6);
|
||||||
|
hb_ft_font_set_funcs(runtime->hb_font);
|
||||||
|
|
||||||
|
runtime->msdf_font = msdfgen::adoptFreetypeFont(face);
|
||||||
|
if (!runtime->msdf_font) {
|
||||||
|
hb_font_destroy(runtime->hb_font);
|
||||||
|
hb_face_destroy(runtime->hb_face);
|
||||||
|
FT_Done_Face(face);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
FontData font_data {};
|
||||||
|
font_data.font_path = path;
|
||||||
|
font_data.atlas_img
|
||||||
|
= GenImageColor(runtime->atlas_width, runtime->atlas_height, BLANK);
|
||||||
|
if (!font_data.atlas_img.data) {
|
||||||
|
msdfgen::destroyFont(runtime->msdf_font);
|
||||||
|
runtime->msdf_font = nullptr;
|
||||||
|
hb_font_destroy(runtime->hb_font);
|
||||||
|
hb_face_destroy(runtime->hb_face);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
font_data.atlas = LoadTextureFromImage(font_data.atlas_img);
|
||||||
|
if (font_data.atlas.id == 0) {
|
||||||
|
UnloadImage(font_data.atlas_img);
|
||||||
|
msdfgen::destroyFont(runtime->msdf_font);
|
||||||
|
runtime->msdf_font = nullptr;
|
||||||
|
hb_font_destroy(runtime->hb_font);
|
||||||
|
hb_face_destroy(runtime->hb_face);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
SetTextureFilter(font_data.atlas, TEXTURE_FILTER_BILINEAR);
|
||||||
|
SetTextureWrap(font_data.atlas, TEXTURE_WRAP_CLAMP);
|
||||||
|
flush_font(*runtime, font_data);
|
||||||
|
|
||||||
|
m_font_data.emplace_back(std::move(font_data));
|
||||||
|
m_font_runtime.emplace_back(std::move(runtime));
|
||||||
|
return m_font_data.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::load_font(std::filesystem::path const &path,
|
||||||
|
std::span<std::filesystem::path const> fallback_fonts)
|
||||||
|
-> std::optional<FontHandle>
|
||||||
|
{
|
||||||
|
auto primary_index = load_single_font(path);
|
||||||
|
if (!primary_index)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
FontSet set;
|
||||||
|
set.font_indices.push_back(*primary_index);
|
||||||
|
|
||||||
|
for (auto const &fallback_path : fallback_fonts) {
|
||||||
|
auto fallback_index = load_single_font(fallback_path);
|
||||||
|
if (!fallback_index) {
|
||||||
|
TraceLog(LOG_WARNING, "Failed to load fallback font: %s",
|
||||||
|
fallback_path.string().c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
set.font_indices.push_back(*fallback_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_font_sets.emplace_back(std::move(set));
|
||||||
|
FontHandle handle;
|
||||||
|
handle.id = m_font_sets.size() - 1;
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::shape_text(FontHandle const font,
|
||||||
|
std::string_view const text) -> std::vector<GlyphPlacement>
|
||||||
|
{
|
||||||
|
std::vector<GlyphPlacement> shaped;
|
||||||
|
if (text.empty())
|
||||||
|
return shaped;
|
||||||
|
|
||||||
|
usize const handle_id = font();
|
||||||
|
if (handle_id >= m_font_sets.size())
|
||||||
|
return shaped;
|
||||||
|
auto const &font_set = m_font_sets[handle_id];
|
||||||
|
if (font_set.font_indices.empty())
|
||||||
|
return shaped;
|
||||||
|
|
||||||
|
auto codepoints = decode_utf8(text);
|
||||||
|
if (codepoints.empty())
|
||||||
|
return shaped;
|
||||||
|
|
||||||
|
constexpr usize kNoFont = std::numeric_limits<usize>::max();
|
||||||
|
std::vector<usize> selections(codepoints.size(), kNoFont);
|
||||||
|
for (usize i = 0; i < codepoints.size(); ++i) {
|
||||||
|
bool matched = false;
|
||||||
|
for (usize candidate = 0; candidate < font_set.font_indices.size();
|
||||||
|
++candidate) {
|
||||||
|
usize runtime_index = font_set.font_indices[candidate];
|
||||||
|
if (runtime_index >= m_font_runtime.size())
|
||||||
|
continue;
|
||||||
|
auto const &runtime_ptr = m_font_runtime[runtime_index];
|
||||||
|
if (!runtime_ptr || !runtime_ptr->face)
|
||||||
|
continue;
|
||||||
|
FT_UInt glyph
|
||||||
|
= FT_Get_Char_Index(runtime_ptr->face, codepoints[i].codepoint);
|
||||||
|
if (glyph != 0) {
|
||||||
|
selections[i] = candidate;
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched)
|
||||||
|
selections[i] = kNoFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
usize idx = 0;
|
||||||
|
while (idx < codepoints.size()) {
|
||||||
|
usize font_choice = selections[idx];
|
||||||
|
if (font_choice == kNoFont) {
|
||||||
|
++idx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (font_choice >= font_set.font_indices.size())
|
||||||
|
font_choice = 0;
|
||||||
|
usize runtime_index = font_set.font_indices[font_choice];
|
||||||
|
if (runtime_index >= m_font_runtime.size()
|
||||||
|
|| !m_font_runtime[runtime_index]
|
||||||
|
|| !m_font_runtime[runtime_index]->hb_font) {
|
||||||
|
++idx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
usize segment_start = codepoints[idx].start;
|
||||||
|
usize segment_end = codepoints[idx].end;
|
||||||
|
usize end_idx = idx + 1;
|
||||||
|
while (
|
||||||
|
end_idx < codepoints.size() && selections[end_idx] == font_choice) {
|
||||||
|
segment_end = codepoints[end_idx].end;
|
||||||
|
++end_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment_end <= segment_start) {
|
||||||
|
idx = end_idx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view segment
|
||||||
|
= text.substr(segment_start, segment_end - segment_start);
|
||||||
|
if (segment.empty()) {
|
||||||
|
idx = end_idx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hb_buffer_t *buffer = hb_buffer_create();
|
||||||
|
hb_buffer_add_utf8(buffer, segment.data(),
|
||||||
|
static_cast<int>(segment.size()), 0,
|
||||||
|
static_cast<int>(segment.size()));
|
||||||
|
hb_buffer_guess_segment_properties(buffer);
|
||||||
|
hb_shape(m_font_runtime[runtime_index]->hb_font, buffer, nullptr, 0);
|
||||||
|
|
||||||
|
unsigned length = hb_buffer_get_length(buffer);
|
||||||
|
auto *infos = hb_buffer_get_glyph_infos(buffer, nullptr);
|
||||||
|
auto *positions = hb_buffer_get_glyph_positions(buffer, nullptr);
|
||||||
|
for (unsigned i = 0; i < length; ++i) {
|
||||||
|
GlyphPlacement placement;
|
||||||
|
placement.runtime_index = runtime_index;
|
||||||
|
placement.glyph_index = infos[i].codepoint;
|
||||||
|
placement.x_advance = positions[i].x_advance;
|
||||||
|
placement.y_advance = positions[i].y_advance;
|
||||||
|
placement.x_offset = positions[i].x_offset;
|
||||||
|
placement.y_offset = positions[i].y_offset;
|
||||||
|
shaped.emplace_back(placement);
|
||||||
|
}
|
||||||
|
hb_buffer_destroy(buffer);
|
||||||
|
idx = end_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shaped;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto TextRenderer::unload_font(FontHandle const font) -> void
|
||||||
|
{
|
||||||
|
usize const handle_id = font();
|
||||||
|
if (handle_id >= m_font_sets.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto &font_set = m_font_sets[handle_id];
|
||||||
|
for (usize runtime_index : font_set.font_indices) {
|
||||||
|
if (runtime_index >= m_font_runtime.size())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (auto &runtime_ptr = m_font_runtime[runtime_index]) {
|
||||||
|
auto &rt = *runtime_ptr;
|
||||||
|
rt.glyph_cache.clear();
|
||||||
|
// No freeing here because they are already cleaned up somewhere...
|
||||||
|
// idk. fml.
|
||||||
|
rt.face = nullptr;
|
||||||
|
}
|
||||||
|
m_font_runtime[runtime_index].reset();
|
||||||
|
|
||||||
|
if (runtime_index < m_font_data.size()) {
|
||||||
|
auto &fd = m_font_data[runtime_index];
|
||||||
|
if (fd.atlas.id != 0)
|
||||||
|
UnloadTexture(fd.atlas);
|
||||||
|
if (fd.atlas_img.data)
|
||||||
|
UnloadImage(fd.atlas_img);
|
||||||
|
fd.atlas = Texture2D {};
|
||||||
|
fd.atlas_img = Image {};
|
||||||
|
fd.glyphs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font_set.font_indices.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto find_font_path(std::string_view path)
|
||||||
|
-> std::optional<std::filesystem::path>
|
||||||
|
{
|
||||||
|
static std::once_flag fc_once;
|
||||||
|
std::call_once(fc_once, []() {
|
||||||
|
if (FcInit())
|
||||||
|
std::atexit([] { FcFini(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
static std::mutex m;
|
||||||
|
static std::unordered_map<std::string, std::optional<std::string>> cache;
|
||||||
|
|
||||||
|
std::string const key(path);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(m);
|
||||||
|
if (auto it = cache.find(key); it != cache.end())
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
FcPattern *pattern
|
||||||
|
= FcNameParse(reinterpret_cast<FcChar8 const *>(key.c_str()));
|
||||||
|
if (!pattern) {
|
||||||
|
std::scoped_lock lock(m);
|
||||||
|
return cache[key] = std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
FcConfigSubstitute(nullptr, pattern, FcMatchPattern);
|
||||||
|
FcDefaultSubstitute(pattern);
|
||||||
|
|
||||||
|
FcResult result;
|
||||||
|
FcPattern *font = FcFontMatch(nullptr, pattern, &result);
|
||||||
|
|
||||||
|
std::optional<std::string> final_path;
|
||||||
|
if (font) {
|
||||||
|
FcChar8 *file;
|
||||||
|
if (FcPatternGetString(font, FC_FILE, 0, &file) == FcResultMatch)
|
||||||
|
final_path = reinterpret_cast<char *>(file);
|
||||||
|
FcPatternDestroy(font);
|
||||||
|
}
|
||||||
|
|
||||||
|
FcPatternDestroy(pattern);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(m);
|
||||||
|
cache[key] = final_path;
|
||||||
|
}
|
||||||
|
return final_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
151
src/TextRenderer.hpp
Normal file
151
src/TextRenderer.hpp
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <raylib.h>
|
||||||
|
|
||||||
|
#include "common.hpp"
|
||||||
|
|
||||||
|
struct hb_face_t;
|
||||||
|
struct hb_font_t;
|
||||||
|
struct FT_FaceRec_;
|
||||||
|
using FT_Face = FT_FaceRec_ *;
|
||||||
|
|
||||||
|
namespace msdfgen {
|
||||||
|
class FontHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
struct FontHandle { // cppcheck-supress noConstructor
|
||||||
|
FontHandle() = default;
|
||||||
|
|
||||||
|
auto operator()() const -> auto const &
|
||||||
|
{
|
||||||
|
if (id == 0xffffffff) {
|
||||||
|
throw std::runtime_error("Uninitialized FontHandle");
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend struct TextRenderer;
|
||||||
|
usize id { 0xffffffff };
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FontRuntime;
|
||||||
|
|
||||||
|
struct TextRenderer {
|
||||||
|
TextRenderer(); // Requires raylib to be initialized!
|
||||||
|
~TextRenderer();
|
||||||
|
|
||||||
|
TextRenderer(TextRenderer const &) = delete;
|
||||||
|
auto operator=(TextRenderer const &) -> TextRenderer & = delete;
|
||||||
|
TextRenderer(TextRenderer &&) = default;
|
||||||
|
auto operator=(TextRenderer &&) -> TextRenderer & = default;
|
||||||
|
|
||||||
|
auto measure_text(FontHandle const font, std::string_view const text,
|
||||||
|
int const size = 16) -> Vector2;
|
||||||
|
auto draw_text(FontHandle const font, std::string_view const text,
|
||||||
|
Vector2 const pos, int const size = 16, Color const color = WHITE)
|
||||||
|
-> void;
|
||||||
|
|
||||||
|
auto load_font(std::filesystem::path const &path,
|
||||||
|
std::span<std::filesystem::path const> fallback_fonts)
|
||||||
|
-> std::optional<FontHandle>;
|
||||||
|
auto unload_font(FontHandle const font) -> void;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct FontData {
|
||||||
|
struct Glyph {
|
||||||
|
struct Rect {
|
||||||
|
float top, left, right, bottom;
|
||||||
|
};
|
||||||
|
|
||||||
|
float advance;
|
||||||
|
Rect plane_bounds;
|
||||||
|
Rect glyph_bounds;
|
||||||
|
};
|
||||||
|
|
||||||
|
Texture2D atlas;
|
||||||
|
Image atlas_img;
|
||||||
|
std::filesystem::path font_path;
|
||||||
|
std::unordered_map<u32, Glyph> glyphs;
|
||||||
|
};
|
||||||
|
|
||||||
|
Shader m_msdf_shader;
|
||||||
|
int m_px_range_uniform { -1 };
|
||||||
|
|
||||||
|
std::vector<FontData> m_font_data;
|
||||||
|
struct GlyphCacheEntry {
|
||||||
|
FontData::Glyph glyph;
|
||||||
|
int atlas_x {};
|
||||||
|
int atlas_y {};
|
||||||
|
int width {};
|
||||||
|
int height {};
|
||||||
|
int stamp {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FontRuntime {
|
||||||
|
FT_Face face {};
|
||||||
|
hb_face_t *hb_face {};
|
||||||
|
hb_font_t *hb_font {};
|
||||||
|
msdfgen::FontHandle *msdf_font {};
|
||||||
|
|
||||||
|
int atlas_width {};
|
||||||
|
int atlas_height {};
|
||||||
|
int pen_x {};
|
||||||
|
int pen_y {};
|
||||||
|
int row_height {};
|
||||||
|
float px_range {};
|
||||||
|
float em_scale {};
|
||||||
|
int frame_stamp {};
|
||||||
|
|
||||||
|
unsigned units_per_em {};
|
||||||
|
float ascent {};
|
||||||
|
float descent {};
|
||||||
|
float line_gap {};
|
||||||
|
|
||||||
|
std::unordered_map<u32, GlyphCacheEntry> glyph_cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<FontRuntime>> m_font_runtime;
|
||||||
|
struct FontSet {
|
||||||
|
std::vector<usize> font_indices;
|
||||||
|
};
|
||||||
|
std::vector<FontSet> m_font_sets;
|
||||||
|
|
||||||
|
struct GlyphPlacement {
|
||||||
|
usize runtime_index {};
|
||||||
|
u32 glyph_index {};
|
||||||
|
i32 x_advance {};
|
||||||
|
i32 y_advance {};
|
||||||
|
i32 x_offset {};
|
||||||
|
i32 y_offset {};
|
||||||
|
};
|
||||||
|
|
||||||
|
static auto flush_font(FontRuntime &rt, FontData &fd) -> void;
|
||||||
|
static auto allocate_region(FontRuntime &rt, FontData &fd, int width,
|
||||||
|
int height) -> std::optional<std::pair<int, int>>;
|
||||||
|
static auto upload_region(FontData &fd, int dst_x, int dst_y, int width,
|
||||||
|
int height, std::vector<Color> const &buffer) -> void;
|
||||||
|
static auto generate_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index)
|
||||||
|
-> std::optional<GlyphCacheEntry>;
|
||||||
|
static auto ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index,
|
||||||
|
bool mark_usage) -> GlyphCacheEntry *;
|
||||||
|
auto load_single_font(std::filesystem::path const &path)
|
||||||
|
-> std::optional<usize>;
|
||||||
|
auto shape_text(FontHandle const font, std::string_view const text)
|
||||||
|
-> std::vector<GlyphPlacement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto find_font_path(std::string_view path = "sans-serif:style=Regular")
|
||||||
|
-> std::optional<std::filesystem::path>;
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
@@ -4,7 +4,11 @@
|
|||||||
|
|
||||||
#include "enum_array.hpp"
|
#include "enum_array.hpp"
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
struct ColorScheme {
|
struct ColorScheme {
|
||||||
|
Color foreground;
|
||||||
|
Color foreground_preedit;
|
||||||
struct {
|
struct {
|
||||||
Color background;
|
Color background;
|
||||||
} window;
|
} window;
|
||||||
@@ -12,24 +16,31 @@ struct ColorScheme {
|
|||||||
|
|
||||||
enum class Theme : int { Light = 0, Dark, _last = Dark };
|
enum class Theme : int { Light = 0, Dark, _last = Dark };
|
||||||
|
|
||||||
template <> struct enum_traits<Theme> {
|
template<> struct enum_traits<Theme> {
|
||||||
static constexpr Theme first = Theme::Light;
|
static constexpr Theme first = Theme::Light;
|
||||||
static constexpr Theme last = Theme::_last;
|
static constexpr Theme last = Theme::_last;
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr auto make_default_themes() -> enum_array<Theme, ColorScheme> const {
|
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_preedit = { 0, 0, 0, 255 },
|
||||||
.window =
|
.window =
|
||||||
{
|
{
|
||||||
.background = {255, 255, 255, 100},
|
.background = { 255, 255, 255, 100 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
array[Theme::Dark] = {
|
array[Theme::Dark] = {
|
||||||
|
.foreground = { 255, 255, 255, 255 },
|
||||||
|
.foreground_preedit = { 255, 255, 255, 255 },
|
||||||
.window =
|
.window =
|
||||||
{
|
{
|
||||||
.background = {0, 0, 0, 100},
|
.background = { 0, 0, 0, 100 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
88
src/Tick.cpp
Normal file
88
src/Tick.cpp
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#include "App.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include <EGL/egl.h>
|
||||||
|
#include <GLES3/gl3.h>
|
||||||
|
#include <raylib.h>
|
||||||
|
#include <rlgl.h>
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
auto App::tick() -> void
|
||||||
|
{
|
||||||
|
static std::pmr::string text_input_data {};
|
||||||
|
|
||||||
|
m_ime.bound_text = &text_input_data;
|
||||||
|
m_ime.bound_id = 1;
|
||||||
|
process_pending_text_input();
|
||||||
|
|
||||||
|
if (!m_visible || m_gl.edpy == EGL_NO_DISPLAY
|
||||||
|
|| m_gl.esurf == EGL_NO_SURFACE)
|
||||||
|
return;
|
||||||
|
|
||||||
|
glViewport(0, 0, m_win_w, m_win_h);
|
||||||
|
|
||||||
|
if (m_kbd.is_sym_pressed(XKB_KEY_Escape)) {
|
||||||
|
set_visible(!visible());
|
||||||
|
if (m_kbd.ctrl() && m_kbd.shift()) {
|
||||||
|
m_running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BeginDrawing();
|
||||||
|
|
||||||
|
ClearBackground(theme().window.background);
|
||||||
|
|
||||||
|
{
|
||||||
|
assert(m_gui);
|
||||||
|
|
||||||
|
m_gui->style().preedit_color = theme().foreground_preedit;
|
||||||
|
m_gui->style().text_color = theme().foreground;
|
||||||
|
m_gui->style().selection_color = m_accent_color;
|
||||||
|
m_gui->style().selection_text_color = WHITE;
|
||||||
|
|
||||||
|
u32 rune { 0 };
|
||||||
|
if (!m_kbd.typing.empty()) {
|
||||||
|
rune = m_kbd.typing.back();
|
||||||
|
m_kbd.typing.clear();
|
||||||
|
}
|
||||||
|
ImGuiGuard gui_scope(m_gui, rune, m_kbd.ctrl(), m_kbd.shift(),
|
||||||
|
clipboard(),
|
||||||
|
[this](std::string_view const &str) { clipboard(str); });
|
||||||
|
|
||||||
|
Rectangle const input_rect {
|
||||||
|
0.0f,
|
||||||
|
0.0f,
|
||||||
|
static_cast<float>(GetScreenWidth()),
|
||||||
|
static_cast<float>(GetScreenHeight()),
|
||||||
|
};
|
||||||
|
;
|
||||||
|
if (auto const result
|
||||||
|
= m_gui->text_input(1, text_input_data, input_rect);
|
||||||
|
result.test(1)) {
|
||||||
|
m_ime.surrounding_dirty = true;
|
||||||
|
} else if (result.test(0)) {
|
||||||
|
if (text_input_data == "kitty") {
|
||||||
|
execute_command(false, "kitty");
|
||||||
|
} else if (text_input_data == "nvim") {
|
||||||
|
execute_command(true, "nvim");
|
||||||
|
}
|
||||||
|
|
||||||
|
text_input_data = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
update_text_input_state(text_input_data, 1, input_rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawTexture(m_ir.lookup("folder", 48).texture(), 48, 48, WHITE);
|
||||||
|
|
||||||
|
EndDrawing();
|
||||||
|
|
||||||
|
eglSwapBuffers(m_gl.edpy, m_gl.esurf);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
19
src/base.vert
Normal file
19
src/base.vert
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#version 100
|
||||||
|
|
||||||
|
attribute vec3 vertexPosition;
|
||||||
|
attribute vec2 vertexTexCoord;
|
||||||
|
attribute vec3 vertexNormal;
|
||||||
|
attribute vec4 vertexColor;
|
||||||
|
|
||||||
|
uniform mat4 mvp;
|
||||||
|
|
||||||
|
varying vec2 fragTexCoord;
|
||||||
|
varying vec4 fragColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fragTexCoord = vertexTexCoord;
|
||||||
|
fragColor = vertexColor;
|
||||||
|
|
||||||
|
gl_Position = mvp*vec4(vertexPosition, 1.0);
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
#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;
|
||||||
@@ -11,10 +17,10 @@ using i64 = std::int64_t;
|
|||||||
using usize = std::uintptr_t;
|
using usize = std::uintptr_t;
|
||||||
using isize = std::intptr_t;
|
using isize = std::intptr_t;
|
||||||
|
|
||||||
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};
|
{
|
||||||
for (auto &c : utf8)
|
static std::array<char, 5> utf8 {};
|
||||||
c = 0;
|
std::fill(utf8.begin(), utf8.end(), 0);
|
||||||
|
|
||||||
if (cp < 0x80) {
|
if (cp < 0x80) {
|
||||||
utf8[0] = cp;
|
utf8[0] = cp;
|
||||||
@@ -32,5 +38,7 @@ inline auto rune_to_string(uint32_t cp) -> char const * {
|
|||||||
utf8[3] = 0x80 | (cp & 0x3F);
|
utf8[3] = 0x80 | (cp & 0x3F);
|
||||||
}
|
}
|
||||||
|
|
||||||
return utf8;
|
return utf8.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
@@ -5,69 +5,80 @@
|
|||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
template <class E> struct enum_traits;
|
#include "common.hpp"
|
||||||
|
|
||||||
template <class E>
|
namespace Waylight {
|
||||||
|
|
||||||
|
template<class E> struct enum_traits;
|
||||||
|
|
||||||
|
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 const T *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->(); }
|
||||||
constexpr const T *begin() const noexcept {
|
constexpr T const *begin() const noexcept
|
||||||
|
{
|
||||||
return _data.begin().operator->();
|
return _data.begin().operator->();
|
||||||
}
|
}
|
||||||
constexpr T *end() noexcept { return _data.end().operator->(); }
|
constexpr T *end() noexcept { return _data.end().operator->(); }
|
||||||
constexpr const T *end() const noexcept { return _data.end().operator->(); }
|
constexpr T const *end() const noexcept { return _data.end().operator->(); }
|
||||||
|
|
||||||
constexpr T &operator[](E e) noexcept { return _data[to_index(e)]; }
|
constexpr T &operator[](E e) noexcept { return _data[to_index(e)]; }
|
||||||
constexpr const T &operator[](E e) const noexcept {
|
constexpr T const &operator[](E e) const noexcept
|
||||||
|
{
|
||||||
return _data[to_index(e)];
|
return _data[to_index(e)];
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr T &at(E e) {
|
constexpr T &at(E e)
|
||||||
|
{
|
||||||
auto i = to_index(e);
|
auto i = to_index(e);
|
||||||
if (i >= size_value)
|
if (i >= size_value)
|
||||||
throw std::out_of_range("enum_array::at");
|
throw std::out_of_range("enum_array::at");
|
||||||
return _data[i];
|
return _data[i];
|
||||||
}
|
}
|
||||||
constexpr const T &at(E e) const {
|
constexpr T const &at(E e) const
|
||||||
|
{
|
||||||
auto i = to_index(e);
|
auto i = to_index(e);
|
||||||
if (i >= size_value)
|
if (i >= size_value)
|
||||||
throw std::out_of_range("enum_array::at");
|
throw std::out_of_range("enum_array::at");
|
||||||
return _data[i];
|
return _data[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr void fill(const T &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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template <class E, class T, class... U>
|
template<class E, class T, class... U>
|
||||||
requires EnumLike<E> && (std::is_same_v<T, U> && ...)
|
requires EnumLike<E> && (std::is_same_v<T, U> && ...)
|
||||||
constexpr auto make_enum_array(T &&first_val, U &&...rest) {
|
constexpr auto make_enum_array(T &&first_val, U &&...rest)
|
||||||
|
{
|
||||||
enum_array<E, std::decay_t<T>> arr;
|
enum_array<E, std::decay_t<T>> arr;
|
||||||
static_assert(sizeof...(rest) + 1 == enum_count_v<E>,
|
static_assert(sizeof...(rest) + 1 == enum_count_v<E>,
|
||||||
"initializer count must match enum range");
|
"initializer count must match enum range");
|
||||||
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
|
||||||
|
|||||||
759
src/main.cpp
759
src/main.cpp
@@ -1,718 +1,31 @@
|
|||||||
#include <cassert>
|
#include <algorithm>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <fcntl.h>
|
||||||
|
#include <iostream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <poll.h>
|
|
||||||
#include <print>
|
#include <print>
|
||||||
|
#include <signal.h>
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/types.h>
|
||||||
#include <sys/signalfd.h>
|
#include <unistd.h>
|
||||||
#include <thread>
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <EGL/egl.h>
|
#include <cpptrace/cpptrace.hpp>
|
||||||
#include <GLES3/gl3.h>
|
#include <cpptrace/from_current.hpp>
|
||||||
#include <glib.h>
|
#include <tinyfiledialogs.h>
|
||||||
#include <libportal/portal.h>
|
|
||||||
#include <raylib.h>
|
|
||||||
#include <rlgl.h>
|
|
||||||
#include <wayland-egl.h>
|
|
||||||
#include <xkbcommon/xkbcommon.h>
|
|
||||||
|
|
||||||
#define namespace namespace_
|
#include "App.hpp"
|
||||||
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
|
|
||||||
extern "C" {
|
|
||||||
#include <libportal/settings.h>
|
|
||||||
}
|
|
||||||
#undef namespace
|
|
||||||
|
|
||||||
#include "blur-client-protocol.h"
|
namespace Waylight {
|
||||||
#include "ext-background-effect-v1-client-protocol.h"
|
|
||||||
|
|
||||||
#include <wayland-client.h>
|
bool signal_running();
|
||||||
|
|
||||||
#include "Theme.hpp"
|
std::optional<App> g_app {};
|
||||||
#include "common.hpp"
|
|
||||||
|
|
||||||
struct TypingBuffer : std::pmr::vector<u32> {
|
bool signal_running()
|
||||||
void push_utf8(const char *s) {
|
{
|
||||||
for (const unsigned char *p = (const unsigned char *)s; *p;) {
|
char const *lock_path = "/tmp/waylight.lock";
|
||||||
u32 cp = 0;
|
|
||||||
int len = 0;
|
|
||||||
if (*p < 0x80) {
|
|
||||||
cp = *p++;
|
|
||||||
len = 1;
|
|
||||||
} else if ((*p & 0xE0) == 0xC0) {
|
|
||||||
cp = *p++ & 0x1F;
|
|
||||||
len = 2;
|
|
||||||
} else if ((*p & 0xF0) == 0xE0) {
|
|
||||||
cp = *p++ & 0x0F;
|
|
||||||
len = 3;
|
|
||||||
} else {
|
|
||||||
cp = *p++ & 0x07;
|
|
||||||
len = 4;
|
|
||||||
}
|
|
||||||
for (int i = 1; i < len; i++)
|
|
||||||
cp = (cp << 6) | ((*p++) & 0x3F);
|
|
||||||
push_back(cp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct App {
|
|
||||||
App() {
|
|
||||||
init_wayland();
|
|
||||||
init_egl();
|
|
||||||
init_signal();
|
|
||||||
init_theme_portal();
|
|
||||||
}
|
|
||||||
~App();
|
|
||||||
|
|
||||||
auto run() -> void {
|
|
||||||
SetWindowSize(m_win_w, m_win_h);
|
|
||||||
while (m_running) {
|
|
||||||
pump_events();
|
|
||||||
render_frame();
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auto set_visible(bool visible) -> void;
|
|
||||||
auto visible() const -> bool { return m_visible; }
|
|
||||||
|
|
||||||
auto stop() -> void { m_running = false; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
auto init_wayland() -> void;
|
|
||||||
auto init_egl() -> void;
|
|
||||||
auto init_signal() -> void;
|
|
||||||
auto init_theme_portal() -> void;
|
|
||||||
auto pump_events() -> void;
|
|
||||||
auto render_frame() -> void;
|
|
||||||
auto create_layer_surface() -> void;
|
|
||||||
auto destroy_layer_surface() -> void;
|
|
||||||
auto ensure_egl_surface() -> void;
|
|
||||||
auto update_blur_region() -> void;
|
|
||||||
auto theme() const -> ColorScheme const & { return m_themes[m_active_theme]; }
|
|
||||||
|
|
||||||
static void on_settings_changed(XdpSettings * /*self*/, const char *ns,
|
|
||||||
const char *key, GVariant * /*value*/,
|
|
||||||
gpointer data);
|
|
||||||
|
|
||||||
struct {
|
|
||||||
wl_display *display{};
|
|
||||||
wl_registry *registry{};
|
|
||||||
wl_compositor *compositor{};
|
|
||||||
wl_seat *seat{};
|
|
||||||
wl_keyboard *kbd{};
|
|
||||||
wl_surface *wl_surface{};
|
|
||||||
zwlr_layer_shell_v1 *layer_shell{};
|
|
||||||
zwlr_layer_surface_v1 *layer_surface{};
|
|
||||||
ext_background_effect_manager_v1 *mgr{};
|
|
||||||
ext_background_effect_surface_v1 *eff{};
|
|
||||||
org_kde_kwin_blur_manager *kde_blur_mgr{};
|
|
||||||
org_kde_kwin_blur *kde_blur{};
|
|
||||||
} m_wayland;
|
|
||||||
|
|
||||||
struct {
|
|
||||||
EGLDisplay edpy{EGL_NO_DISPLAY};
|
|
||||||
EGLConfig ecfg{};
|
|
||||||
EGLContext ectx{EGL_NO_CONTEXT};
|
|
||||||
EGLSurface esurf{EGL_NO_SURFACE};
|
|
||||||
wl_egl_window *wegl{};
|
|
||||||
} m_gl;
|
|
||||||
|
|
||||||
struct {
|
|
||||||
XdpPortal *portal{};
|
|
||||||
XdpSettings *settings{};
|
|
||||||
} m_xdp;
|
|
||||||
|
|
||||||
struct {
|
|
||||||
TypingBuffer typing{};
|
|
||||||
|
|
||||||
xkb_context *xkb_ctx{};
|
|
||||||
xkb_keymap *xkb_keymap{};
|
|
||||||
xkb_state *xkb_state{};
|
|
||||||
|
|
||||||
std::unordered_set<u32> held;
|
|
||||||
std::unordered_set<u32> pressed_syms;
|
|
||||||
std::unordered_set<u32> released_syms;
|
|
||||||
|
|
||||||
auto is_down_evdev(u32 evdev) const -> bool {
|
|
||||||
return held.find(evdev) != held.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto is_down_sym(xkb_keysym_t sym) const -> bool {
|
|
||||||
if (!xkb_state)
|
|
||||||
return false;
|
|
||||||
for (auto k : held) {
|
|
||||||
if (xkb_state_key_get_one_sym(xkb_state, (xkb_keycode_t)(k + 8)) == sym)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto is_sym_pressed(xkb_keysym_t sym) const -> bool {
|
|
||||||
return pressed_syms.find(sym) != pressed_syms.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto is_sym_released(xkb_keysym_t sym) const -> bool {
|
|
||||||
return released_syms.find(sym) != released_syms.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto mod_active(const char *name) const -> bool {
|
|
||||||
return xkb_state && xkb_state_mod_name_is_active(
|
|
||||||
xkb_state, name, XKB_STATE_MODS_EFFECTIVE) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ctrl() const -> bool { return mod_active("Control"); }
|
|
||||||
auto shift() const -> bool { return mod_active("Shift"); }
|
|
||||||
|
|
||||||
void clear_transients() {
|
|
||||||
pressed_syms.clear();
|
|
||||||
released_syms.clear();
|
|
||||||
}
|
|
||||||
} m_kbd;
|
|
||||||
|
|
||||||
enum_array<Theme, ColorScheme> m_themes{make_default_themes()};
|
|
||||||
Theme m_active_theme{Theme::Light};
|
|
||||||
|
|
||||||
int m_win_w{800};
|
|
||||||
int m_win_h{600};
|
|
||||||
bool m_running{true};
|
|
||||||
bool m_visible{true};
|
|
||||||
|
|
||||||
int m_sfd{-1}; // Signal fd for toggling visibility
|
|
||||||
};
|
|
||||||
|
|
||||||
App::~App() {
|
|
||||||
if (m_sfd != -1)
|
|
||||||
close(m_sfd);
|
|
||||||
|
|
||||||
destroy_layer_surface();
|
|
||||||
|
|
||||||
if (m_gl.edpy != EGL_NO_DISPLAY) {
|
|
||||||
if (m_gl.ectx != EGL_NO_CONTEXT)
|
|
||||||
eglDestroyContext(m_gl.edpy, m_gl.ectx);
|
|
||||||
eglTerminate(m_gl.edpy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_kbd.xkb_state)
|
|
||||||
xkb_state_unref(m_kbd.xkb_state);
|
|
||||||
if (m_kbd.xkb_keymap)
|
|
||||||
xkb_keymap_unref(m_kbd.xkb_keymap);
|
|
||||||
if (m_kbd.xkb_ctx)
|
|
||||||
xkb_context_unref(m_kbd.xkb_ctx);
|
|
||||||
if (m_wayland.kbd)
|
|
||||||
wl_keyboard_destroy(m_wayland.kbd);
|
|
||||||
if (m_wayland.seat)
|
|
||||||
wl_seat_destroy(m_wayland.seat);
|
|
||||||
if (m_wayland.compositor)
|
|
||||||
wl_compositor_destroy(m_wayland.compositor);
|
|
||||||
if (m_wayland.registry)
|
|
||||||
wl_registry_destroy(m_wayland.registry);
|
|
||||||
if (m_wayland.display)
|
|
||||||
wl_display_disconnect(m_wayland.display);
|
|
||||||
|
|
||||||
if (m_xdp.settings)
|
|
||||||
g_object_unref(m_xdp.settings);
|
|
||||||
if (m_xdp.portal)
|
|
||||||
g_object_unref(m_xdp.portal);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::init_wayland() -> void {
|
|
||||||
m_wayland.display = wl_display_connect(nullptr);
|
|
||||||
if (!m_wayland.display) {
|
|
||||||
std::fprintf(stderr, "failed to connect to Wayland display\n");
|
|
||||||
std::exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
static wl_keyboard_listener keyboard_listener{};
|
|
||||||
{
|
|
||||||
auto kb_keymap = [](void *data, wl_keyboard *, u32 format, i32 fd,
|
|
||||||
u32 size) -> void {
|
|
||||||
auto *app = (App *)data;
|
|
||||||
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
|
|
||||||
close(fd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void *map = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
||||||
if (map == MAP_FAILED) {
|
|
||||||
close(fd);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (app->m_kbd.xkb_keymap)
|
|
||||||
xkb_keymap_unref(app->m_kbd.xkb_keymap);
|
|
||||||
if (app->m_kbd.xkb_state) {
|
|
||||||
xkb_state_unref(app->m_kbd.xkb_state);
|
|
||||||
app->m_kbd.xkb_state = nullptr;
|
|
||||||
}
|
|
||||||
app->m_kbd.xkb_keymap = xkb_keymap_new_from_string(
|
|
||||||
app->m_kbd.xkb_ctx, (const char *)map, XKB_KEYMAP_FORMAT_TEXT_V1,
|
|
||||||
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
||||||
app->m_kbd.xkb_state = app->m_kbd.xkb_keymap
|
|
||||||
? xkb_state_new(app->m_kbd.xkb_keymap)
|
|
||||||
: nullptr;
|
|
||||||
munmap(map, size);
|
|
||||||
close(fd);
|
|
||||||
};
|
|
||||||
|
|
||||||
auto kb_enter = [](void *, wl_keyboard *, u32, wl_surface *,
|
|
||||||
wl_array *) -> void {};
|
|
||||||
auto kb_leave = [](void *data, wl_keyboard *, u32, wl_surface *) -> void {
|
|
||||||
auto *app = (App *)data;
|
|
||||||
app->m_kbd.held.clear();
|
|
||||||
};
|
|
||||||
|
|
||||||
auto kb_key = [](void *data, wl_keyboard *, u32, u32, u32 key,
|
|
||||||
u32 state) -> void {
|
|
||||||
auto *app = (App *)data;
|
|
||||||
if (!app->m_kbd.xkb_state)
|
|
||||||
return;
|
|
||||||
|
|
||||||
xkb_keycode_t kc = key + 8;
|
|
||||||
|
|
||||||
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
||||||
app->m_kbd.held.insert(key);
|
|
||||||
|
|
||||||
xkb_state_update_key(app->m_kbd.xkb_state, kc, XKB_KEY_DOWN);
|
|
||||||
xkb_keysym_t sym = xkb_state_key_get_one_sym(app->m_kbd.xkb_state, kc);
|
|
||||||
app->m_kbd.pressed_syms.insert(sym);
|
|
||||||
|
|
||||||
bool ctrl = app->m_kbd.mod_active("Control");
|
|
||||||
bool alt = app->m_kbd.mod_active("Mod1");
|
|
||||||
bool meta = app->m_kbd.mod_active("Mod4");
|
|
||||||
if (!(ctrl || alt || meta)) {
|
|
||||||
u32 cp = xkb_keysym_to_utf32(sym);
|
|
||||||
if (cp >= 0x20) {
|
|
||||||
char buf[8];
|
|
||||||
int n = xkb_keysym_to_utf8(sym, buf, sizeof(buf));
|
|
||||||
if (n > 0)
|
|
||||||
app->m_kbd.typing.push_utf8(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
xkb_keysym_t sym = xkb_state_key_get_one_sym(app->m_kbd.xkb_state, kc);
|
|
||||||
app->m_kbd.released_syms.insert(sym);
|
|
||||||
|
|
||||||
app->m_kbd.held.erase(key);
|
|
||||||
xkb_state_update_key(app->m_kbd.xkb_state, kc, XKB_KEY_UP);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
auto kb_mods = [](void *data, wl_keyboard *, u32, u32 depressed,
|
|
||||||
u32 latched, u32 locked, u32 group) -> void {
|
|
||||||
auto *app = (App *)data;
|
|
||||||
if (!app->m_kbd.xkb_state)
|
|
||||||
return;
|
|
||||||
xkb_state_update_mask(app->m_kbd.xkb_state, depressed, latched, locked, 0,
|
|
||||||
0, group);
|
|
||||||
};
|
|
||||||
|
|
||||||
auto kb_repeat_info = [](void *, wl_keyboard *, i32, i32) -> void {};
|
|
||||||
|
|
||||||
keyboard_listener = {kb_keymap, kb_enter, kb_leave,
|
|
||||||
kb_key, kb_mods, kb_repeat_info};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto handle_registry_global = [](void *data, wl_registry *registry, u32 name,
|
|
||||||
const char *interface, u32 version) -> void {
|
|
||||||
auto *app = static_cast<App *>(data);
|
|
||||||
if (std::strcmp(interface, wl_compositor_interface.name) == 0) {
|
|
||||||
app->m_wayland.compositor = static_cast<wl_compositor *>(
|
|
||||||
wl_registry_bind(registry, name, &wl_compositor_interface, 4));
|
|
||||||
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
|
|
||||||
app->m_wayland.seat = static_cast<wl_seat *>(
|
|
||||||
wl_registry_bind(registry, name, &wl_seat_interface, 9));
|
|
||||||
static struct wl_seat_listener const seat_listener = {
|
|
||||||
.capabilities =
|
|
||||||
[](void *data, struct wl_seat *seat, u32 caps) {
|
|
||||||
auto *app = static_cast<App *>(data);
|
|
||||||
if (caps & WL_SEAT_CAPABILITY_KEYBOARD) {
|
|
||||||
app->m_wayland.kbd = wl_seat_get_keyboard(seat);
|
|
||||||
wl_keyboard_add_listener(app->m_wayland.kbd,
|
|
||||||
&keyboard_listener, data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.name = [](void *data, struct wl_seat *wl_seat, const char *name) {},
|
|
||||||
};
|
|
||||||
wl_seat_add_listener(app->m_wayland.seat, &seat_listener, data);
|
|
||||||
} else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) {
|
|
||||||
app->m_wayland.layer_shell = (zwlr_layer_shell_v1 *)wl_registry_bind(
|
|
||||||
registry, name, &zwlr_layer_shell_v1_interface,
|
|
||||||
version >= 4 ? 4 : version);
|
|
||||||
} else if (strcmp(interface,
|
|
||||||
ext_background_effect_manager_v1_interface.name) == 0) {
|
|
||||||
app->m_wayland.mgr = (ext_background_effect_manager_v1 *)wl_registry_bind(
|
|
||||||
registry, name, &ext_background_effect_manager_v1_interface, 1);
|
|
||||||
} else if (strcmp(interface, "org_kde_kwin_blur_manager") == 0) {
|
|
||||||
app->m_wayland.kde_blur_mgr =
|
|
||||||
(org_kde_kwin_blur_manager *)wl_registry_bind(
|
|
||||||
registry, name, &org_kde_kwin_blur_manager_interface, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static wl_registry_listener const registry_listener{
|
|
||||||
.global = handle_registry_global,
|
|
||||||
.global_remove = [](void *, wl_registry *, u32) {},
|
|
||||||
};
|
|
||||||
|
|
||||||
m_wayland.registry = wl_display_get_registry(m_wayland.display);
|
|
||||||
wl_registry_add_listener(m_wayland.registry, ®istry_listener, this);
|
|
||||||
|
|
||||||
m_kbd.xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
||||||
|
|
||||||
wl_display_roundtrip(m_wayland.display);
|
|
||||||
|
|
||||||
create_layer_surface();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::set_visible(bool visible) -> void {
|
|
||||||
if (visible == m_visible)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (visible) {
|
|
||||||
create_layer_surface();
|
|
||||||
ensure_egl_surface();
|
|
||||||
} else {
|
|
||||||
destroy_layer_surface();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_wayland.display)
|
|
||||||
wl_display_flush(m_wayland.display);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::init_egl() -> void {
|
|
||||||
m_gl.edpy =
|
|
||||||
eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_wayland.display));
|
|
||||||
eglInitialize(m_gl.edpy, nullptr, nullptr);
|
|
||||||
|
|
||||||
const EGLint cfgAttribs[]{
|
|
||||||
EGL_SURFACE_TYPE,
|
|
||||||
EGL_WINDOW_BIT,
|
|
||||||
EGL_RENDERABLE_TYPE,
|
|
||||||
EGL_OPENGL_ES2_BIT,
|
|
||||||
EGL_RED_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_GREEN_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_BLUE_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_ALPHA_SIZE,
|
|
||||||
8,
|
|
||||||
EGL_NONE,
|
|
||||||
};
|
|
||||||
EGLint n = 0;
|
|
||||||
eglChooseConfig(m_gl.edpy, cfgAttribs, &m_gl.ecfg, 1, &n);
|
|
||||||
|
|
||||||
const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 2, EGL_NONE};
|
|
||||||
m_gl.ectx =
|
|
||||||
eglCreateContext(m_gl.edpy, m_gl.ecfg, EGL_NO_CONTEXT, ctxAttribs);
|
|
||||||
|
|
||||||
ensure_egl_surface();
|
|
||||||
|
|
||||||
InitWindow(m_win_w, m_win_h, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::init_signal() -> void {
|
|
||||||
sigset_t mask;
|
|
||||||
sigemptyset(&mask);
|
|
||||||
sigaddset(&mask, SIGUSR1);
|
|
||||||
|
|
||||||
if (pthread_sigmask(SIG_BLOCK, &mask, nullptr) != 0) {
|
|
||||||
std::perror("pthread_sigmask");
|
|
||||||
std::exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
|
|
||||||
if (m_sfd == -1) {
|
|
||||||
std::perror("signalfd");
|
|
||||||
std::exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void App::on_settings_changed(XdpSettings * /*self*/, const char *ns,
|
|
||||||
const char *key, GVariant * /*value*/,
|
|
||||||
gpointer data) {
|
|
||||||
auto *app = static_cast<App *>(data);
|
|
||||||
if (g_strcmp0(ns, "org.freedesktop.appearance") == 0 &&
|
|
||||||
g_strcmp0(key, "color-scheme") == 0) {
|
|
||||||
guint v = xdp_settings_read_uint(app->m_xdp.settings,
|
|
||||||
"org.freedesktop.appearance",
|
|
||||||
"color-scheme", NULL, NULL);
|
|
||||||
|
|
||||||
if (v == 1)
|
|
||||||
app->m_active_theme = Theme::Dark;
|
|
||||||
else
|
|
||||||
app->m_active_theme = Theme::Light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::init_theme_portal() -> void {
|
|
||||||
m_xdp.portal = xdp_portal_new();
|
|
||||||
m_xdp.settings = xdp_portal_get_settings(m_xdp.portal);
|
|
||||||
|
|
||||||
guint v = xdp_settings_read_uint(m_xdp.settings, "org.freedesktop.appearance",
|
|
||||||
"color-scheme", NULL, NULL);
|
|
||||||
if (v == 1)
|
|
||||||
m_active_theme = Theme::Dark;
|
|
||||||
else
|
|
||||||
m_active_theme = Theme::Light;
|
|
||||||
|
|
||||||
g_signal_connect(m_xdp.settings, "changed", G_CALLBACK(on_settings_changed),
|
|
||||||
this);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::render_frame() -> void {
|
|
||||||
if (!m_visible || m_gl.edpy == EGL_NO_DISPLAY || m_gl.esurf == EGL_NO_SURFACE)
|
|
||||||
return;
|
|
||||||
|
|
||||||
glViewport(0, 0, m_win_w, m_win_h);
|
|
||||||
|
|
||||||
for (auto const cp : m_kbd.typing) {
|
|
||||||
std::println("Char typed: {} ({}) shift={} ctrl={}", rune_to_string(cp),
|
|
||||||
cp, m_kbd.shift() ? 'y' : 'n', m_kbd.ctrl() ? 'y' : 'n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_kbd.is_sym_pressed(XKB_KEY_Escape)) {
|
|
||||||
set_visible(!visible());
|
|
||||||
if (m_kbd.ctrl() && m_kbd.shift()) {
|
|
||||||
m_running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BeginDrawing();
|
|
||||||
|
|
||||||
ClearBackground(BLANK);
|
|
||||||
|
|
||||||
DrawFPS(10, 10);
|
|
||||||
|
|
||||||
EndDrawing();
|
|
||||||
|
|
||||||
eglSwapBuffers(m_gl.edpy, m_gl.esurf);
|
|
||||||
m_kbd.typing.clear();
|
|
||||||
m_kbd.clear_transients();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::create_layer_surface() -> void {
|
|
||||||
if (m_wayland.layer_surface)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!m_wayland.compositor || !m_wayland.layer_shell)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_wayland.wl_surface = wl_compositor_create_surface(m_wayland.compositor);
|
|
||||||
|
|
||||||
if (m_wayland.mgr) {
|
|
||||||
m_wayland.eff = ext_background_effect_manager_v1_get_background_effect(
|
|
||||||
m_wayland.mgr, m_wayland.wl_surface);
|
|
||||||
}
|
|
||||||
if (m_wayland.kde_blur_mgr) {
|
|
||||||
m_wayland.kde_blur = org_kde_kwin_blur_manager_create(
|
|
||||||
m_wayland.kde_blur_mgr, m_wayland.wl_surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_wayland.layer_surface = zwlr_layer_shell_v1_get_layer_surface(
|
|
||||||
m_wayland.layer_shell, m_wayland.wl_surface, nullptr,
|
|
||||||
ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "waylight-overlay");
|
|
||||||
|
|
||||||
if (!m_wayland.layer_surface) {
|
|
||||||
if (m_wayland.eff) {
|
|
||||||
ext_background_effect_surface_v1_destroy(m_wayland.eff);
|
|
||||||
m_wayland.eff = nullptr;
|
|
||||||
}
|
|
||||||
if (m_wayland.kde_blur) {
|
|
||||||
org_kde_kwin_blur_destroy(m_wayland.kde_blur);
|
|
||||||
m_wayland.kde_blur = nullptr;
|
|
||||||
}
|
|
||||||
if (m_wayland.wl_surface) {
|
|
||||||
wl_surface_destroy(m_wayland.wl_surface);
|
|
||||||
m_wayland.wl_surface = nullptr;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
zwlr_layer_surface_v1_set_anchor(m_wayland.layer_surface, 0);
|
|
||||||
zwlr_layer_surface_v1_set_size(m_wayland.layer_surface, m_win_w, m_win_h);
|
|
||||||
zwlr_layer_surface_v1_set_exclusive_zone(m_wayland.layer_surface, 0);
|
|
||||||
|
|
||||||
if (zwlr_layer_shell_v1_get_version(m_wayland.layer_shell) >= 3) {
|
|
||||||
zwlr_layer_surface_v1_set_keyboard_interactivity(
|
|
||||||
m_wayland.layer_surface,
|
|
||||||
ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto handle_layer_configure = [](void *data, zwlr_layer_surface_v1 *ls,
|
|
||||||
u32 serial, u32 w, u32 h) -> void {
|
|
||||||
auto *app = static_cast<App *>(data);
|
|
||||||
if (w)
|
|
||||||
app->m_win_w = static_cast<int>(w);
|
|
||||||
if (h)
|
|
||||||
app->m_win_h = static_cast<int>(h);
|
|
||||||
|
|
||||||
zwlr_layer_surface_v1_ack_configure(ls, serial);
|
|
||||||
|
|
||||||
if (app->m_gl.edpy != EGL_NO_DISPLAY) {
|
|
||||||
if (!app->m_gl.wegl || app->m_gl.esurf == EGL_NO_SURFACE) {
|
|
||||||
app->ensure_egl_surface();
|
|
||||||
} else {
|
|
||||||
wl_egl_window_resize(app->m_gl.wegl, app->m_win_w, app->m_win_h, 0, 0);
|
|
||||||
eglMakeCurrent(app->m_gl.edpy, app->m_gl.esurf, app->m_gl.esurf,
|
|
||||||
app->m_gl.ectx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app->update_blur_region();
|
|
||||||
|
|
||||||
if (app->m_wayland.wl_surface)
|
|
||||||
wl_surface_commit(app->m_wayland.wl_surface);
|
|
||||||
};
|
|
||||||
|
|
||||||
auto handle_layer_closed = [](void *data, zwlr_layer_surface_v1 *) -> void {
|
|
||||||
static_cast<App *>(data)->m_running = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
static const zwlr_layer_surface_v1_listener lsl = {
|
|
||||||
.configure = handle_layer_configure,
|
|
||||||
.closed = handle_layer_closed,
|
|
||||||
};
|
|
||||||
|
|
||||||
zwlr_layer_surface_v1_add_listener(m_wayland.layer_surface, &lsl, this);
|
|
||||||
|
|
||||||
update_blur_region();
|
|
||||||
|
|
||||||
if (m_wayland.wl_surface)
|
|
||||||
wl_surface_commit(m_wayland.wl_surface);
|
|
||||||
|
|
||||||
if (m_wayland.display)
|
|
||||||
wl_display_roundtrip(m_wayland.display);
|
|
||||||
|
|
||||||
ensure_egl_surface();
|
|
||||||
|
|
||||||
m_visible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::destroy_layer_surface() -> void {
|
|
||||||
if (m_gl.edpy != EGL_NO_DISPLAY && m_gl.esurf != EGL_NO_SURFACE) {
|
|
||||||
eglMakeCurrent(m_gl.edpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
||||||
eglDestroySurface(m_gl.edpy, m_gl.esurf);
|
|
||||||
m_gl.esurf = EGL_NO_SURFACE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_gl.wegl) {
|
|
||||||
wl_egl_window_destroy(m_gl.wegl);
|
|
||||||
m_gl.wegl = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_wayland.eff) {
|
|
||||||
ext_background_effect_surface_v1_destroy(m_wayland.eff);
|
|
||||||
m_wayland.eff = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_wayland.kde_blur) {
|
|
||||||
org_kde_kwin_blur_destroy(m_wayland.kde_blur);
|
|
||||||
m_wayland.kde_blur = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_wayland.layer_surface) {
|
|
||||||
zwlr_layer_surface_v1_destroy(m_wayland.layer_surface);
|
|
||||||
m_wayland.layer_surface = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_wayland.wl_surface) {
|
|
||||||
wl_surface_destroy(m_wayland.wl_surface);
|
|
||||||
m_wayland.wl_surface = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_wayland.display)
|
|
||||||
wl_display_flush(m_wayland.display);
|
|
||||||
|
|
||||||
m_visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::ensure_egl_surface() -> void {
|
|
||||||
if (m_gl.edpy == EGL_NO_DISPLAY || m_gl.ectx == EGL_NO_CONTEXT)
|
|
||||||
return;
|
|
||||||
if (!m_wayland.wl_surface)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!m_gl.wegl)
|
|
||||||
m_gl.wegl = wl_egl_window_create(m_wayland.wl_surface, m_win_w, m_win_h);
|
|
||||||
|
|
||||||
if (!m_gl.wegl)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (m_gl.esurf == EGL_NO_SURFACE) {
|
|
||||||
m_gl.esurf = eglCreateWindowSurface(
|
|
||||||
m_gl.edpy, m_gl.ecfg, reinterpret_cast<EGLNativeWindowType>(m_gl.wegl),
|
|
||||||
nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_gl.esurf == EGL_NO_SURFACE)
|
|
||||||
return;
|
|
||||||
|
|
||||||
eglMakeCurrent(m_gl.edpy, m_gl.esurf, m_gl.esurf, m_gl.ectx);
|
|
||||||
eglSwapInterval(m_gl.edpy, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::update_blur_region() -> void {
|
|
||||||
if (!m_wayland.compositor)
|
|
||||||
return;
|
|
||||||
if (!m_wayland.eff && !m_wayland.kde_blur)
|
|
||||||
return;
|
|
||||||
|
|
||||||
wl_region *region = wl_compositor_create_region(m_wayland.compositor);
|
|
||||||
if (!region)
|
|
||||||
return;
|
|
||||||
|
|
||||||
wl_region_add(region, 0, 0, m_win_w - 50, m_win_h);
|
|
||||||
|
|
||||||
if (m_wayland.eff)
|
|
||||||
ext_background_effect_surface_v1_set_blur_region(m_wayland.eff, region);
|
|
||||||
if (m_wayland.kde_blur)
|
|
||||||
org_kde_kwin_blur_set_region(m_wayland.kde_blur, region);
|
|
||||||
|
|
||||||
wl_region_destroy(region);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::pump_events() -> void {
|
|
||||||
while (g_main_context_iteration(nullptr, false))
|
|
||||||
;
|
|
||||||
|
|
||||||
wl_display_dispatch_pending(m_wayland.display);
|
|
||||||
wl_display_flush(m_wayland.display);
|
|
||||||
|
|
||||||
pollfd fds[2]{{wl_display_get_fd(m_wayland.display), POLLIN, 0},
|
|
||||||
{m_sfd, POLLIN, 0}};
|
|
||||||
|
|
||||||
auto prepared = (wl_display_prepare_read(m_wayland.display) == 0);
|
|
||||||
auto ret = poll(fds, 2, 0);
|
|
||||||
|
|
||||||
// Wayland
|
|
||||||
if (ret > 0 && (fds[0].revents & POLLIN)) {
|
|
||||||
if (prepared) {
|
|
||||||
wl_display_read_events(m_wayland.display);
|
|
||||||
prepared = false;
|
|
||||||
}
|
|
||||||
} else if (prepared) {
|
|
||||||
wl_display_cancel_read(m_wayland.display);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signals
|
|
||||||
if (ret > 0 && (fds[1].revents & POLLIN)) {
|
|
||||||
signalfd_siginfo si;
|
|
||||||
while (read(m_sfd, &si, sizeof(si)) == sizeof(si)) {
|
|
||||||
if (si.ssi_signo == SIGUSR1) {
|
|
||||||
set_visible(!visible());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -729,23 +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;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<App> g_app{};
|
} // namespace Waylight
|
||||||
|
|
||||||
auto main() -> int {
|
auto main() -> int
|
||||||
if (check_or_signal_running()) {
|
{
|
||||||
|
if (Waylight::signal_running()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::signal(SIGINT, [](int) {
|
std::signal(SIGINT, [](int) {
|
||||||
if (g_app)
|
if (Waylight::g_app)
|
||||||
g_app->stop();
|
Waylight::g_app->stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
g_app.emplace();
|
CPPTRACE_TRY
|
||||||
g_app->run();
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/msdf.frag
Normal file
34
src/msdf.frag
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#version 100
|
||||||
|
#extension GL_OES_standard_derivatives : enable
|
||||||
|
|
||||||
|
#ifdef GL_ES
|
||||||
|
precision mediump float;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
varying vec2 fragTexCoord;
|
||||||
|
varying vec4 fragColor;
|
||||||
|
|
||||||
|
uniform sampler2D texture0;
|
||||||
|
uniform vec4 colDiffuse;
|
||||||
|
uniform float pxRange;
|
||||||
|
|
||||||
|
float median(float r, float g, float b) {
|
||||||
|
return max(min(r, g), min(max(r, g), b));
|
||||||
|
}
|
||||||
|
|
||||||
|
float screenPxRange(vec2 uv) {
|
||||||
|
vec2 duv_dx = dFdx(uv);
|
||||||
|
vec2 duv_dy = dFdy(uv);
|
||||||
|
float scale = 0.5 * (length(duv_dx) + length(duv_dy));
|
||||||
|
return max(scale * pxRange, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 msd = texture2D(texture0, fragTexCoord).rgb;
|
||||||
|
float sd = median(msd.r, msd.g, msd.b);
|
||||||
|
float spx = screenPxRange(fragTexCoord);
|
||||||
|
float dist = spx * (sd - 0.5);
|
||||||
|
float opacity = clamp(dist + 0.5, 0.0, 1.0);
|
||||||
|
|
||||||
|
gl_FragColor = vec4(fragColor.rgb, fragColor.a * opacity) * colDiffuse;
|
||||||
|
}
|
||||||
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
|
||||||
|
|
||||||
8
tools/format.sh
Executable file
8
tools/format.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
find "$ROOT/src" -type f -name '*.cpp' -o -name '*.hpp' -print0 | while IFS= read -r -d '' f; do
|
||||||
|
clang-format -i --style=file "$f"
|
||||||
|
done
|
||||||
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