Compare commits
26 Commits
18614ccee9
...
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 |
@@ -1,10 +1,25 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
project(waylight LANGUAGES C CXX)
|
project(waylight LANGUAGES C CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 23)
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
|
find_program(cppcheck_exe NAMES cppcheck)
|
||||||
|
if (cppcheck_exe)
|
||||||
|
set(cppcheck_opts --enable=all --inline-suppr --quiet --suppressions-list=${PROJECT_SOURCE_DIR}/cppcheck.supp)
|
||||||
|
add_custom_target(run_cppcheck
|
||||||
|
COMMAND ${cppcheck_exe}
|
||||||
|
--std=c++20 --enable=all --inline-suppr --quiet
|
||||||
|
--suppressions-list=${PROJECT_SOURCE_DIR}/cppcheck.supp
|
||||||
|
--project=${CMAKE_BINARY_DIR}/compile_commands.json
|
||||||
|
--check-level=exhaustive
|
||||||
|
-i ${CMAKE_BINARY_DIR}
|
||||||
|
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
|
pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
|
||||||
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
||||||
@@ -44,6 +59,48 @@ set(MSDFGEN_DISABLE_SVG ON)
|
|||||||
set(MSDFGEN_DISABLE_PNG ON)
|
set(MSDFGEN_DISABLE_PNG ON)
|
||||||
FetchContent_MakeAvailable(msdfgen)
|
FetchContent_MakeAvailable(msdfgen)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
mINI
|
||||||
|
GIT_REPOSITORY https://github.com/metayeti/mINI.git
|
||||||
|
GIT_TAG "0.9.18"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(mINI)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
lunasvg
|
||||||
|
GIT_REPOSITORY https://github.com/sammycage/lunasvg.git
|
||||||
|
GIT_TAG "v3.5.0"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(lunasvg)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
SQLiteCpp
|
||||||
|
GIT_REPOSITORY https://github.com/SRombauts/SQLiteCpp.git
|
||||||
|
GIT_TAG "3.3.3"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(SQLiteCpp)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
cpptrace
|
||||||
|
GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
|
||||||
|
GIT_TAG "v1.0.1"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(cpptrace)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
tomlplusplus
|
||||||
|
GIT_REPOSITORY https://github.com/marzer/tomlplusplus
|
||||||
|
GIT_TAG "v3.4.0"
|
||||||
|
GIT_SHALLOW 1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(tomlplusplus)
|
||||||
|
|
||||||
|
add_subdirectory(vendor)
|
||||||
|
|
||||||
find_program(WAYLAND_SCANNER wayland-scanner REQUIRED)
|
find_program(WAYLAND_SCANNER wayland-scanner REQUIRED)
|
||||||
|
|
||||||
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
|
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
|
||||||
@@ -93,17 +150,23 @@ set(WLR_LAYER_SHELL_XML
|
|||||||
"${WLR_PROTOCOLS_DIR}/unstable/wlr-layer-shell-unstable-v1.xml"
|
"${WLR_PROTOCOLS_DIR}/unstable/wlr-layer-shell-unstable-v1.xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(TEXT_INPUT_XML
|
||||||
|
"${WAYLAND_PROTOCOLS_DIR}/unstable/text-input/text-input-unstable-v3.xml"
|
||||||
|
)
|
||||||
|
|
||||||
set(GEN_C_HEADERS
|
set(GEN_C_HEADERS
|
||||||
"${GEN_DIR}/xdg-shell-client-protocol.h"
|
"${GEN_DIR}/xdg-shell-client-protocol.h"
|
||||||
"${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h"
|
"${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h"
|
||||||
"${GEN_DIR}/ext-background-effect-v1-client-protocol.h"
|
"${GEN_DIR}/ext-background-effect-v1-client-protocol.h"
|
||||||
"${GEN_DIR}/blur-client-protocol.h"
|
"${GEN_DIR}/blur-client-protocol.h"
|
||||||
|
"${GEN_DIR}/text-input-unstable-v3-client-protocol.h"
|
||||||
)
|
)
|
||||||
set(GEN_C_PRIVATES
|
set(GEN_C_PRIVATES
|
||||||
"${GEN_DIR}/xdg-shell-protocol.c"
|
"${GEN_DIR}/xdg-shell-protocol.c"
|
||||||
"${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c"
|
"${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c"
|
||||||
"${GEN_DIR}/ext-background-effect-v1-protocol.c"
|
"${GEN_DIR}/ext-background-effect-v1-protocol.c"
|
||||||
"${GEN_DIR}/blur-protocol.c"
|
"${GEN_DIR}/blur-protocol.c"
|
||||||
|
"${GEN_DIR}/text-input-unstable-v3-protocol.c"
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
@@ -116,10 +179,13 @@ add_custom_command(
|
|||||||
# wlr-layer-shell
|
# wlr-layer-shell
|
||||||
COMMAND "${WAYLAND_SCANNER}" client-header "${WLR_LAYER_SHELL_XML}" "${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h"
|
COMMAND "${WAYLAND_SCANNER}" client-header "${WLR_LAYER_SHELL_XML}" "${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h"
|
||||||
COMMAND "${WAYLAND_SCANNER}" private-code "${WLR_LAYER_SHELL_XML}" "${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c"
|
COMMAND "${WAYLAND_SCANNER}" private-code "${WLR_LAYER_SHELL_XML}" "${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c"
|
||||||
|
# text-input-unstable-v3
|
||||||
|
COMMAND "${WAYLAND_SCANNER}" client-header "${TEXT_INPUT_XML}" "${GEN_DIR}/text-input-unstable-v3-client-protocol.h"
|
||||||
|
COMMAND "${WAYLAND_SCANNER}" private-code "${TEXT_INPUT_XML}" "${GEN_DIR}/text-input-unstable-v3-protocol.c"
|
||||||
# org-kde-win-blur
|
# org-kde-win-blur
|
||||||
COMMAND "${WAYLAND_SCANNER}" client-header "${CMAKE_CURRENT_SOURCE_DIR}/blur.xml" "${GEN_DIR}/blur-client-protocol.h"
|
COMMAND "${WAYLAND_SCANNER}" client-header "${CMAKE_CURRENT_SOURCE_DIR}/blur.xml" "${GEN_DIR}/blur-client-protocol.h"
|
||||||
COMMAND "${WAYLAND_SCANNER}" private-code "${CMAKE_CURRENT_SOURCE_DIR}/blur.xml" "${GEN_DIR}/blur-protocol.c"
|
COMMAND "${WAYLAND_SCANNER}" private-code "${CMAKE_CURRENT_SOURCE_DIR}/blur.xml" "${GEN_DIR}/blur-protocol.c"
|
||||||
DEPENDS "${XDG_SHELL_XML}" "${WLR_LAYER_SHELL_XML}"
|
DEPENDS "${XDG_SHELL_XML}" "${WLR_LAYER_SHELL_XML}" "${TEXT_INPUT_XML}"
|
||||||
COMMENT "Generating Wayland + wlr-layer-shell client headers and private code"
|
COMMENT "Generating Wayland + wlr-layer-shell client headers and private code"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
@@ -131,6 +197,10 @@ 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/TextRenderer.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/ImGui.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/ImGui.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/App.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/App.cpp
|
||||||
@@ -154,9 +224,15 @@ target_link_libraries(waylight PRIVATE
|
|||||||
PkgConfig::FONTCONFIG
|
PkgConfig::FONTCONFIG
|
||||||
PkgConfig::HARFBUZZ
|
PkgConfig::HARFBUZZ
|
||||||
|
|
||||||
|
tomlplusplus::tomlplusplus
|
||||||
|
cpptrace::cpptrace
|
||||||
|
tinyfiledialogs
|
||||||
|
mINI
|
||||||
raylib
|
raylib
|
||||||
msdfgen::msdfgen-core
|
msdfgen::msdfgen-core
|
||||||
msdfgen::msdfgen-ext
|
msdfgen::msdfgen-ext
|
||||||
|
lunasvg::lunasvg
|
||||||
|
SQLiteCpp
|
||||||
|
|
||||||
m
|
m
|
||||||
dl
|
dl
|
||||||
|
|||||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
8
cppcheck.supp
Normal file
8
cppcheck.supp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
unusedFunction
|
||||||
|
shadowFunction
|
||||||
|
missingIncludeSystem
|
||||||
|
ignoredReturnValue
|
||||||
|
|
||||||
|
*:build/generated/*
|
||||||
|
*:build/_deps/*
|
||||||
|
*:vendor/*
|
||||||
@@ -28,6 +28,9 @@
|
|||||||
libxkbcommon
|
libxkbcommon
|
||||||
fontconfig
|
fontconfig
|
||||||
harfbuzz
|
harfbuzz
|
||||||
|
sqlite
|
||||||
|
zenity
|
||||||
|
boost
|
||||||
];
|
];
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
cmake
|
cmake
|
||||||
|
|||||||
755
src/App.cpp
755
src/App.cpp
@@ -1,12 +1,16 @@
|
|||||||
#include "App.hpp"
|
#include "App.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
|
#include <print>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include <ranges>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
@@ -30,6 +34,84 @@
|
|||||||
#include "blur-client-protocol.h"
|
#include "blur-client-protocol.h"
|
||||||
#include "ext-background-effect-v1-client-protocol.h"
|
#include "ext-background-effect-v1-client-protocol.h"
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr usize MAX_SURROUNDING_BYTES = 4000;
|
||||||
|
|
||||||
|
inline auto is_utf8_continuation(char c) -> bool
|
||||||
|
{
|
||||||
|
return (static_cast<unsigned char>(c) & 0xC0) == 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto adjust_utf8_backward(std::string const &text, int index) -> int
|
||||||
|
{
|
||||||
|
index = std::clamp(index, 0, static_cast<int>(text.size()));
|
||||||
|
while (
|
||||||
|
index > 0 && is_utf8_continuation(text[static_cast<usize>(index - 1)]))
|
||||||
|
--index;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto adjust_utf8_forward(std::string const &text, int index) -> int
|
||||||
|
{
|
||||||
|
int const size = static_cast<int>(text.size());
|
||||||
|
index = std::clamp(index, 0, size);
|
||||||
|
while (
|
||||||
|
index < size && is_utf8_continuation(text[static_cast<usize>(index)]))
|
||||||
|
++index;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SurroundingSlice {
|
||||||
|
std::string text;
|
||||||
|
int cursor { 0 };
|
||||||
|
int anchor { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
auto clamp_surrounding_text(std::string const &text, int cursor, int anchor)
|
||||||
|
-> SurroundingSlice
|
||||||
|
{
|
||||||
|
int const size = static_cast<int>(text.size());
|
||||||
|
cursor = std::clamp(cursor, 0, size);
|
||||||
|
anchor = std::clamp(anchor, 0, size);
|
||||||
|
|
||||||
|
if (text.size() <= MAX_SURROUNDING_BYTES) {
|
||||||
|
return SurroundingSlice { text, cursor, anchor };
|
||||||
|
}
|
||||||
|
|
||||||
|
int window_start = std::max(0,
|
||||||
|
std::min(cursor, anchor) - static_cast<int>(MAX_SURROUNDING_BYTES / 2));
|
||||||
|
int window_end = window_start + static_cast<int>(MAX_SURROUNDING_BYTES);
|
||||||
|
int const max_pos = std::max(cursor, anchor);
|
||||||
|
if (window_end < max_pos) {
|
||||||
|
window_end = max_pos;
|
||||||
|
window_start
|
||||||
|
= std::max(0, window_end - static_cast<int>(MAX_SURROUNDING_BYTES));
|
||||||
|
}
|
||||||
|
if (window_end > size)
|
||||||
|
window_end = size;
|
||||||
|
if (window_end - window_start > static_cast<int>(MAX_SURROUNDING_BYTES)) {
|
||||||
|
window_start = window_end - static_cast<int>(MAX_SURROUNDING_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
window_start = adjust_utf8_backward(text, window_start);
|
||||||
|
window_end = adjust_utf8_forward(text, window_end);
|
||||||
|
if (window_end < window_start)
|
||||||
|
window_end = window_start;
|
||||||
|
|
||||||
|
std::string slice(text.begin() + window_start, text.begin() + window_end);
|
||||||
|
int const new_cursor
|
||||||
|
= std::clamp(cursor - window_start, 0, static_cast<int>(slice.size()));
|
||||||
|
int const new_anchor
|
||||||
|
= std::clamp(anchor - window_start, 0, static_cast<int>(slice.size()));
|
||||||
|
|
||||||
|
return SurroundingSlice { std::move(slice), new_cursor, new_anchor };
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
auto TypingBuffer::push_utf8(char const *s) -> void
|
auto TypingBuffer::push_utf8(char const *s) -> void
|
||||||
{
|
{
|
||||||
for (unsigned char const *p = reinterpret_cast<unsigned char const *>(s);
|
for (unsigned char const *p = reinterpret_cast<unsigned char const *>(s);
|
||||||
@@ -61,6 +143,73 @@ App::App()
|
|||||||
init_egl();
|
init_egl();
|
||||||
init_signal();
|
init_signal();
|
||||||
init_theme_portal();
|
init_theme_portal();
|
||||||
|
|
||||||
|
{
|
||||||
|
auto const env = getenv("XDG_DATA_HOME");
|
||||||
|
if (env && *env) {
|
||||||
|
if (std::filesystem::exists(env)) {
|
||||||
|
m_data_home_dir = env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_data_home_dir.empty()) {
|
||||||
|
auto const home = getenv("HOME");
|
||||||
|
assert(home && *home);
|
||||||
|
m_data_home_dir = std::filesystem::path(home) / ".local" / "share";
|
||||||
|
std::filesystem::create_directories(m_data_home_dir);
|
||||||
|
}
|
||||||
|
m_data_home_dir /= "waylight";
|
||||||
|
std::filesystem::create_directories(m_data_home_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto const env = getenv("XDG_CONFIG_HOME");
|
||||||
|
if (env && *env) {
|
||||||
|
if (std::filesystem::exists(env)) {
|
||||||
|
m_config_home_dir = env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_config_home_dir.empty()) {
|
||||||
|
auto const home = getenv("HOME");
|
||||||
|
assert(home && *home);
|
||||||
|
m_config_home_dir = std::filesystem::path(home) / ".config";
|
||||||
|
std::filesystem::create_directories(m_config_home_dir);
|
||||||
|
}
|
||||||
|
m_config_home_dir /= "waylight";
|
||||||
|
std::filesystem::create_directories(m_config_home_dir);
|
||||||
|
|
||||||
|
m_config = Config::load(m_config_home_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_db = std::make_shared<SQLite::Database>(m_data_home_dir / "data.db",
|
||||||
|
SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
|
||||||
|
|
||||||
|
SQLite::Statement(*m_db, R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS ApplicationCache (
|
||||||
|
id INTEGER PRIMARY KEY NOT NULL,
|
||||||
|
type INTEGER NOT NULL,
|
||||||
|
desktop_entry_path TEXT NOT NULL,
|
||||||
|
terminal BOOL NOT NULL,
|
||||||
|
no_display BOOL NOT NULL,
|
||||||
|
path TEXT,
|
||||||
|
comment TEXT,
|
||||||
|
dbus_activatable BOOL NOT NULL
|
||||||
|
)
|
||||||
|
)")
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
SQLite::Statement(*m_db, R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS ApplicationActionCache (
|
||||||
|
id INTEGER PRIMARY KEY NOT NULL,
|
||||||
|
id_app INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
exec TEXT,
|
||||||
|
icon TEXT,
|
||||||
|
FOREIGN KEY (id_app) REFERENCES ApplicationCache(id)
|
||||||
|
)
|
||||||
|
)")
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
m_cache.emplace(m_db);
|
||||||
}
|
}
|
||||||
|
|
||||||
App::~App()
|
App::~App()
|
||||||
@@ -68,6 +217,9 @@ App::~App()
|
|||||||
if (m_sfd != -1)
|
if (m_sfd != -1)
|
||||||
close(m_sfd);
|
close(m_sfd);
|
||||||
|
|
||||||
|
for (auto &[_, tex] : m_textures)
|
||||||
|
UnloadTexture(tex);
|
||||||
|
|
||||||
destroy_layer_surface();
|
destroy_layer_surface();
|
||||||
|
|
||||||
if (m_gl.edpy != EGL_NO_DISPLAY) {
|
if (m_gl.edpy != EGL_NO_DISPLAY) {
|
||||||
@@ -82,6 +234,14 @@ App::~App()
|
|||||||
xkb_keymap_unref(m_kbd.xkb_keymap_v);
|
xkb_keymap_unref(m_kbd.xkb_keymap_v);
|
||||||
if (m_kbd.xkb_ctx_v)
|
if (m_kbd.xkb_ctx_v)
|
||||||
xkb_context_unref(m_kbd.xkb_ctx_v);
|
xkb_context_unref(m_kbd.xkb_ctx_v);
|
||||||
|
if (m_wayland.text_input) {
|
||||||
|
zwp_text_input_v3_destroy(m_wayland.text_input);
|
||||||
|
m_wayland.text_input = nullptr;
|
||||||
|
}
|
||||||
|
if (m_wayland.text_input_mgr) {
|
||||||
|
zwp_text_input_manager_v3_destroy(m_wayland.text_input_mgr);
|
||||||
|
m_wayland.text_input_mgr = nullptr;
|
||||||
|
}
|
||||||
if (m_wayland.kbd)
|
if (m_wayland.kbd)
|
||||||
wl_keyboard_destroy(m_wayland.kbd);
|
wl_keyboard_destroy(m_wayland.kbd);
|
||||||
if (m_wayland.seat)
|
if (m_wayland.seat)
|
||||||
@@ -104,7 +264,10 @@ auto App::run() -> void
|
|||||||
SetWindowSize(m_win_w, m_win_h);
|
SetWindowSize(m_win_w, m_win_h);
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
pump_events();
|
pump_events();
|
||||||
|
m_ir.color(m_accent_color);
|
||||||
tick();
|
tick();
|
||||||
|
m_kbd.typing.clear();
|
||||||
|
m_kbd.clear_transients();
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,9 +298,9 @@ auto App::init_wayland() -> void
|
|||||||
|
|
||||||
static wl_keyboard_listener keyboard_listener {};
|
static wl_keyboard_listener keyboard_listener {};
|
||||||
{
|
{
|
||||||
auto kb_keymap = [](void *data, wl_keyboard *, u32 format, i32 fd,
|
auto kb_keymap { [](void *data, wl_keyboard *, u32 format, i32 fd,
|
||||||
u32 size) -> void {
|
u32 size) -> void {
|
||||||
auto *app = static_cast<App *>(data);
|
auto *app { static_cast<App *>(data) };
|
||||||
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
|
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
|
||||||
close(fd);
|
close(fd);
|
||||||
return;
|
return;
|
||||||
@@ -161,21 +324,25 @@ auto App::init_wayland() -> void
|
|||||||
: nullptr;
|
: nullptr;
|
||||||
munmap(map, size);
|
munmap(map, size);
|
||||||
close(fd);
|
close(fd);
|
||||||
};
|
} };
|
||||||
|
|
||||||
auto kb_enter = [](void *, wl_keyboard *, u32, wl_surface *,
|
auto kb_enter { [](void *data, wl_keyboard *, u32 serial, wl_surface *,
|
||||||
wl_array *) -> void { };
|
wl_array *) -> void {
|
||||||
|
static_cast<App *>(data)->m_last_serial = serial;
|
||||||
|
} };
|
||||||
auto kb_leave
|
auto kb_leave
|
||||||
= [](void *data, wl_keyboard *, u32, wl_surface *) -> void {
|
= [](void *data, wl_keyboard *, u32, wl_surface *) -> void {
|
||||||
static_cast<App *>(data)->m_kbd.held.clear();
|
static_cast<App *>(data)->m_kbd.held.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
auto kb_key = [](void *data, wl_keyboard *, u32, u32, u32 key,
|
auto kb_key { [](void *data, wl_keyboard *, u32 serial, u32, u32 key,
|
||||||
u32 state) -> void {
|
u32 state) -> void {
|
||||||
auto *app = static_cast<App *>(data);
|
auto *app { static_cast<App *>(data) };
|
||||||
if (!app->m_kbd.xkb_state_v)
|
if (!app->m_kbd.xkb_state_v)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
app->m_last_serial = serial;
|
||||||
|
|
||||||
xkb_keycode_t kc = key + 8;
|
xkb_keycode_t kc = key + 8;
|
||||||
|
|
||||||
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
||||||
@@ -222,6 +389,16 @@ auto App::init_wayland() -> void
|
|||||||
app->m_kbd.typing.push_back('\n');
|
app->m_kbd.typing.push_back('\n');
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
|
case XKB_KEY_v:
|
||||||
|
case XKB_KEY_V:
|
||||||
|
app->m_kbd.typing.push_back('v');
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
case XKB_KEY_c:
|
||||||
|
case XKB_KEY_C:
|
||||||
|
app->m_kbd.typing.push_back('c');
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
case XKB_KEY_w:
|
case XKB_KEY_w:
|
||||||
case XKB_KEY_W:
|
case XKB_KEY_W:
|
||||||
if (ctrl) {
|
if (ctrl) {
|
||||||
@@ -229,6 +406,13 @@ auto App::init_wayland() -> void
|
|||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case XKB_KEY_a:
|
||||||
|
case XKB_KEY_A:
|
||||||
|
if (ctrl) {
|
||||||
|
app->m_kbd.typing.push_back('a');
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -250,27 +434,117 @@ auto App::init_wayland() -> void
|
|||||||
app->m_kbd.held.erase(key);
|
app->m_kbd.held.erase(key);
|
||||||
xkb_state_update_key(app->m_kbd.xkb_state_v, kc, XKB_KEY_UP);
|
xkb_state_update_key(app->m_kbd.xkb_state_v, kc, XKB_KEY_UP);
|
||||||
}
|
}
|
||||||
};
|
} };
|
||||||
|
|
||||||
auto kb_mods = [](void *data, wl_keyboard *, u32, u32 depressed,
|
auto kb_mods { [](void *data, wl_keyboard *, u32, u32 depressed,
|
||||||
u32 latched, u32 locked, u32 group) -> void {
|
u32 latched, u32 locked, u32 group) -> void {
|
||||||
auto *app = static_cast<App *>(data);
|
auto *app { static_cast<App *>(data) };
|
||||||
if (!app->m_kbd.xkb_state_v)
|
if (!app->m_kbd.xkb_state_v)
|
||||||
return;
|
return;
|
||||||
xkb_state_update_mask(app->m_kbd.xkb_state_v, depressed, latched,
|
xkb_state_update_mask(app->m_kbd.xkb_state_v, depressed, latched,
|
||||||
locked, 0, 0, group);
|
locked, 0, 0, group);
|
||||||
};
|
} };
|
||||||
|
|
||||||
auto kb_repeat_info = [](void *, wl_keyboard *, i32, i32) -> void { };
|
auto kb_repeat_info { [](void *, wl_keyboard *, i32, i32) -> void { } };
|
||||||
|
|
||||||
keyboard_listener = { kb_keymap, kb_enter, kb_leave, kb_key, kb_mods,
|
keyboard_listener = { kb_keymap, kb_enter, kb_leave, kb_key, kb_mods,
|
||||||
kb_repeat_info };
|
kb_repeat_info };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static zwp_text_input_v3_listener text_input_listener {};
|
||||||
|
{
|
||||||
|
auto ti_enter =
|
||||||
|
[](void *data, zwp_text_input_v3 *,
|
||||||
|
wl_surface *surface) // cppcheck-suppress constParameterPointer
|
||||||
|
-> void {
|
||||||
|
auto *app { static_cast<App *>(data) };
|
||||||
|
bool const focused_surface
|
||||||
|
= surface && surface == app->m_wayland.surface;
|
||||||
|
app->m_ime.seat_focus = focused_surface;
|
||||||
|
app->m_ime.pending = {};
|
||||||
|
app->m_ime.pending_done = false;
|
||||||
|
if (!focused_surface) {
|
||||||
|
app->m_ime.enabled = false;
|
||||||
|
app->m_ime.last_surrounding.clear();
|
||||||
|
if (app->m_gui)
|
||||||
|
app->m_gui->ime_clear_preedit();
|
||||||
|
} else {
|
||||||
|
app->m_ime.surrounding_dirty = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto ti_leave { [](void *data, zwp_text_input_v3 *,
|
||||||
|
wl_surface *) -> void {
|
||||||
|
auto *app { static_cast<App *>(data) };
|
||||||
|
app->m_ime.seat_focus = false;
|
||||||
|
app->m_ime.enabled = false;
|
||||||
|
app->m_ime.pending = {};
|
||||||
|
app->m_ime.pending_done = false;
|
||||||
|
app->m_ime.last_surrounding.clear();
|
||||||
|
if (app->m_gui)
|
||||||
|
app->m_gui->ime_clear_preedit();
|
||||||
|
} };
|
||||||
|
|
||||||
|
auto ti_preedit { [](void *data, zwp_text_input_v3 *, char const *text,
|
||||||
|
int32_t cursor_begin,
|
||||||
|
int32_t cursor_end) -> void {
|
||||||
|
auto *app { static_cast<App *>(data) };
|
||||||
|
auto &pending { app->m_ime.pending };
|
||||||
|
pending.has_preedit = true;
|
||||||
|
pending.preedit_text = text ? text : "";
|
||||||
|
pending.cursor_begin = cursor_begin;
|
||||||
|
pending.cursor_end = cursor_end;
|
||||||
|
} };
|
||||||
|
|
||||||
|
auto ti_commit { [](void *data, zwp_text_input_v3 *,
|
||||||
|
char const *text) -> void {
|
||||||
|
auto *app { static_cast<App *>(data) };
|
||||||
|
auto &pending { app->m_ime.pending };
|
||||||
|
pending.has_commit = true;
|
||||||
|
pending.commit_text = text ? text : "";
|
||||||
|
} };
|
||||||
|
|
||||||
|
auto ti_delete { [](void *data, zwp_text_input_v3 *, uint32_t before,
|
||||||
|
uint32_t after) -> void {
|
||||||
|
auto *app { static_cast<App *>(data) };
|
||||||
|
auto &pending { app->m_ime.pending };
|
||||||
|
pending.has_delete = true;
|
||||||
|
pending.before = before;
|
||||||
|
pending.after = after;
|
||||||
|
} };
|
||||||
|
|
||||||
|
auto ti_done
|
||||||
|
= [](void *data, zwp_text_input_v3 *, uint32_t serial) -> void {
|
||||||
|
auto *app { static_cast<App *>(data) };
|
||||||
|
app->m_ime.pending_done = true;
|
||||||
|
app->m_ime.pending_serial = serial;
|
||||||
|
app->m_ime.surrounding_dirty = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
text_input_listener
|
||||||
|
= { ti_enter, ti_leave, ti_preedit, ti_commit, ti_delete, ti_done };
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto ensure_text_input { +[](App *app) -> void {
|
||||||
|
if (!app->m_wayland.text_input_mgr || !app->m_wayland.seat
|
||||||
|
|| app->m_wayland.text_input)
|
||||||
|
return;
|
||||||
|
app->m_wayland.text_input = zwp_text_input_manager_v3_get_text_input(
|
||||||
|
app->m_wayland.text_input_mgr, app->m_wayland.seat);
|
||||||
|
if (!app->m_wayland.text_input)
|
||||||
|
return;
|
||||||
|
zwp_text_input_v3_add_listener(
|
||||||
|
app->m_wayland.text_input, &text_input_listener, app);
|
||||||
|
app->m_ime.supported = true;
|
||||||
|
app->m_ime.enabled = false;
|
||||||
|
app->m_ime.last_surrounding.clear();
|
||||||
|
app->m_ime.sent_serial = 0;
|
||||||
|
} };
|
||||||
|
|
||||||
auto handle_registry_global
|
auto handle_registry_global
|
||||||
= [](void *data, wl_registry *registry, u32 name, char const *interface,
|
= [](void *data, wl_registry *registry, u32 name, char const *interface,
|
||||||
u32 version) -> void {
|
u32 version) -> void {
|
||||||
auto *app = static_cast<App *>(data);
|
auto *app { static_cast<App *>(data) };
|
||||||
if (std::strcmp(interface, wl_compositor_interface.name) == 0) {
|
if (std::strcmp(interface, wl_compositor_interface.name) == 0) {
|
||||||
app->m_wayland.compositor = static_cast<wl_compositor *>(
|
app->m_wayland.compositor = static_cast<wl_compositor *>(
|
||||||
wl_registry_bind(registry, name, &wl_compositor_interface, 4));
|
wl_registry_bind(registry, name, &wl_compositor_interface, 4));
|
||||||
@@ -280,12 +554,14 @@ auto App::init_wayland() -> void
|
|||||||
static struct wl_seat_listener const seat_listener = {
|
static struct wl_seat_listener const seat_listener = {
|
||||||
.capabilities =
|
.capabilities =
|
||||||
[](void *data, struct wl_seat *seat, u32 caps) {
|
[](void *data, struct wl_seat *seat, u32 caps) {
|
||||||
auto *app = static_cast<App *>(data);
|
auto *app { static_cast<App *>(data) };
|
||||||
if (caps & WL_SEAT_CAPABILITY_KEYBOARD) {
|
if (caps & WL_SEAT_CAPABILITY_KEYBOARD) {
|
||||||
app->m_wayland.kbd = wl_seat_get_keyboard(seat);
|
app->m_wayland.kbd = wl_seat_get_keyboard(seat);
|
||||||
wl_keyboard_add_listener(
|
wl_keyboard_add_listener(
|
||||||
app->m_wayland.kbd, &keyboard_listener, data);
|
app->m_wayland.kbd, &keyboard_listener, data);
|
||||||
|
app->m_ime.seat_focus = false;
|
||||||
}
|
}
|
||||||
|
ensure_text_input(app);
|
||||||
},
|
},
|
||||||
.name = [](void *, struct wl_seat *, char const *) {},
|
.name = [](void *, struct wl_seat *, char const *) {},
|
||||||
};
|
};
|
||||||
@@ -306,6 +582,120 @@ auto App::init_wayland() -> void
|
|||||||
app->m_wayland.kde_blur_mgr
|
app->m_wayland.kde_blur_mgr
|
||||||
= static_cast<org_kde_kwin_blur_manager *>(wl_registry_bind(
|
= static_cast<org_kde_kwin_blur_manager *>(wl_registry_bind(
|
||||||
registry, name, &org_kde_kwin_blur_manager_interface, 1));
|
registry, name, &org_kde_kwin_blur_manager_interface, 1));
|
||||||
|
} else if (std::strcmp(
|
||||||
|
interface, zwp_text_input_manager_v3_interface.name)
|
||||||
|
== 0) {
|
||||||
|
app->m_wayland.text_input_mgr
|
||||||
|
= static_cast<zwp_text_input_manager_v3 *>(wl_registry_bind(
|
||||||
|
registry, name, &zwp_text_input_manager_v3_interface, 1));
|
||||||
|
app->m_ime.supported = true;
|
||||||
|
ensure_text_input(app);
|
||||||
|
} else if (std::strcmp(interface, wl_data_device_manager_interface.name)
|
||||||
|
== 0) {
|
||||||
|
app->m_wayland.ddm
|
||||||
|
= static_cast<wl_data_device_manager *>(wl_registry_bind(
|
||||||
|
registry, name, &wl_data_device_manager_interface,
|
||||||
|
std::min<uint32_t>(version, 3)));
|
||||||
|
if (app->m_wayland.ddm && !app->m_wayland.ddev) {
|
||||||
|
app->m_wayland.ddev = wl_data_device_manager_get_data_device(
|
||||||
|
app->m_wayland.ddm, app->m_wayland.seat);
|
||||||
|
static wl_data_device_listener const ddev_l = {
|
||||||
|
.data_offer =
|
||||||
|
[](void *data, wl_data_device *, wl_data_offer *offer) {
|
||||||
|
auto *app = static_cast<App *>(data);
|
||||||
|
static wl_data_offer_listener const offer_l = {
|
||||||
|
.offer =
|
||||||
|
[](void *data, wl_data_offer *,
|
||||||
|
char const *mime) {
|
||||||
|
auto *app = static_cast<App *>(data);
|
||||||
|
(void)app;
|
||||||
|
(void)mime;
|
||||||
|
},
|
||||||
|
#if WL_DATA_OFFER_SOURCE_ACTIONS_SINCE_VERSION
|
||||||
|
.source_actions
|
||||||
|
= [](void *, wl_data_offer *, uint32_t) {},
|
||||||
|
.action
|
||||||
|
= [](void *, wl_data_offer *, uint32_t) {}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
wl_data_offer_add_listener(offer, &offer_l, app);
|
||||||
|
if (app->m_wayland.curr_offer
|
||||||
|
&& app->m_wayland.curr_offer != offer)
|
||||||
|
wl_data_offer_destroy(
|
||||||
|
app->m_wayland.curr_offer);
|
||||||
|
app->m_wayland.curr_offer = offer;
|
||||||
|
},
|
||||||
|
.enter
|
||||||
|
= [](void *, wl_data_device *, uint32_t, wl_surface *,
|
||||||
|
wl_fixed_t, wl_fixed_t, wl_data_offer *) {},
|
||||||
|
.leave = [](void *, wl_data_device *) {},
|
||||||
|
.motion = [](void *, wl_data_device *, uint32_t, wl_fixed_t,
|
||||||
|
wl_fixed_t) {},
|
||||||
|
.drop = [](void *, wl_data_device *) {},
|
||||||
|
.selection =
|
||||||
|
[](void *data, wl_data_device *, wl_data_offer *offer) {
|
||||||
|
auto *app = static_cast<App *>(data);
|
||||||
|
if (!offer) {
|
||||||
|
app->m_clipboard_cache.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char const *mime = "text/plain;charset=utf-8";
|
||||||
|
int fds[2];
|
||||||
|
if (pipe(fds) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
wl_data_offer_receive(offer, mime, fds[1]);
|
||||||
|
wl_display_flush(app->m_wayland.display);
|
||||||
|
close(fds[1]);
|
||||||
|
|
||||||
|
int rfd = fds[0];
|
||||||
|
|
||||||
|
std::thread([app, rfd, offer]() {
|
||||||
|
std::string data;
|
||||||
|
char buf[4096];
|
||||||
|
for (;;) {
|
||||||
|
ssize_t n = read(rfd, buf, sizeof buf);
|
||||||
|
if (n > 0) {
|
||||||
|
data.append(buf, buf + n);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (n < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
close(rfd);
|
||||||
|
|
||||||
|
struct Ctx {
|
||||||
|
App *app;
|
||||||
|
wl_data_offer *offer;
|
||||||
|
std::string data;
|
||||||
|
};
|
||||||
|
auto *ctx
|
||||||
|
= new Ctx { app, offer, std::move(data) };
|
||||||
|
|
||||||
|
g_main_context_invoke(
|
||||||
|
nullptr,
|
||||||
|
+[](gpointer p) -> gboolean {
|
||||||
|
auto *ctx = static_cast<Ctx *>(p);
|
||||||
|
if (!ctx->data.empty())
|
||||||
|
ctx->app->m_clipboard_cache
|
||||||
|
= std::move(ctx->data);
|
||||||
|
if (ctx->offer
|
||||||
|
== ctx->app->m_wayland.curr_offer) {
|
||||||
|
wl_data_offer_destroy(ctx->offer);
|
||||||
|
ctx->app->m_wayland.curr_offer
|
||||||
|
= nullptr;
|
||||||
|
}
|
||||||
|
delete ctx;
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
},
|
||||||
|
ctx);
|
||||||
|
}).detach();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
wl_data_device_add_listener(app->m_wayland.ddev, &ddev_l, app);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -342,15 +732,22 @@ auto App::init_egl() -> void
|
|||||||
|
|
||||||
ensure_egl_surface();
|
ensure_egl_surface();
|
||||||
|
|
||||||
|
{
|
||||||
|
auto const *env = getenv("WAYLIGHT_DEBUG");
|
||||||
|
if (env && *env) {
|
||||||
|
SetTraceLogLevel(LOG_DEBUG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
InitWindow(m_win_w, m_win_h, "");
|
InitWindow(m_win_w, m_win_h, "");
|
||||||
|
|
||||||
m_tr = std::make_shared<TextRenderer>();
|
m_tr = std::make_shared<TextRenderer>();
|
||||||
m_gui = std::make_shared<ImGui>(m_tr);
|
m_gui = std::make_shared<ImGui>(m_tr);
|
||||||
auto const font = find_font_path();
|
auto const font { find_font_path() };
|
||||||
assert(font && "Could not find font");
|
assert(font && "Could not find font");
|
||||||
std::vector<std::filesystem::path> fallback_paths;
|
std::vector<std::filesystem::path> fallback_paths;
|
||||||
std::unordered_set<std::string> seen_paths;
|
std::unordered_set<std::string> seen_paths;
|
||||||
auto const primary_path_str = font->string();
|
auto const primary_path_str { font->string() };
|
||||||
|
|
||||||
constexpr char const *fallback_candidates[] = {
|
constexpr char const *fallback_candidates[] = {
|
||||||
"Noto Sans CJK JP:style=Regular",
|
"Noto Sans CJK JP:style=Regular",
|
||||||
@@ -362,10 +759,11 @@ auto App::init_egl() -> void
|
|||||||
"sans-serif:lang=zh-cn",
|
"sans-serif:lang=zh-cn",
|
||||||
"sans-serif:lang=zh-tw",
|
"sans-serif:lang=zh-tw",
|
||||||
"sans-serif:lang=zh-hk",
|
"sans-serif:lang=zh-hk",
|
||||||
|
"Noto Color Emoji:style=Regular",
|
||||||
};
|
};
|
||||||
for (auto const *name : fallback_candidates) {
|
for (auto const *name : fallback_candidates) {
|
||||||
if (auto fallback = find_font_path(name)) {
|
if (auto fallback { find_font_path(name) }) {
|
||||||
auto const path_str = fallback->string();
|
auto const path_str { fallback->string() };
|
||||||
if (path_str == primary_path_str)
|
if (path_str == primary_path_str)
|
||||||
continue;
|
continue;
|
||||||
if (!seen_paths.emplace(path_str).second)
|
if (!seen_paths.emplace(path_str).second)
|
||||||
@@ -377,7 +775,8 @@ auto App::init_egl() -> void
|
|||||||
TraceLog(LOG_WARNING,
|
TraceLog(LOG_WARNING,
|
||||||
"No fallback fonts found; some glyphs may render as missing");
|
"No fallback fonts found; some glyphs may render as missing");
|
||||||
}
|
}
|
||||||
auto const font_handle = m_tr->load_font(*font, std::span(fallback_paths));
|
auto const font_handle { m_tr->load_font(
|
||||||
|
*font, std::span(fallback_paths)) };
|
||||||
assert(font_handle && "Could not load font");
|
assert(font_handle && "Could not load font");
|
||||||
m_font = *font_handle;
|
m_font = *font_handle;
|
||||||
m_gui->set_font(m_font);
|
m_gui->set_font(m_font);
|
||||||
@@ -404,16 +803,28 @@ auto App::init_signal() -> void
|
|||||||
void App::on_settings_changed(XdpSettings * /*self*/, char const *ns,
|
void App::on_settings_changed(XdpSettings * /*self*/, char const *ns,
|
||||||
char const *key, GVariant * /*value*/, gpointer data)
|
char const *key, GVariant * /*value*/, gpointer data)
|
||||||
{
|
{
|
||||||
auto *app = static_cast<App *>(data);
|
auto *app { static_cast<App *>(data) };
|
||||||
if (g_strcmp0(ns, "org.freedesktop.appearance") == 0
|
if (g_strcmp0(ns, "org.freedesktop.appearance") == 0) {
|
||||||
&& g_strcmp0(key, "color-scheme") == 0) {
|
if (g_strcmp0(key, "color-scheme") == 0) {
|
||||||
guint v = xdp_settings_read_uint(app->m_xdp.settings,
|
guint v = xdp_settings_read_uint(app->m_xdp.settings,
|
||||||
"org.freedesktop.appearance", "color-scheme", NULL, NULL);
|
"org.freedesktop.appearance", "color-scheme", NULL, NULL);
|
||||||
|
|
||||||
if (v == 1)
|
if (v == 1)
|
||||||
app->m_active_theme = Theme::Dark;
|
app->m_active_theme = Theme::Dark;
|
||||||
else
|
else
|
||||||
app->m_active_theme = Theme::Light;
|
app->m_active_theme = Theme::Light;
|
||||||
|
} else if (g_strcmp0(key, "accent-color") == 0) {
|
||||||
|
auto val { xdp_settings_read_value(app->m_xdp.settings,
|
||||||
|
"org.freedesktop.appearance", "accent-color", NULL, NULL) };
|
||||||
|
if (val) {
|
||||||
|
gdouble r, g, b;
|
||||||
|
g_variant_get(val, "(ddd)", &r, &g, &b);
|
||||||
|
app->m_accent_color.r = static_cast<u8>(r * 255);
|
||||||
|
app->m_accent_color.g = static_cast<u8>(g * 255);
|
||||||
|
app->m_accent_color.b = static_cast<u8>(b * 255);
|
||||||
|
g_variant_unref(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +840,17 @@ auto App::init_theme_portal() -> void
|
|||||||
else
|
else
|
||||||
m_active_theme = Theme::Light;
|
m_active_theme = Theme::Light;
|
||||||
|
|
||||||
|
auto val { xdp_settings_read_value(m_xdp.settings,
|
||||||
|
"org.freedesktop.appearance", "accent-color", NULL, NULL) };
|
||||||
|
if (val) {
|
||||||
|
gdouble r, g, b;
|
||||||
|
g_variant_get(val, "(ddd)", &r, &g, &b);
|
||||||
|
m_accent_color.r = static_cast<u8>(r * 255);
|
||||||
|
m_accent_color.g = static_cast<u8>(g * 255);
|
||||||
|
m_accent_color.b = static_cast<u8>(b * 255);
|
||||||
|
g_variant_unref(val);
|
||||||
|
}
|
||||||
|
|
||||||
g_signal_connect(
|
g_signal_connect(
|
||||||
m_xdp.settings, "changed", G_CALLBACK(on_settings_changed), this);
|
m_xdp.settings, "changed", G_CALLBACK(on_settings_changed), this);
|
||||||
}
|
}
|
||||||
@@ -482,9 +904,9 @@ auto App::create_layer_surface() -> void
|
|||||||
ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND);
|
ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto handle_layer_configure = [](void *data, zwlr_layer_surface_v1 *ls,
|
auto handle_layer_configure { [](void *data, zwlr_layer_surface_v1 *ls,
|
||||||
u32 serial, u32 w, u32 h) -> void {
|
u32 serial, u32 w, u32 h) -> void {
|
||||||
auto *app = static_cast<App *>(data);
|
auto *app { static_cast<App *>(data) };
|
||||||
if (w)
|
if (w)
|
||||||
app->m_win_w = static_cast<int>(w);
|
app->m_win_w = static_cast<int>(w);
|
||||||
if (h)
|
if (h)
|
||||||
@@ -507,11 +929,11 @@ auto App::create_layer_surface() -> void
|
|||||||
|
|
||||||
if (app->m_wayland.surface)
|
if (app->m_wayland.surface)
|
||||||
wl_surface_commit(app->m_wayland.surface);
|
wl_surface_commit(app->m_wayland.surface);
|
||||||
};
|
} };
|
||||||
|
|
||||||
auto handle_layer_closed = [](void *data, zwlr_layer_surface_v1 *) -> void {
|
auto handle_layer_closed { [](void *data, zwlr_layer_surface_v1 *) -> void {
|
||||||
static_cast<App *>(data)->m_running = false;
|
static_cast<App *>(data)->m_running = false;
|
||||||
};
|
} };
|
||||||
|
|
||||||
static zwlr_layer_surface_v1_listener const lsl = {
|
static zwlr_layer_surface_v1_listener const lsl = {
|
||||||
.configure = handle_layer_configure,
|
.configure = handle_layer_configure,
|
||||||
@@ -619,6 +1041,127 @@ auto App::update_blur_region() -> void
|
|||||||
wl_region_destroy(region);
|
wl_region_destroy(region);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto App::process_pending_text_input() -> void
|
||||||
|
{
|
||||||
|
if (!m_ime.pending_done)
|
||||||
|
return;
|
||||||
|
if (!m_gui)
|
||||||
|
return;
|
||||||
|
if (!m_ime.bound_text)
|
||||||
|
return;
|
||||||
|
if (!m_wayland.text_input)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto focused { m_gui->focused_text_input() };
|
||||||
|
if (!focused || *focused != m_ime.bound_id) {
|
||||||
|
m_ime.pending = {};
|
||||||
|
m_ime.pending_done = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_gui->ime_clear_preedit();
|
||||||
|
|
||||||
|
if (m_ime.pending.has_delete) {
|
||||||
|
m_gui->ime_delete_surrounding(
|
||||||
|
*m_ime.bound_text, m_ime.pending.before, m_ime.pending.after);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_ime.pending.has_commit) {
|
||||||
|
m_gui->ime_commit_text(*m_ime.bound_text, m_ime.pending.commit_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_ime.pending.has_preedit) {
|
||||||
|
m_gui->ime_set_preedit(m_ime.pending.preedit_text,
|
||||||
|
m_ime.pending.cursor_begin, m_ime.pending.cursor_end);
|
||||||
|
} else {
|
||||||
|
m_gui->ime_clear_preedit();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ime.pending = {};
|
||||||
|
m_ime.pending_done = false;
|
||||||
|
m_ime.surrounding_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto App::update_text_input_state(
|
||||||
|
std::pmr::string const &text, usize id, Rectangle field_rect) -> void
|
||||||
|
{
|
||||||
|
if (!m_wayland.text_input || !m_ime.supported || !m_gui)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_ime.bound_rect = field_rect;
|
||||||
|
|
||||||
|
auto focused { m_gui->focused_text_input() };
|
||||||
|
bool const has_focus { focused && (*focused == id) };
|
||||||
|
bool const should_enable { has_focus && m_ime.seat_focus };
|
||||||
|
|
||||||
|
if (!should_enable) {
|
||||||
|
if (m_ime.enabled) {
|
||||||
|
zwp_text_input_v3_disable(m_wayland.text_input);
|
||||||
|
zwp_text_input_v3_commit(m_wayland.text_input);
|
||||||
|
m_ime.sent_serial++;
|
||||||
|
m_ime.enabled = false;
|
||||||
|
m_ime.last_surrounding.clear();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool state_dirty = false;
|
||||||
|
|
||||||
|
if (!m_ime.enabled) {
|
||||||
|
zwp_text_input_v3_enable(m_wayland.text_input);
|
||||||
|
zwp_text_input_v3_set_content_type(m_wayland.text_input,
|
||||||
|
ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE,
|
||||||
|
ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL);
|
||||||
|
m_ime.enabled = true;
|
||||||
|
m_ime.surrounding_dirty = true;
|
||||||
|
state_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto info { m_gui->text_input_surrounding(id, text) }) {
|
||||||
|
auto slice
|
||||||
|
= clamp_surrounding_text(info->text, info->cursor, info->anchor);
|
||||||
|
if (m_ime.surrounding_dirty || slice.text != m_ime.last_surrounding
|
||||||
|
|| slice.cursor != m_ime.last_cursor
|
||||||
|
|| slice.anchor != m_ime.last_anchor) {
|
||||||
|
zwp_text_input_v3_set_surrounding_text(m_wayland.text_input,
|
||||||
|
slice.text.c_str(), slice.cursor, slice.anchor);
|
||||||
|
m_ime.last_surrounding = std::move(slice.text);
|
||||||
|
m_ime.last_cursor = slice.cursor;
|
||||||
|
m_ime.last_anchor = slice.anchor;
|
||||||
|
state_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto cursor_info { m_gui->text_input_cursor(id) }) {
|
||||||
|
Rectangle rect = cursor_info->rect;
|
||||||
|
int32_t const x = static_cast<int32_t>(std::round(rect.x));
|
||||||
|
int32_t const y = static_cast<int32_t>(std::round(rect.y));
|
||||||
|
int32_t const width
|
||||||
|
= std::max(1, static_cast<int32_t>(std::round(rect.width)));
|
||||||
|
int32_t const height
|
||||||
|
= std::max(1, static_cast<int32_t>(std::round(rect.height)));
|
||||||
|
bool const cur_visible = cursor_info->visible;
|
||||||
|
|
||||||
|
if (rect.x != m_ime.last_cursor_rect.x
|
||||||
|
|| rect.y != m_ime.last_cursor_rect.y
|
||||||
|
|| rect.width != m_ime.last_cursor_rect.width
|
||||||
|
|| rect.height != m_ime.last_cursor_rect.height
|
||||||
|
|| cur_visible != m_ime.last_cursor_visible) {
|
||||||
|
zwp_text_input_v3_set_cursor_rectangle(
|
||||||
|
m_wayland.text_input, x, y, width, height);
|
||||||
|
m_ime.last_cursor_rect = rect;
|
||||||
|
m_ime.last_cursor_visible = cur_visible;
|
||||||
|
state_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state_dirty) {
|
||||||
|
zwp_text_input_v3_commit(m_wayland.text_input);
|
||||||
|
m_ime.sent_serial++;
|
||||||
|
m_ime.surrounding_dirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto App::pump_events() -> void
|
auto App::pump_events() -> void
|
||||||
{
|
{
|
||||||
while (g_main_context_iteration(nullptr, false))
|
while (g_main_context_iteration(nullptr, false))
|
||||||
@@ -630,13 +1173,13 @@ auto App::pump_events() -> void
|
|||||||
pollfd fds[2] { { wl_display_get_fd(m_wayland.display), POLLIN, 0 },
|
pollfd fds[2] { { wl_display_get_fd(m_wayland.display), POLLIN, 0 },
|
||||||
{ m_sfd, POLLIN, 0 } };
|
{ m_sfd, POLLIN, 0 } };
|
||||||
|
|
||||||
auto prepared = (wl_display_prepare_read(m_wayland.display) == 0);
|
auto prepared { (wl_display_prepare_read(m_wayland.display) == 0) };
|
||||||
auto ret = poll(fds, 2, 0);
|
auto ret { poll(fds, 2, 0) };
|
||||||
|
|
||||||
if (ret > 0 && (fds[0].revents & POLLIN)) {
|
if (ret > 0 && (fds[0].revents & POLLIN)) {
|
||||||
if (prepared) {
|
if (prepared) {
|
||||||
wl_display_read_events(m_wayland.display);
|
wl_display_read_events(m_wayland.display);
|
||||||
prepared = false;
|
prepared = false; // cppcheck-suppress unreadVariable
|
||||||
}
|
}
|
||||||
} else if (prepared) {
|
} else if (prepared) {
|
||||||
wl_display_cancel_read(m_wayland.display);
|
wl_display_cancel_read(m_wayland.display);
|
||||||
@@ -651,3 +1194,135 @@ auto App::pump_events() -> void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto App::clipboard(std::string_view const &str) -> void
|
||||||
|
{
|
||||||
|
if (!m_wayland.ddm || !m_wayland.ddev || !m_wayland.seat)
|
||||||
|
return;
|
||||||
|
if (m_last_serial == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (m_wayland.curr_source) {
|
||||||
|
wl_data_source_destroy(m_wayland.curr_source);
|
||||||
|
m_wayland.curr_source = nullptr;
|
||||||
|
}
|
||||||
|
m_wayland.curr_source
|
||||||
|
= wl_data_device_manager_create_data_source(m_wayland.ddm);
|
||||||
|
|
||||||
|
static wl_data_source_listener const src_l = {
|
||||||
|
.target = [](void *, wl_data_source *, char const *) {},
|
||||||
|
.send =
|
||||||
|
[](void *data, wl_data_source *, char const *, int32_t fd) {
|
||||||
|
auto *app = static_cast<App *>(data);
|
||||||
|
|
||||||
|
int wfd = dup(fd);
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
std::pmr::string payload = app->m_clipboard_cache;
|
||||||
|
std::thread([wfd, payload = std::move(payload)]() {
|
||||||
|
size_t off = 0;
|
||||||
|
while (off < payload.size()) {
|
||||||
|
ssize_t n = write(wfd, payload.data() + off,
|
||||||
|
std::min<size_t>(64 * 1024, payload.size() - off));
|
||||||
|
if (n > 0) {
|
||||||
|
off += (size_t)n;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (n < 0 && (errno == EINTR))
|
||||||
|
continue;
|
||||||
|
if (n < 0 && (errno == EAGAIN)) {
|
||||||
|
std::this_thread::sleep_for(
|
||||||
|
std::chrono::milliseconds(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
close(wfd);
|
||||||
|
}).detach();
|
||||||
|
},
|
||||||
|
.cancelled =
|
||||||
|
[](void *data, wl_data_source *src) {
|
||||||
|
auto *app = static_cast<App *>(data);
|
||||||
|
if (app->m_wayland.curr_source == src)
|
||||||
|
app->m_wayland.curr_source = nullptr;
|
||||||
|
wl_data_source_destroy(src);
|
||||||
|
},
|
||||||
|
#if WL_DATA_SOURCE_DND_DROP_PERFORMED_SINCE_VERSION
|
||||||
|
.dnd_drop_performed = [](void *, wl_data_source *) {},
|
||||||
|
.dnd_finished = [](void *, wl_data_source *) {},
|
||||||
|
.action = [](void *, wl_data_source *, uint32_t) {}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
wl_data_source_add_listener(m_wayland.curr_source, &src_l, this);
|
||||||
|
wl_data_source_offer(m_wayland.curr_source, "text/plain;charset=utf-8");
|
||||||
|
wl_data_source_offer(m_wayland.curr_source, "text/plain");
|
||||||
|
|
||||||
|
m_clipboard_cache.assign(str.begin(), str.end());
|
||||||
|
|
||||||
|
wl_data_device_set_selection(
|
||||||
|
m_wayland.ddev, m_wayland.curr_source, m_last_serial);
|
||||||
|
wl_display_flush(m_wayland.display);
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::execute_command(bool terminal, std::string_view const command)
|
||||||
|
{
|
||||||
|
constexpr auto resolve_cmdline { [](std::string_view const cmdline,
|
||||||
|
std::vector<std::string> &out) {
|
||||||
|
std::ranges::copy(cmdline | std::views::split(' ')
|
||||||
|
| std::views::transform(
|
||||||
|
[](auto &&s) { return std::string(s.begin(), s.end()); }),
|
||||||
|
std::back_inserter(out));
|
||||||
|
} };
|
||||||
|
|
||||||
|
std::vector<std::string> args;
|
||||||
|
if (terminal) {
|
||||||
|
resolve_cmdline(m_config.terminal_cmdline, args);
|
||||||
|
} else {
|
||||||
|
args.push_back("/bin/sh");
|
||||||
|
args.push_back("-c");
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push_back(std::string(command));
|
||||||
|
if (auto const &exe { args.at(0) }; exe.at(0) != '/') {
|
||||||
|
auto const *path_env { getenv("PATH") };
|
||||||
|
if (!(path_env && *path_env)) {
|
||||||
|
path_env = "/bin";
|
||||||
|
}
|
||||||
|
auto const path { std::string(path_env) };
|
||||||
|
|
||||||
|
for (auto const &dir :
|
||||||
|
path | std::views::split(':') | std::views::transform([](auto &&s) {
|
||||||
|
return std::filesystem::path(s.begin(), s.end());
|
||||||
|
}) | std::views::filter([](auto &&p) {
|
||||||
|
return std::filesystem::is_directory(p);
|
||||||
|
})) {
|
||||||
|
auto const path = dir / exe;
|
||||||
|
if (std::filesystem::is_regular_file(path)) {
|
||||||
|
args[0] = path.string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::print("Final args: ");
|
||||||
|
for (auto const &arg : args) {
|
||||||
|
std::print("{} ", arg);
|
||||||
|
}
|
||||||
|
std::println("");
|
||||||
|
|
||||||
|
std::vector<char const *> cargs;
|
||||||
|
std::transform(args.begin(), args.end(), std::back_inserter(cargs),
|
||||||
|
[](auto &&s) { return s.c_str(); });
|
||||||
|
cargs.push_back(nullptr);
|
||||||
|
auto cargsc { const_cast<char *const *>(cargs.data()) };
|
||||||
|
|
||||||
|
auto const pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
setsid();
|
||||||
|
|
||||||
|
execv(args.at(0).c_str(), cargsc);
|
||||||
|
} else if (pid < 0) {
|
||||||
|
throw std::runtime_error("Failed to fork process");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
101
src/App.hpp
101
src/App.hpp
@@ -1,27 +1,38 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <EGL/egl.h>
|
#include <EGL/egl.h>
|
||||||
#include <libportal/portal.h>
|
#include <libportal/portal.h>
|
||||||
|
#include <wayland-client-protocol.h>
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "blur-client-protocol.h"
|
#include "blur-client-protocol.h"
|
||||||
#define namespace namespace_
|
#define namespace namespace_
|
||||||
#include "ext-background-effect-v1-client-protocol.h"
|
#include "ext-background-effect-v1-client-protocol.h"
|
||||||
|
#include "text-input-unstable-v3-client-protocol.h"
|
||||||
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
|
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
|
||||||
#include <libportal/settings.h>
|
#include <libportal/settings.h>
|
||||||
#undef namespace
|
#undef namespace
|
||||||
}
|
}
|
||||||
|
#include <SQLiteCpp/SQLiteCpp.h>
|
||||||
#include <wayland-client.h>
|
#include <wayland-client.h>
|
||||||
#include <wayland-egl.h>
|
#include <wayland-egl.h>
|
||||||
#include <xkbcommon/xkbcommon.h>
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
#include "Cache.hpp"
|
||||||
|
#include "Config.hpp"
|
||||||
|
#include "IconRegistry.hpp"
|
||||||
#include "ImGui.hpp"
|
#include "ImGui.hpp"
|
||||||
#include "TextRenderer.hpp"
|
#include "TextRenderer.hpp"
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "common.hpp"
|
#include "common.hpp"
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
struct TypingBuffer : std::pmr::vector<u32> {
|
struct TypingBuffer : std::pmr::vector<u32> {
|
||||||
void push_utf8(char const *s);
|
void push_utf8(char const *s);
|
||||||
};
|
};
|
||||||
@@ -47,11 +58,20 @@ private:
|
|||||||
auto destroy_layer_surface() -> void;
|
auto destroy_layer_surface() -> void;
|
||||||
auto ensure_egl_surface() -> void;
|
auto ensure_egl_surface() -> void;
|
||||||
auto update_blur_region() -> void;
|
auto update_blur_region() -> void;
|
||||||
|
auto process_pending_text_input() -> void;
|
||||||
|
auto update_text_input_state(
|
||||||
|
std::pmr::string const &text, usize id, Rectangle field_rect) -> void;
|
||||||
auto theme() const -> ColorScheme const &
|
auto theme() const -> ColorScheme const &
|
||||||
{
|
{
|
||||||
return m_themes[m_active_theme];
|
return m_themes[m_active_theme];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto clipboard() const -> std::pmr::string const &
|
||||||
|
{
|
||||||
|
return m_clipboard_cache;
|
||||||
|
}
|
||||||
|
auto clipboard(std::string_view const &str) -> void;
|
||||||
|
|
||||||
static void on_settings_changed(XdpSettings * /*self*/, char const *ns,
|
static void on_settings_changed(XdpSettings * /*self*/, char const *ns,
|
||||||
char const *key, GVariant * /*value*/, gpointer data);
|
char const *key, GVariant * /*value*/, gpointer data);
|
||||||
|
|
||||||
@@ -68,7 +88,15 @@ private:
|
|||||||
ext_background_effect_surface_v1 *eff {};
|
ext_background_effect_surface_v1 *eff {};
|
||||||
org_kde_kwin_blur_manager *kde_blur_mgr {};
|
org_kde_kwin_blur_manager *kde_blur_mgr {};
|
||||||
org_kde_kwin_blur *kde_blur {};
|
org_kde_kwin_blur *kde_blur {};
|
||||||
|
zwp_text_input_manager_v3 *text_input_mgr {};
|
||||||
|
zwp_text_input_v3 *text_input {};
|
||||||
|
wl_data_device_manager *ddm {};
|
||||||
|
wl_data_device *ddev {};
|
||||||
|
wl_data_offer *curr_offer {};
|
||||||
|
wl_data_source *curr_source {};
|
||||||
} m_wayland;
|
} m_wayland;
|
||||||
|
std::pmr::string m_clipboard_cache;
|
||||||
|
u32 m_last_serial { 0 };
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
EGLDisplay edpy { EGL_NO_DISPLAY };
|
EGLDisplay edpy { EGL_NO_DISPLAY };
|
||||||
@@ -103,13 +131,11 @@ private:
|
|||||||
{
|
{
|
||||||
if (!xkb_state_v)
|
if (!xkb_state_v)
|
||||||
return false;
|
return false;
|
||||||
for (auto k : held) {
|
return std::any_of(held.begin(), held.end(), [&](u32 const k) {
|
||||||
if (xkb_state_key_get_one_sym(
|
return (xkb_state_key_get_one_sym(
|
||||||
xkb_state_v, static_cast<xkb_keycode_t>(k + 8))
|
xkb_state_v, static_cast<xkb_keycode_t>(k + 8))
|
||||||
== sym)
|
== sym);
|
||||||
return true;
|
});
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto is_sym_pressed(xkb_keysym_t sym) const -> bool
|
auto is_sym_pressed(xkb_keysym_t sym) const -> bool
|
||||||
@@ -144,13 +170,74 @@ private:
|
|||||||
FontHandle m_font;
|
FontHandle m_font;
|
||||||
std::shared_ptr<ImGui> m_gui { nullptr };
|
std::shared_ptr<ImGui> m_gui { nullptr };
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool supported { false };
|
||||||
|
bool seat_focus { false };
|
||||||
|
bool enabled { false };
|
||||||
|
bool pending_done { false };
|
||||||
|
uint32_t pending_serial { 0 };
|
||||||
|
uint32_t sent_serial { 0 };
|
||||||
|
|
||||||
|
std::pmr::string *bound_text { nullptr };
|
||||||
|
usize bound_id { 0 };
|
||||||
|
Rectangle bound_rect {};
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool has_preedit { false };
|
||||||
|
std::string preedit_text;
|
||||||
|
int cursor_begin { 0 };
|
||||||
|
int cursor_end { 0 };
|
||||||
|
bool has_commit { false };
|
||||||
|
std::string commit_text;
|
||||||
|
bool has_delete { false };
|
||||||
|
uint32_t before { 0 };
|
||||||
|
uint32_t after { 0 };
|
||||||
|
} pending;
|
||||||
|
|
||||||
|
std::string last_surrounding;
|
||||||
|
int last_cursor { 0 };
|
||||||
|
int last_anchor { 0 };
|
||||||
|
Rectangle last_cursor_rect {};
|
||||||
|
bool last_cursor_visible { false };
|
||||||
|
bool surrounding_dirty { false };
|
||||||
|
} m_ime;
|
||||||
|
|
||||||
|
auto get_texture(std::filesystem::path const &path) -> Texture2D const &
|
||||||
|
{
|
||||||
|
if (m_textures.contains(path)) {
|
||||||
|
return m_textures[path];
|
||||||
|
}
|
||||||
|
auto fname = path.c_str();
|
||||||
|
assert(fname);
|
||||||
|
TraceLog(LOG_INFO, std::format("loading texture at {}", fname).c_str());
|
||||||
|
auto const tex = LoadTexture(fname);
|
||||||
|
assert(IsTextureValid(tex));
|
||||||
|
m_textures[path] = tex;
|
||||||
|
return m_textures[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
void execute_command(bool terminal, std::string_view const command);
|
||||||
|
|
||||||
|
// NOTE: Canonicalize first!
|
||||||
|
std::unordered_map<std::filesystem::path, Texture2D> m_textures;
|
||||||
|
|
||||||
enum_array<Theme, ColorScheme> m_themes { make_default_themes() };
|
enum_array<Theme, ColorScheme> m_themes { make_default_themes() };
|
||||||
Theme m_active_theme { Theme::Light };
|
Theme m_active_theme { Theme::Light };
|
||||||
|
IconRegistry m_ir;
|
||||||
|
std::optional<Cache> m_cache;
|
||||||
|
Config m_config;
|
||||||
|
|
||||||
int m_win_w { 800 };
|
int m_win_w { 800 };
|
||||||
int m_win_h { 600 };
|
int m_win_h { 600 };
|
||||||
bool m_running { true };
|
bool m_running { true };
|
||||||
bool m_visible { true };
|
bool m_visible { true };
|
||||||
|
|
||||||
|
Color m_accent_color { 127, 127, 255, 255 };
|
||||||
|
|
||||||
|
std::filesystem::path m_data_home_dir {};
|
||||||
|
std::filesystem::path m_config_home_dir {};
|
||||||
|
std::shared_ptr<SQLite::Database> m_db {};
|
||||||
int m_sfd { -1 };
|
int m_sfd { -1 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
435
src/Cache.cpp
Normal file
435
src/Cache.cpp
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
#include "Cache.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <numeric>
|
||||||
|
#include <ranges>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <SQLiteCpp/Statement.h>
|
||||||
|
#include <SQLiteCpp/Transaction.h>
|
||||||
|
#include <mini/ini.h>
|
||||||
|
#include <raylib.h>
|
||||||
|
|
||||||
|
#include "common.hpp"
|
||||||
|
#include <sys/inotify.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
void replace_all(
|
||||||
|
std::string &str, std::string const &from, std::string const &to)
|
||||||
|
{
|
||||||
|
if (from.empty())
|
||||||
|
return;
|
||||||
|
size_t pos = 0;
|
||||||
|
while ((pos = str.find(from, pos)) != std::string::npos) {
|
||||||
|
str.replace(pos, from.size(), to);
|
||||||
|
pos += to.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::Cache(std::shared_ptr<SQLite::Database> db)
|
||||||
|
: m_db(db)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto const *env { getenv("XDG_DATA_DIRS") };
|
||||||
|
if (env && *env) {
|
||||||
|
std::ranges::copy(std::string_view(env) | std::views::split(':')
|
||||||
|
| std::views::transform([](auto &&s) {
|
||||||
|
return std::filesystem::path(s.begin(), s.end())
|
||||||
|
/ "applications";
|
||||||
|
})
|
||||||
|
| std::views::filter([](auto &&p) {
|
||||||
|
if (!std::filesystem::is_directory(p))
|
||||||
|
return false;
|
||||||
|
if (std::filesystem::directory_iterator(p)
|
||||||
|
== std::filesystem::directory_iterator {})
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
std::back_inserter(m_app_dirs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
||||||
|
auto total = std::accumulate(m_app_dirs.begin(), m_app_dirs.end(),
|
||||||
|
static_cast<usize>(0), [](usize acc, auto &&dir) {
|
||||||
|
return acc
|
||||||
|
+ std::count_if(std::filesystem::directory_iterator(dir),
|
||||||
|
std::filesystem::directory_iterator {}, [](auto &&entry) {
|
||||||
|
return entry.is_regular_file()
|
||||||
|
&& entry.path().extension() == ".desktop";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (total != m_apps.size()) {
|
||||||
|
rescan();
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceLog(LOG_DEBUG, std::format("Applications in cache:").c_str());
|
||||||
|
for (auto const &app : m_apps) {
|
||||||
|
TraceLog(LOG_DEBUG,
|
||||||
|
std::format("{}:", app.desktop_entry_path.string()).c_str());
|
||||||
|
if (app.comment)
|
||||||
|
TraceLog(
|
||||||
|
LOG_DEBUG, std::format(" - Comment: {}", *app.comment).c_str());
|
||||||
|
if (app.path)
|
||||||
|
TraceLog(LOG_DEBUG, std::format(" - Path: {}", *app.path).c_str());
|
||||||
|
TraceLog(
|
||||||
|
LOG_DEBUG, std::format(" - Terminal: {}", app.terminal).c_str());
|
||||||
|
TraceLog(
|
||||||
|
LOG_DEBUG, std::format(" - NoDisplay: {}", app.no_display).c_str());
|
||||||
|
TraceLog(LOG_DEBUG, std::format(" - Actions:").c_str());
|
||||||
|
for (auto const &action : app.actions) {
|
||||||
|
TraceLog(
|
||||||
|
LOG_DEBUG, std::format(" - Name: {}", action.name).c_str());
|
||||||
|
if (action.exec)
|
||||||
|
TraceLog(LOG_DEBUG,
|
||||||
|
std::format(" Exec: {}", *action.exec).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto const &dir : m_app_dirs) {
|
||||||
|
m_inotify.watch_path_recursively(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_inotify.set_callback([this](FileWatchEvent const &event) {
|
||||||
|
auto const mask = event.mask;
|
||||||
|
if (mask & IN_Q_OVERFLOW) {
|
||||||
|
rescan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask & IN_ISDIR) {
|
||||||
|
rescan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask & (IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM
|
||||||
|
| IN_MOVED_TO | IN_CLOSE_WRITE)) {
|
||||||
|
if (event.path.extension() == ".desktop") {
|
||||||
|
rescan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_inotify_thread = std::thread([this]() { m_inotify.run(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::~Cache()
|
||||||
|
{
|
||||||
|
m_inotify.stop();
|
||||||
|
if (m_inotify_thread.joinable())
|
||||||
|
m_inotify_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cache::rescan()
|
||||||
|
{
|
||||||
|
m_apps.clear();
|
||||||
|
|
||||||
|
int id = 0;
|
||||||
|
for (auto const &dir : m_app_dirs) {
|
||||||
|
for (auto const &file : std::filesystem::directory_iterator(dir)) {
|
||||||
|
if (!file.is_regular_file())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (file.path().extension() != ".desktop")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
mINI::INIFile ini_file(file.path());
|
||||||
|
mINI::INIStructure ini;
|
||||||
|
ini_file.read(ini);
|
||||||
|
|
||||||
|
constexpr auto read_action = [&](std::string const
|
||||||
|
&desktop_file_uri,
|
||||||
|
mINI::INIMap<std::string> const
|
||||||
|
§ion) {
|
||||||
|
auto const name = section.get("Name");
|
||||||
|
auto const icon = [&]()
|
||||||
|
-> std::optional<
|
||||||
|
std::variant<std::filesystem::path, std::string>> {
|
||||||
|
if (section.has("Icon")) {
|
||||||
|
auto const icon_name = section.get("Icon");
|
||||||
|
if (!icon_name.empty()) {
|
||||||
|
if (icon_name[0] == '/') {
|
||||||
|
return std::filesystem::path(icon_name);
|
||||||
|
} else {
|
||||||
|
return icon_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return ApplicationCache::Action {
|
||||||
|
.name = name,
|
||||||
|
.exec = [&]() -> std::optional<std::string> {
|
||||||
|
if (section.has("Exec")) {
|
||||||
|
auto s = section.get("Exec");
|
||||||
|
if (!s.empty()) {
|
||||||
|
// Either deprecated or not used...
|
||||||
|
replace_all(s, "%f", "");
|
||||||
|
replace_all(s, "%F", "");
|
||||||
|
replace_all(s, "%u", "");
|
||||||
|
replace_all(s, "%U", "");
|
||||||
|
replace_all(s, "%d", "");
|
||||||
|
replace_all(s, "%D", "");
|
||||||
|
replace_all(s, "%n", "");
|
||||||
|
replace_all(s, "%N", "");
|
||||||
|
replace_all(s, "%v", "");
|
||||||
|
replace_all(s, "%M", "");
|
||||||
|
|
||||||
|
replace_all(s, "%c", name);
|
||||||
|
if (icon) {
|
||||||
|
if (auto const p
|
||||||
|
= std::get_if<std::filesystem::path>(
|
||||||
|
&*icon)) {
|
||||||
|
replace_all(s, "%i",
|
||||||
|
"--icon '" + p->string() + "'");
|
||||||
|
} else if (auto const n
|
||||||
|
= std::get_if<std::string>(&*icon)) {
|
||||||
|
replace_all(
|
||||||
|
s, "%i", "--icon '" + *n + "'");
|
||||||
|
} else {
|
||||||
|
replace_all(s, "%i", "");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
replace_all(s, "%i", "");
|
||||||
|
}
|
||||||
|
replace_all(s, "%k", "'" + desktop_file_uri);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}(),
|
||||||
|
.icon = icon,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
m_apps.push_back({
|
||||||
|
.id = id++,
|
||||||
|
.desktop_entry_path = file.path(),
|
||||||
|
.type =
|
||||||
|
[&]() {
|
||||||
|
auto const type_str { ini["Desktop Entry"].get(
|
||||||
|
"Type") };
|
||||||
|
auto type { ApplicationCache::Type::Application };
|
||||||
|
if (type_str == "Application")
|
||||||
|
type = ApplicationCache::Type::Application;
|
||||||
|
else if (type_str == "Link")
|
||||||
|
type = ApplicationCache::Type::Link;
|
||||||
|
else if (type_str == "Directory")
|
||||||
|
type = ApplicationCache::Type::Directory;
|
||||||
|
return type;
|
||||||
|
}(),
|
||||||
|
.terminal =
|
||||||
|
[&]() {
|
||||||
|
if (ini["Desktop Entry"].has("Terminal")) {
|
||||||
|
return ini["Desktop Entry"]["Terminal"] == "true"
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}(),
|
||||||
|
.no_display =
|
||||||
|
[&]() {
|
||||||
|
if (ini["Desktop Entry"].has("NoDisplay")) {
|
||||||
|
return ini["Desktop Entry"]["NoDisplay"] == "true"
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}(),
|
||||||
|
.path = [&]() -> std::optional<std::string> {
|
||||||
|
if (ini["Desktop Entry"].has("Path")) {
|
||||||
|
return ini["Desktop Entry"]["Path"];
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}(),
|
||||||
|
.comment = [&]() -> std::optional<std::string> {
|
||||||
|
if (ini["Desktop Entry"].has("Comment")) {
|
||||||
|
return ini["Desktop Entry"]["Comment"];
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}(),
|
||||||
|
.actions =
|
||||||
|
[&]() {
|
||||||
|
std::vector<ApplicationCache::Action> actions;
|
||||||
|
for (auto const &[_, v] : ini) {
|
||||||
|
try {
|
||||||
|
auto const action = read_action(
|
||||||
|
std::filesystem::canonical(file.path())
|
||||||
|
.string(),
|
||||||
|
v);
|
||||||
|
actions.push_back(action);
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
}(),
|
||||||
|
.dbus_activatable =
|
||||||
|
[&]() {
|
||||||
|
if (ini["Desktop Entry"].has("DBusActivatable")) {
|
||||||
|
return ini["Desktop Entry"]["DBusActivatable"]
|
||||||
|
== "true"
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cache::dump()
|
||||||
|
{
|
||||||
|
SQLite::Transaction tx(*m_db);
|
||||||
|
|
||||||
|
SQLite::Statement(*m_db, "DELETE FROM ApplicationCache").exec();
|
||||||
|
SQLite::Statement(*m_db, "DELETE FROM ApplicationActionCache").exec();
|
||||||
|
|
||||||
|
try {
|
||||||
|
SQLite::Statement(
|
||||||
|
*m_db, "DELETE FROM sqlite_sequence WHERE name='ApplicationCache'")
|
||||||
|
.exec();
|
||||||
|
SQLite::Statement(*m_db,
|
||||||
|
"DELETE FROM sqlite_sequence WHERE name='ApplicationActionCache'")
|
||||||
|
.exec();
|
||||||
|
} catch (std::exception const &) {
|
||||||
|
}
|
||||||
|
|
||||||
|
SQLite::Statement ins_app(*m_db,
|
||||||
|
"INSERT INTO ApplicationCache(type, desktop_entry_path, terminal, "
|
||||||
|
"no_display, path, comment, dbus_activatable) VALUES (?,?,?,?,?,?,?)");
|
||||||
|
|
||||||
|
SQLite::Statement ins_act(*m_db,
|
||||||
|
"INSERT INTO ApplicationActionCache(id_app, name, exec, icon) VALUES "
|
||||||
|
"(?,?,?,?)");
|
||||||
|
|
||||||
|
for (auto &app : m_apps) {
|
||||||
|
ins_app.reset();
|
||||||
|
ins_app.clearBindings();
|
||||||
|
ins_app.bind(1, static_cast<int>(app.type));
|
||||||
|
ins_app.bind(2, app.desktop_entry_path.string());
|
||||||
|
ins_app.bind(3, app.terminal ? 1 : 0);
|
||||||
|
ins_app.bind(4, app.no_display ? 1 : 0);
|
||||||
|
if (app.path)
|
||||||
|
ins_app.bind(5, *app.path);
|
||||||
|
else
|
||||||
|
ins_app.bind(5);
|
||||||
|
if (app.comment)
|
||||||
|
ins_app.bind(6, *app.comment);
|
||||||
|
else
|
||||||
|
ins_app.bind(6);
|
||||||
|
ins_app.bind(7, app.dbus_activatable ? 1 : 0);
|
||||||
|
ins_app.exec();
|
||||||
|
|
||||||
|
app.id = m_db->getLastInsertRowid();
|
||||||
|
|
||||||
|
for (auto const &action : app.actions) {
|
||||||
|
ins_act.reset();
|
||||||
|
ins_act.clearBindings();
|
||||||
|
ins_act.bind(1, app.id);
|
||||||
|
ins_act.bind(2, action.name);
|
||||||
|
if (action.exec)
|
||||||
|
ins_act.bind(3, *action.exec);
|
||||||
|
else
|
||||||
|
ins_act.bind(3);
|
||||||
|
|
||||||
|
if (action.icon) {
|
||||||
|
std::string str;
|
||||||
|
if (auto const *s = std::get_if<std::string>(&*action.icon)) {
|
||||||
|
str = *s;
|
||||||
|
} else if (auto const *p
|
||||||
|
= std::get_if<std::filesystem::path>(&*action.icon)) {
|
||||||
|
str = std::filesystem::canonical(*p).string();
|
||||||
|
}
|
||||||
|
ins_act.bind(4, str);
|
||||||
|
} else {
|
||||||
|
ins_act.bind(4);
|
||||||
|
}
|
||||||
|
ins_act.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cache::load()
|
||||||
|
{
|
||||||
|
m_apps.clear();
|
||||||
|
|
||||||
|
SQLite::Statement get_apps(*m_db,
|
||||||
|
"SELECT id, type, desktop_entry_path, terminal, no_display, path, "
|
||||||
|
"comment, dbus_activatable "
|
||||||
|
"FROM ApplicationCache");
|
||||||
|
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> id_to_index;
|
||||||
|
|
||||||
|
while (get_apps.executeStep()) {
|
||||||
|
ApplicationCache app {};
|
||||||
|
app.id = get_apps.getColumn(0).getInt64();
|
||||||
|
app.type = static_cast<ApplicationCache::Type>(
|
||||||
|
get_apps.getColumn(1).getInt());
|
||||||
|
app.desktop_entry_path
|
||||||
|
= std::filesystem::path(get_apps.getColumn(2).getString());
|
||||||
|
app.terminal = get_apps.getColumn(3).getInt() != 0;
|
||||||
|
app.no_display = get_apps.getColumn(4).getInt() != 0;
|
||||||
|
|
||||||
|
if (!get_apps.getColumn(5).isNull())
|
||||||
|
app.path = std::string(get_apps.getColumn(5).getString());
|
||||||
|
else
|
||||||
|
app.path.reset();
|
||||||
|
|
||||||
|
if (!get_apps.getColumn(6).isNull())
|
||||||
|
app.comment = std::string(get_apps.getColumn(6).getString());
|
||||||
|
else
|
||||||
|
app.comment.reset();
|
||||||
|
|
||||||
|
app.dbus_activatable = get_apps.getColumn(7).getInt() != 0;
|
||||||
|
|
||||||
|
id_to_index.emplace(app.id, m_apps.size());
|
||||||
|
m_apps.push_back(std::move(app));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_apps.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
SQLite::Statement get_actions(*m_db,
|
||||||
|
"SELECT id_app, name, exec, icon "
|
||||||
|
"FROM ApplicationActionCache "
|
||||||
|
"ORDER BY id_app");
|
||||||
|
|
||||||
|
while (get_actions.executeStep()) {
|
||||||
|
auto id_app = get_actions.getColumn(0).getInt64();
|
||||||
|
auto it = id_to_index.find(id_app);
|
||||||
|
if (it == id_to_index.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ApplicationCache::Action action {};
|
||||||
|
action.name = std::string(get_actions.getColumn(1).getString());
|
||||||
|
|
||||||
|
if (!get_actions.getColumn(2).isNull())
|
||||||
|
action.exec = std::string(get_actions.getColumn(2).getString());
|
||||||
|
else
|
||||||
|
action.exec.reset();
|
||||||
|
|
||||||
|
if (!get_actions.getColumn(3).isNull()) {
|
||||||
|
auto const str = get_actions.getColumn(3).getString();
|
||||||
|
if (str.at(0) == '/') {
|
||||||
|
action.icon
|
||||||
|
= std::filesystem::canonical(std::filesystem::path(str));
|
||||||
|
} else {
|
||||||
|
action.icon = str;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
action.icon.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_apps[it->second].actions.push_back(std::move(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
66
src/Cache.hpp
Normal file
66
src/Cache.hpp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "InotifyWatcher.hpp"
|
||||||
|
#include <SQLiteCpp/Database.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
struct ApplicationCache {
|
||||||
|
enum class Type {
|
||||||
|
Application,
|
||||||
|
Link,
|
||||||
|
Directory,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Action {
|
||||||
|
std::string name;
|
||||||
|
// May not exist if DBusActivable=true
|
||||||
|
std::optional<std::string> exec;
|
||||||
|
// Freedesktop Desktop Entry Spec 11.2 Table 3 says:
|
||||||
|
//
|
||||||
|
// If the name is an absolute path, the given file will be used.
|
||||||
|
// If the name is not an absolute path, the algorithm described in
|
||||||
|
// the Icon Theme Specification will be used to locate the icon.
|
||||||
|
//
|
||||||
|
// Thus, when deserializing, we will just check if it starts with /
|
||||||
|
// to determine type.
|
||||||
|
std::optional<std::variant<std::filesystem::path, std::string>> icon;
|
||||||
|
};
|
||||||
|
|
||||||
|
int id;
|
||||||
|
std::filesystem::path desktop_entry_path;
|
||||||
|
|
||||||
|
Type type { Type::Application };
|
||||||
|
bool terminal { false };
|
||||||
|
bool no_display { false };
|
||||||
|
std::optional<std::string> path;
|
||||||
|
std::optional<std::string> comment;
|
||||||
|
std::vector<Action> actions; // There should always be at least 1.
|
||||||
|
bool dbus_activatable {}; // Unimplemented for now.
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Cache {
|
||||||
|
explicit Cache(std::shared_ptr<SQLite::Database> db);
|
||||||
|
~Cache();
|
||||||
|
|
||||||
|
void rescan();
|
||||||
|
void dump();
|
||||||
|
void load();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<ApplicationCache> m_apps;
|
||||||
|
std::vector<std::filesystem::path> m_app_dirs;
|
||||||
|
std::shared_ptr<SQLite::Database> m_db;
|
||||||
|
|
||||||
|
InotifyWatcher m_inotify;
|
||||||
|
std::thread m_inotify_thread;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
47
src/Config.cpp
Normal file
47
src/Config.cpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#include "Config.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <print>
|
||||||
|
|
||||||
|
#include <toml++/toml.hpp>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
void Config::write(std::filesystem::path const &path)
|
||||||
|
{
|
||||||
|
std::ofstream f(path);
|
||||||
|
if (!f) {
|
||||||
|
throw std::runtime_error("Failed to open config file for writing");
|
||||||
|
}
|
||||||
|
std::println(f, "[settings]");
|
||||||
|
std::println(f, "terminal_cmdline=\"{}\"", terminal_cmdline);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Config::load(std::filesystem::path const &config_dir_path) -> Config const
|
||||||
|
{
|
||||||
|
if (!std::filesystem::is_directory(config_dir_path))
|
||||||
|
throw std::runtime_error("Provided path is not a directory!");
|
||||||
|
|
||||||
|
Config cfg {};
|
||||||
|
|
||||||
|
std::filesystem::path path_config { config_dir_path / "config.toml" };
|
||||||
|
if (!std::filesystem::is_regular_file(path_config)) {
|
||||||
|
try {
|
||||||
|
std::filesystem::remove_all(path_config);
|
||||||
|
} catch (std::exception const &e) {
|
||||||
|
}
|
||||||
|
cfg.write(path_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::println("Config file: {}", path_config.string());
|
||||||
|
auto const tbl { toml::parse_file(path_config.string()) };
|
||||||
|
auto const terminal_cmdline { tbl["settings"]["terminal_cmdline"].value_or(
|
||||||
|
"kitty -c") };
|
||||||
|
|
||||||
|
cfg.terminal_cmdline = terminal_cmdline;
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
17
src/Config.hpp
Normal file
17
src/Config.hpp
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
std::string terminal_cmdline { "kitty" };
|
||||||
|
|
||||||
|
void write(std::filesystem::path const &path);
|
||||||
|
|
||||||
|
static auto load(std::filesystem::path const &config_dir_path)
|
||||||
|
-> Config const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#include <cstdio>
|
|
||||||
#include <dlfcn.h>
|
|
||||||
|
|
||||||
#include <ft2build.h>
|
|
||||||
#include FT_FREETYPE_H
|
|
||||||
|
|
||||||
using FtInitFn = FT_Error (*)(FT_Library *);
|
|
||||||
using FtDoneFn = FT_Error (*)(FT_Library);
|
|
||||||
|
|
||||||
extern "C" FT_Error FT_Init_FreeType(FT_Library *library)
|
|
||||||
{
|
|
||||||
static FtInitFn real_fn
|
|
||||||
= reinterpret_cast<FtInitFn>(dlsym(RTLD_NEXT, "FT_Init_FreeType"));
|
|
||||||
if (!real_fn) {
|
|
||||||
std::fprintf(stderr,
|
|
||||||
"FT_Init_FreeType hook: failed to locate real symbol\n");
|
|
||||||
return FT_Err_Invalid_Handle;
|
|
||||||
}
|
|
||||||
FT_Error error = real_fn(library);
|
|
||||||
std::fprintf(stderr, "FT_Init_FreeType -> %p (err=%d)\n",
|
|
||||||
library ? static_cast<void *>(*library) : nullptr, error);
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" FT_Error FT_Done_FreeType(FT_Library library)
|
|
||||||
{
|
|
||||||
static FtDoneFn real_fn
|
|
||||||
= reinterpret_cast<FtDoneFn>(dlsym(RTLD_NEXT, "FT_Done_FreeType"));
|
|
||||||
std::fprintf(stderr, "FT_Done_FreeType(%p)\n",
|
|
||||||
static_cast<void *>(library));
|
|
||||||
if (!real_fn) {
|
|
||||||
std::fprintf(stderr,
|
|
||||||
"FT_Done_FreeType hook: failed to locate real symbol\n");
|
|
||||||
return FT_Err_Invalid_Handle;
|
|
||||||
}
|
|
||||||
return real_fn(library);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
731
src/ImGui.cpp
731
src/ImGui.cpp
@@ -11,75 +11,65 @@
|
|||||||
|
|
||||||
#include <raylib.h>
|
#include <raylib.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct CodepointSpan {
|
struct CodepointSpan {
|
||||||
u32 codepoint {};
|
u32 codepoint {};
|
||||||
std::size_t start {};
|
usize start {};
|
||||||
std::size_t end {};
|
usize end {};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto decode_utf8(std::string_view text) -> std::vector<CodepointSpan>
|
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;
|
std::vector<CodepointSpan> spans;
|
||||||
std::size_t i = 0;
|
usize i = 0;
|
||||||
spans.reserve(text.size());
|
spans.reserve(text.size());
|
||||||
|
|
||||||
while (i < text.size()) {
|
while (i < text.size()) {
|
||||||
u8 const byte = static_cast<u8>(text[i]);
|
u8 b = static_cast<u8>(text[i]);
|
||||||
std::size_t const start = i;
|
usize len = 1;
|
||||||
std::size_t length = 1;
|
|
||||||
u32 cp = 0xFFFD;
|
|
||||||
|
|
||||||
if (byte < 0x80) {
|
if (b < 0x80)
|
||||||
cp = byte;
|
len = 1;
|
||||||
} else if ((byte & 0xE0) == 0xC0) {
|
else if ((b & 0xE0) == 0xC0)
|
||||||
if (i + 1 < text.size()) {
|
len = 2;
|
||||||
u8 const b1 = static_cast<u8>(text[i + 1]);
|
else if ((b & 0xF0) == 0xE0)
|
||||||
if ((b1 & 0xC0) == 0x80) {
|
len = 3;
|
||||||
u32 const t = ((static_cast<u32>(byte) & 0x1F) << 6)
|
else if ((b & 0xF8) == 0xF0)
|
||||||
| (static_cast<u32>(b1) & 0x3F);
|
len = 4;
|
||||||
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) {
|
|
||||||
u32 const t = ((static_cast<u32>(byte) & 0x0F) << 12)
|
|
||||||
| ((static_cast<u32>(b1) & 0x3F) << 6)
|
|
||||||
| (static_cast<u32>(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) {
|
|
||||||
u32 const t = ((static_cast<u32>(byte) & 0x07) << 18)
|
|
||||||
| ((static_cast<u32>(b1) & 0x3F) << 12)
|
|
||||||
| ((static_cast<u32>(b2) & 0x3F) << 6)
|
|
||||||
| (static_cast<u32>(b3) & 0x3F);
|
|
||||||
if (t >= 0x10000 && t <= 0x10FFFF) {
|
|
||||||
cp = t;
|
|
||||||
length = 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spans.push_back(CodepointSpan { cp, start, start + length });
|
if (i + len > text.size())
|
||||||
i += length;
|
len = 1;
|
||||||
|
|
||||||
|
u32 cp = utf8_rune_from_first(text.data() + i);
|
||||||
|
spans.push_back({ cp, i, i + len });
|
||||||
|
i += len;
|
||||||
}
|
}
|
||||||
|
|
||||||
return spans;
|
return spans;
|
||||||
@@ -111,9 +101,30 @@ auto encode_utf8(u32 cp) -> std::string
|
|||||||
return std::string(buf, len);
|
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 HORIZONTAL_PADDING = 6.0f;
|
||||||
constexpr float VERTICAL_PADDING = 4.0f;
|
constexpr float VERTICAL_PADDING = 4.0f;
|
||||||
constexpr float CARET_WIDTH = 2.0f;
|
|
||||||
constexpr double CARET_BLINK_INTERVAL = 0.5;
|
constexpr double CARET_BLINK_INTERVAL = 0.5;
|
||||||
constexpr float CARET_DESCENT_FRACTION = 0.25f;
|
constexpr float CARET_DESCENT_FRACTION = 0.25f;
|
||||||
|
|
||||||
@@ -124,18 +135,156 @@ ImGui::ImGui(std::shared_ptr<TextRenderer> text_renderer)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImGui::begin(u32 const rune, bool ctrl, bool shift)
|
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_rune = rune;
|
||||||
m_ctrl = ctrl;
|
m_ctrl = ctrl;
|
||||||
m_shift = shift;
|
m_shift = shift;
|
||||||
|
m_clipboard = clipboard;
|
||||||
|
m_clipboard_set = clipboard_set;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImGui::end() { }
|
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; }
|
void ImGui::set_font(FontHandle font) { m_font = font; }
|
||||||
|
|
||||||
auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
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>
|
TextInputOptions options) -> std::bitset<2>
|
||||||
{
|
{
|
||||||
assert(id != 0);
|
assert(id != 0);
|
||||||
@@ -145,23 +294,22 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
|||||||
bool submitted { false };
|
bool submitted { false };
|
||||||
bool changed { false };
|
bool changed { false };
|
||||||
|
|
||||||
auto &state = m_ti_states[id];
|
auto &state { m_ti_states[id] };
|
||||||
|
|
||||||
assert(!options.multiline && "Multiline not yet implemented.");
|
assert(!options.multiline && "Multiline not yet implemented.");
|
||||||
|
|
||||||
if (m_focused_id == 0)
|
if (m_focused_id == 0)
|
||||||
m_focused_id = id;
|
m_focused_id = id;
|
||||||
|
|
||||||
if (options.font_size > rec.height) {
|
if (style().font_size > rec.height) {
|
||||||
TraceLog(LOG_WARNING,
|
TraceLog(LOG_WARNING,
|
||||||
std::format("Text size for text input {} is bigger than height ({} "
|
std::format("Text size for text input {} is bigger than height ({} "
|
||||||
"> {}). Clipping will occur.",
|
"> {}). Clipping will occur.",
|
||||||
id, options.font_size, rec.height)
|
id, style().font_size, rec.height)
|
||||||
.c_str());
|
.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string_view str_view(str.data(), str.size());
|
std::string_view str_view(str.data(), str.size());
|
||||||
auto spans = decode_utf8(str_view);
|
auto spans { decode_utf8(str_view) };
|
||||||
|
|
||||||
auto is_space = [](u32 cp) -> bool {
|
auto is_space = [](u32 cp) -> bool {
|
||||||
if (cp == '\n' || cp == '\r' || cp == '\t' || cp == '\v' || cp == '\f')
|
if (cp == '\n' || cp == '\r' || cp == '\t' || cp == '\v' || cp == '\f')
|
||||||
@@ -171,15 +319,15 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto clamp_cursor = [&]() -> std::size_t {
|
auto clamp_cursor = [&]() -> usize {
|
||||||
int const max_idx = static_cast<int>(spans.size());
|
int const max_idx = static_cast<int>(spans.size());
|
||||||
state.current_rune_idx = std::clamp(state.current_rune_idx, 0, max_idx);
|
state.current_rune_idx = std::clamp(state.current_rune_idx, 0, max_idx);
|
||||||
if (state.current_rune_idx == max_idx)
|
if (state.current_rune_idx == max_idx)
|
||||||
return str.size();
|
return str.size();
|
||||||
return spans[state.current_rune_idx].start;
|
return spans[(usize)state.current_rune_idx].start;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::size_t caret_byte = clamp_cursor();
|
usize caret_byte = clamp_cursor();
|
||||||
|
|
||||||
auto refresh_spans = [&]() {
|
auto refresh_spans = [&]() {
|
||||||
str_view = std::string_view(str.data(), str.size());
|
str_view = std::string_view(str.data(), str.size());
|
||||||
@@ -187,44 +335,79 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
|||||||
caret_byte = clamp_cursor();
|
caret_byte = clamp_cursor();
|
||||||
};
|
};
|
||||||
|
|
||||||
auto erase_range = [&](std::size_t byte_begin, std::size_t byte_end) {
|
auto erase_range = [&](usize byte_begin, usize byte_end) {
|
||||||
if (byte_end > byte_begin && byte_begin < str.size()) {
|
if (byte_end > byte_begin && byte_begin < str.size()) {
|
||||||
str.erase(byte_begin, byte_end - byte_begin);
|
str.erase(byte_begin, byte_end - byte_begin);
|
||||||
changed = true;
|
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;
|
bool caret_activity = false;
|
||||||
|
|
||||||
if (m_focused_id == id && m_rune != 0) {
|
if (m_focused_id == id && m_rune != 0) {
|
||||||
bool request_refresh = false;
|
bool request_refresh = false;
|
||||||
|
|
||||||
auto handle_backspace = [&]() {
|
auto handle_backspace = [&]() {
|
||||||
if (state.current_rune_idx <= 0
|
if (state.current_rune_idx <= 0
|
||||||
|| state.current_rune_idx > static_cast<int>(spans.size()))
|
|| state.current_rune_idx > (int)spans.size())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (m_ctrl) {
|
if (m_ctrl) {
|
||||||
int idx = state.current_rune_idx;
|
int idx = state.current_rune_idx, scan = idx - 1;
|
||||||
int scan = idx - 1;
|
while (scan >= 0 && is_space(spans[(usize)scan].codepoint))
|
||||||
while (scan >= 0
|
|
||||||
&& is_space(
|
|
||||||
spans[static_cast<std::size_t>(scan)].codepoint))
|
|
||||||
scan--;
|
scan--;
|
||||||
while (scan >= 0
|
while (scan >= 0 && !is_space(spans[(usize)scan].codepoint))
|
||||||
&& !is_space(
|
|
||||||
spans[static_cast<std::size_t>(scan)].codepoint))
|
|
||||||
scan--;
|
scan--;
|
||||||
int start_idx = std::max(scan + 1, 0);
|
int start_idx = std::max(scan + 1, 0);
|
||||||
std::size_t byte_begin
|
usize b = spans[(usize)start_idx].start;
|
||||||
= spans[static_cast<std::size_t>(start_idx)].start;
|
usize e = (idx >= (int)spans.size()) ? str.size()
|
||||||
std::size_t byte_end = (idx >= static_cast<int>(spans.size()))
|
: spans[(usize)idx].start;
|
||||||
? str.size()
|
erase_range(b, e);
|
||||||
: spans[static_cast<std::size_t>(idx)].start;
|
|
||||||
erase_range(byte_begin, byte_end);
|
|
||||||
state.current_rune_idx = start_idx;
|
state.current_rune_idx = start_idx;
|
||||||
} else {
|
} else {
|
||||||
auto const &prev = spans[static_cast<std::size_t>(
|
auto const &prev = spans[(usize)(state.current_rune_idx - 1)];
|
||||||
state.current_rune_idx - 1)];
|
|
||||||
erase_range(prev.start, prev.end);
|
erase_range(prev.start, prev.end);
|
||||||
state.current_rune_idx--;
|
state.current_rune_idx--;
|
||||||
}
|
}
|
||||||
@@ -233,93 +416,153 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
|||||||
|
|
||||||
auto handle_delete = [&]() {
|
auto handle_delete = [&]() {
|
||||||
if (state.current_rune_idx < 0
|
if (state.current_rune_idx < 0
|
||||||
|| state.current_rune_idx >= static_cast<int>(spans.size())) {
|
|| state.current_rune_idx >= (int)spans.size()) {
|
||||||
if (!m_ctrl)
|
if (!m_ctrl)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int idx = state.current_rune_idx;
|
int idx = state.current_rune_idx;
|
||||||
if (m_ctrl) {
|
if (m_ctrl) {
|
||||||
int scan = idx;
|
int scan = idx;
|
||||||
while (scan < static_cast<int>(spans.size())
|
while (scan < (int)spans.size()
|
||||||
&& is_space(
|
&& is_space(spans[(usize)scan].codepoint))
|
||||||
spans[static_cast<std::size_t>(scan)].codepoint))
|
|
||||||
scan++;
|
scan++;
|
||||||
while (scan < static_cast<int>(spans.size())
|
while (scan < (int)spans.size()
|
||||||
&& !is_space(
|
&& !is_space(spans[(usize)scan].codepoint))
|
||||||
spans[static_cast<std::size_t>(scan)].codepoint))
|
|
||||||
scan++;
|
scan++;
|
||||||
std::size_t byte_begin = (idx < static_cast<int>(spans.size()))
|
usize b = (idx < (int)spans.size()) ? spans[(usize)idx].start
|
||||||
? spans[static_cast<std::size_t>(idx)].start
|
: str.size();
|
||||||
: str.size();
|
usize e = (scan < (int)spans.size()) ? spans[(usize)scan].start
|
||||||
std::size_t byte_end = (scan < static_cast<int>(spans.size()))
|
: str.size();
|
||||||
? spans[static_cast<std::size_t>(scan)].start
|
erase_range(b, e);
|
||||||
: str.size();
|
} else if (idx < (int)spans.size()) {
|
||||||
erase_range(byte_begin, byte_end);
|
auto const &curr = spans[(usize)idx];
|
||||||
} else if (idx < static_cast<int>(spans.size())) {
|
|
||||||
auto const &curr = spans[static_cast<std::size_t>(idx)];
|
|
||||||
erase_range(curr.start, curr.end);
|
erase_range(curr.start, curr.end);
|
||||||
}
|
}
|
||||||
request_refresh = true;
|
request_refresh = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool extend = m_shift;
|
||||||
|
|
||||||
switch (m_rune) {
|
switch (m_rune) {
|
||||||
case 1: // Left (H)
|
case 1: // Left
|
||||||
|
if (!extend)
|
||||||
|
state.clear_selection();
|
||||||
if (state.current_rune_idx > 0) {
|
if (state.current_rune_idx > 0) {
|
||||||
state.current_rune_idx--;
|
if (extend && state.sel_anchor_idx == -1)
|
||||||
if (m_ctrl) {
|
state.sel_anchor_idx = state.current_rune_idx;
|
||||||
while (state.current_rune_idx > 0
|
if (m_ctrl)
|
||||||
&& is_space(spans[static_cast<std::size_t>(
|
move_left_word();
|
||||||
state.current_rune_idx)]
|
else
|
||||||
.codepoint))
|
state.current_rune_idx--;
|
||||||
state.current_rune_idx--;
|
|
||||||
while (state.current_rune_idx > 0
|
|
||||||
&& !is_space(spans[static_cast<std::size_t>(
|
|
||||||
state.current_rune_idx)]
|
|
||||||
.codepoint))
|
|
||||||
state.current_rune_idx--;
|
|
||||||
}
|
|
||||||
caret_byte = clamp_cursor();
|
caret_byte = clamp_cursor();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 4: // Right (L)
|
case 4: // Right
|
||||||
if (state.current_rune_idx < static_cast<int>(spans.size())) {
|
if (!extend)
|
||||||
state.current_rune_idx++;
|
state.clear_selection();
|
||||||
if (m_ctrl) {
|
if (state.current_rune_idx < (int)spans.size()) {
|
||||||
while (
|
if (extend && state.sel_anchor_idx == -1)
|
||||||
state.current_rune_idx < static_cast<int>(spans.size())
|
state.sel_anchor_idx = state.current_rune_idx;
|
||||||
&& is_space(spans[static_cast<std::size_t>(
|
if (m_ctrl)
|
||||||
state.current_rune_idx - 1)]
|
move_right_word();
|
||||||
.codepoint))
|
else
|
||||||
state.current_rune_idx++;
|
state.current_rune_idx++;
|
||||||
while (
|
|
||||||
state.current_rune_idx < static_cast<int>(spans.size())
|
|
||||||
&& !is_space(spans[static_cast<std::size_t>(
|
|
||||||
state.current_rune_idx - 1)]
|
|
||||||
.codepoint))
|
|
||||||
state.current_rune_idx++;
|
|
||||||
}
|
|
||||||
caret_byte = clamp_cursor();
|
caret_byte = clamp_cursor();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3: // Up (K)
|
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;
|
state.current_rune_idx = 0;
|
||||||
caret_byte = clamp_cursor();
|
caret_byte = clamp_cursor();
|
||||||
break;
|
break;
|
||||||
case 2: // Down (J)
|
case 2: // Down -> end
|
||||||
state.current_rune_idx = static_cast<int>(spans.size());
|
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();
|
caret_byte = clamp_cursor();
|
||||||
break;
|
break;
|
||||||
case 8: // Backspace
|
case 8: // Backspace
|
||||||
|
if (erase_selection_if_any()) {
|
||||||
|
request_refresh = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
handle_backspace();
|
handle_backspace();
|
||||||
break;
|
break;
|
||||||
case 0x7F: // Delete
|
case 0x7F: // Delete
|
||||||
|
if (erase_selection_if_any()) {
|
||||||
|
request_refresh = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
handle_delete();
|
handle_delete();
|
||||||
break;
|
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 '\r':
|
||||||
case '\n':
|
case '\n':
|
||||||
if (options.multiline) {
|
if (options.multiline) {
|
||||||
auto encoded = encode_utf8('\n');
|
erase_selection_if_any();
|
||||||
|
auto encoded { encode_utf8('\n') };
|
||||||
if (!encoded.empty()) {
|
if (!encoded.empty()) {
|
||||||
str.insert(caret_byte, encoded);
|
str.insert(caret_byte, encoded);
|
||||||
state.current_rune_idx++;
|
state.current_rune_idx++;
|
||||||
@@ -331,8 +574,10 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
insert_printable:
|
||||||
if (m_rune >= 0x20) {
|
if (m_rune >= 0x20) {
|
||||||
auto encoded = encode_utf8(m_rune);
|
erase_selection_if_any();
|
||||||
|
auto encoded { encode_utf8(m_rune) };
|
||||||
if (!encoded.empty()) {
|
if (!encoded.empty()) {
|
||||||
str.insert(caret_byte, encoded);
|
str.insert(caret_byte, encoded);
|
||||||
state.current_rune_idx++;
|
state.current_rune_idx++;
|
||||||
@@ -343,17 +588,21 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request_refresh) {
|
if (request_refresh)
|
||||||
refresh_spans();
|
refresh_spans();
|
||||||
} else {
|
else
|
||||||
caret_byte = clamp_cursor();
|
caret_byte = clamp_cursor();
|
||||||
}
|
|
||||||
caret_activity = true;
|
caret_activity = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
double const dt = static_cast<double>(GetFrameTime());
|
state.caret_byte = caret_byte;
|
||||||
|
|
||||||
|
double const dt = (double)GetFrameTime();
|
||||||
if (m_focused_id == id) {
|
if (m_focused_id == id) {
|
||||||
if (caret_activity) {
|
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_timer = 0.0;
|
||||||
state.caret_visible = true;
|
state.caret_visible = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -361,8 +610,7 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
|||||||
state.caret_visible = true;
|
state.caret_visible = true;
|
||||||
state.caret_timer += dt;
|
state.caret_timer += dt;
|
||||||
if (state.caret_timer >= CARET_BLINK_INTERVAL) {
|
if (state.caret_timer >= CARET_BLINK_INTERVAL) {
|
||||||
int toggles = static_cast<int>(
|
int toggles = (int)(state.caret_timer / CARET_BLINK_INTERVAL);
|
||||||
state.caret_timer / CARET_BLINK_INTERVAL);
|
|
||||||
state.caret_timer
|
state.caret_timer
|
||||||
= std::fmod(state.caret_timer, CARET_BLINK_INTERVAL);
|
= std::fmod(state.caret_timer, CARET_BLINK_INTERVAL);
|
||||||
if (toggles % 2 == 1)
|
if (toggles % 2 == 1)
|
||||||
@@ -374,56 +622,27 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
|||||||
state.caret_timer = 0.0;
|
state.caret_timer = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector2 prefix_metrics { 0.0f, 0.0f };
|
|
||||||
Vector2 full_metrics { 0.0f, 0.0f };
|
|
||||||
if (m_font.has_value() && m_text_renderer) {
|
|
||||||
std::string_view const prefix_view(str.data(), caret_byte);
|
|
||||||
prefix_metrics = m_text_renderer->measure_text(
|
|
||||||
*m_font, prefix_view, static_cast<int>(options.font_size));
|
|
||||||
full_metrics = m_text_renderer->measure_text(
|
|
||||||
*m_font, str_view, static_cast<int>(options.font_size));
|
|
||||||
}
|
|
||||||
|
|
||||||
state.cursor_position.x = prefix_metrics.x;
|
|
||||||
|
|
||||||
float const available_width
|
|
||||||
= std::max(0.0f, rec.width - 2.0f * HORIZONTAL_PADDING);
|
|
||||||
if (full_metrics.x <= available_width) {
|
|
||||||
state.scroll_offset.x = 0.0f;
|
|
||||||
} else {
|
|
||||||
float &scroll = state.scroll_offset.x;
|
|
||||||
float caret_local = state.cursor_position.x - scroll;
|
|
||||||
if (caret_local > available_width) {
|
|
||||||
scroll = state.cursor_position.x - available_width;
|
|
||||||
} else if (caret_local < 0.0f) {
|
|
||||||
scroll = state.cursor_position.x;
|
|
||||||
}
|
|
||||||
scroll = std::clamp(
|
|
||||||
scroll, 0.0f, std::max(0.0f, full_metrics.x - available_width));
|
|
||||||
}
|
|
||||||
state.scroll_offset.y = 0.0f;
|
|
||||||
|
|
||||||
Color const bg_col { 16, 16, 16, 100 };
|
Color const bg_col { 16, 16, 16, 100 };
|
||||||
Color const border_col { 220, 220, 220, 180 };
|
Color const border_col { 220, 220, 220, 180 };
|
||||||
DrawRectangleRec(rec, bg_col);
|
DrawRectangleRec(rec, bg_col);
|
||||||
DrawRectangleLinesEx(rec, 1.0f, border_col);
|
DrawRectangleLinesEx(rec, 1.0f, border_col);
|
||||||
|
|
||||||
float const text_top = rec.y + VERTICAL_PADDING;
|
float const text_top = rec.y + VERTICAL_PADDING;
|
||||||
float const baseline_y = text_top + options.font_size;
|
float const baseline_y = text_top + style().font_size;
|
||||||
float const max_caret_height
|
float const max_caret_h
|
||||||
= std::max(0.0f, rec.height - 2.0f * VERTICAL_PADDING);
|
= std::max(0.0f, rec.height - 2.0f * VERTICAL_PADDING);
|
||||||
float caret_height = options.font_size * (1.0f + CARET_DESCENT_FRACTION);
|
float caret_height = style().font_size * (1.0f + CARET_DESCENT_FRACTION);
|
||||||
caret_height = std::min(caret_height,
|
caret_height = std::min(
|
||||||
max_caret_height > 0.0f ? max_caret_height : caret_height);
|
caret_height, max_caret_h > 0.0f ? max_caret_h : caret_height);
|
||||||
if (caret_height <= 0.0f)
|
if (caret_height <= 0.0f)
|
||||||
caret_height = options.font_size;
|
caret_height = style().font_size;
|
||||||
float caret_top = baseline_y - options.font_size;
|
float caret_top = baseline_y - style().font_size;
|
||||||
float const min_top = rec.y + VERTICAL_PADDING;
|
float const min_top = rec.y + VERTICAL_PADDING;
|
||||||
float const max_top = rec.y + rec.height - VERTICAL_PADDING - caret_height;
|
float const max_top = rec.y + rec.height - VERTICAL_PADDING - caret_height;
|
||||||
caret_top = std::clamp(caret_top, min_top, max_top);
|
caret_top = std::clamp(caret_top, min_top, max_top);
|
||||||
float caret_bottom = caret_top + caret_height;
|
float caret_bottom = caret_top + caret_height;
|
||||||
float const desired_bottom
|
float const desired_bottom
|
||||||
= baseline_y + options.font_size * CARET_DESCENT_FRACTION;
|
= baseline_y + style().font_size * CARET_DESCENT_FRACTION;
|
||||||
if (caret_bottom < desired_bottom) {
|
if (caret_bottom < desired_bottom) {
|
||||||
float const adjust = desired_bottom - caret_bottom;
|
float const adjust = desired_bottom - caret_bottom;
|
||||||
caret_top = std::min(caret_top + adjust, max_top);
|
caret_top = std::min(caret_top + adjust, max_top);
|
||||||
@@ -431,33 +650,143 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
|||||||
}
|
}
|
||||||
state.cursor_position.y = caret_top;
|
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);
|
BeginScissorMode(rec.x, rec.y, rec.width, rec.height);
|
||||||
{
|
{
|
||||||
if (m_font.has_value() && m_text_renderer) {
|
if (m_font.has_value() && m_text_renderer) {
|
||||||
Vector2 const text_pos {
|
Color const &text_color = style().text_color;
|
||||||
rec.x + HORIZONTAL_PADDING - state.scroll_offset.x,
|
|
||||||
baseline_y,
|
std::string display;
|
||||||
};
|
display.reserve(
|
||||||
Color const text_color { 255, 255, 255, 255 };
|
str.size() + (has_preedit ? state.preedit_text.size() : 0));
|
||||||
m_text_renderer->draw_text(*m_font, str_view, text_pos,
|
display.append(std::string_view(str.data(), caret_byte));
|
||||||
static_cast<int>(options.font_size), text_color);
|
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) {
|
if (m_focused_id == id && state.caret_visible) {
|
||||||
float const caret_x = std::round(rec.x + HORIZONTAL_PADDING
|
float const caret_x = std::floor(origin + caret_offset + 0.5f);
|
||||||
+ state.cursor_position.x - state.scroll_offset.x);
|
Vector2 const p0 { caret_x, std::floor(caret_top + 0.5f) };
|
||||||
Rectangle caret_rect {
|
Vector2 const p1 { caret_x,
|
||||||
caret_x,
|
std::floor((caret_top + caret_height) + 0.5f) };
|
||||||
caret_top,
|
DrawLineV(p0, p1, text_color);
|
||||||
CARET_WIDTH,
|
|
||||||
caret_height,
|
|
||||||
};
|
|
||||||
Color const caret_color { 255, 255, 255, 200 };
|
|
||||||
DrawRectangleRec(caret_rect, caret_color);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EndScissorMode();
|
EndScissorMode();
|
||||||
|
|
||||||
|
if (state.external_change) {
|
||||||
|
changed = true;
|
||||||
|
state.external_change = false;
|
||||||
|
}
|
||||||
|
|
||||||
return std::bitset<2> { static_cast<unsigned long long>(
|
return std::bitset<2> { static_cast<unsigned long long>(
|
||||||
(submitted ? 1 : 0) | (changed ? 2 : 0)) };
|
(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
|
||||||
|
|||||||
114
src/ImGui.hpp
114
src/ImGui.hpp
@@ -1,39 +1,96 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <bitset>
|
#include <bitset>
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <queue>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include <raylib.h>
|
#include <raylib.h>
|
||||||
|
|
||||||
#include "TextRenderer.hpp"
|
#include "TextRenderer.hpp"
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
constexpr float DEFAULT_FONT_SIZE { 24 };
|
constexpr float DEFAULT_FONT_SIZE { 24 };
|
||||||
|
|
||||||
struct TextInputOptions {
|
struct TextInputOptions {
|
||||||
bool multiline { false };
|
bool multiline { false };
|
||||||
float font_size { DEFAULT_FONT_SIZE };
|
};
|
||||||
|
|
||||||
|
struct ListViewOptions {
|
||||||
|
bool selectable { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ImGui {
|
struct ImGui {
|
||||||
ImGui(std::shared_ptr<TextRenderer> text_renderer);
|
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;
|
ImGui(ImGui const &) = delete;
|
||||||
auto operator=(ImGui const &) -> ImGui & = delete;
|
auto operator=(ImGui const &) -> ImGui & = delete;
|
||||||
ImGui(ImGui &&) = default;
|
ImGui(ImGui &&) = default;
|
||||||
auto operator=(ImGui &&) -> ImGui & = default;
|
auto operator=(ImGui &&) -> ImGui & = default;
|
||||||
|
|
||||||
void begin(u32 const rune, bool ctrl, bool shift);
|
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();
|
void end();
|
||||||
|
|
||||||
// Bit 0 -> Submitted
|
// Bit 0 -> Submitted
|
||||||
// Bit 1 -> String changed
|
// Bit 1 -> String changed
|
||||||
auto text_input(std::size_t id, std::pmr::string &str, Rectangle rec,
|
auto text_input(usize id, std::pmr::string &str, Rectangle rec,
|
||||||
TextInputOptions options = {}) -> std::bitset<2>;
|
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);
|
void set_font(FontHandle font);
|
||||||
|
|
||||||
[[nodiscard]] inline auto id(std::string_view const str) -> std::size_t
|
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;
|
std::hash<std::string_view> hasher;
|
||||||
return hasher(str);
|
return hasher(str);
|
||||||
@@ -42,17 +99,49 @@ struct ImGui {
|
|||||||
private:
|
private:
|
||||||
struct TextInputState {
|
struct TextInputState {
|
||||||
int current_rune_idx { 0 };
|
int current_rune_idx { 0 };
|
||||||
Vector2 scroll_offset; // y not used if multiline == false
|
|
||||||
Vector2 cursor_position; // y not used if multiline == false
|
// y not used if multiline == false
|
||||||
|
Vector2 scroll_offset { 0, 0 };
|
||||||
|
Vector2 cursor_position { 0, 0 };
|
||||||
|
|
||||||
bool caret_visible { true };
|
bool caret_visible { true };
|
||||||
double caret_timer { 0.0 };
|
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; }
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unordered_map<std::size_t, TextInputState> m_ti_states;
|
struct ListViewState {
|
||||||
std::size_t m_focused_id {};
|
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
|
u32 m_rune {}; // 1234 <-> hjkl arrow keys
|
||||||
bool m_ctrl {};
|
bool m_ctrl {};
|
||||||
bool m_shift {};
|
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::optional<FontHandle> m_font {};
|
||||||
std::shared_ptr<TextRenderer> m_text_renderer {};
|
std::shared_ptr<TextRenderer> m_text_renderer {};
|
||||||
@@ -60,13 +149,16 @@ private:
|
|||||||
|
|
||||||
struct ImGuiGuard {
|
struct ImGuiGuard {
|
||||||
ImGuiGuard(std::shared_ptr<ImGui> imgui, u32 const rune, bool const ctrl,
|
ImGuiGuard(std::shared_ptr<ImGui> imgui, u32 const rune, bool const ctrl,
|
||||||
bool const shift)
|
bool const shift, std::string_view const clipboard,
|
||||||
|
std::function<void(std::string_view const &)> clipboard_set)
|
||||||
: m_imgui(imgui)
|
: m_imgui(imgui)
|
||||||
{
|
{
|
||||||
m_imgui->begin(rune, ctrl, shift);
|
m_imgui->begin(rune, ctrl, shift, clipboard, clipboard_set);
|
||||||
}
|
}
|
||||||
~ImGuiGuard() { m_imgui->end(); }
|
~ImGuiGuard() { m_imgui->end(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::shared_ptr<ImGui> m_imgui { nullptr };
|
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
|
||||||
@@ -37,11 +37,13 @@
|
|||||||
#include <ext/import-font.h>
|
#include <ext/import-font.h>
|
||||||
#include <msdfgen.h>
|
#include <msdfgen.h>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr int kAtlasDimension = 1024;
|
constexpr int ATLAS_DIMENSION = 1024;
|
||||||
constexpr int kAtlasPadding = 2;
|
constexpr int ATLAS_PADDING = 2;
|
||||||
constexpr float kDefaultEmScale = 48.0f;
|
constexpr float DEFAULT_EM_SCALE = 48.0f;
|
||||||
|
|
||||||
constexpr float hb_to_em(hb_position_t value, unsigned upem)
|
constexpr float hb_to_em(hb_position_t value, unsigned upem)
|
||||||
{
|
{
|
||||||
@@ -67,18 +69,18 @@ auto ft_library() -> FT_Library
|
|||||||
|
|
||||||
struct CodepointSpan {
|
struct CodepointSpan {
|
||||||
uint32_t codepoint {};
|
uint32_t codepoint {};
|
||||||
size_t start {};
|
usize start {};
|
||||||
size_t end {};
|
usize end {};
|
||||||
};
|
};
|
||||||
|
|
||||||
auto decode_utf8(std::string_view text) -> std::vector<CodepointSpan>
|
auto decode_utf8(std::string_view text) -> std::vector<CodepointSpan>
|
||||||
{
|
{
|
||||||
std::vector<CodepointSpan> spans;
|
std::vector<CodepointSpan> spans;
|
||||||
size_t i = 0;
|
usize i = 0;
|
||||||
while (i < text.size()) {
|
while (i < text.size()) {
|
||||||
u8 const byte = static_cast<u8>(text[i]);
|
u8 const byte = static_cast<u8>(text[i]);
|
||||||
size_t const start = i;
|
usize const start = i;
|
||||||
size_t length = 1;
|
usize length = 1;
|
||||||
uint32_t cp = 0xFFFD;
|
uint32_t cp = 0xFFFD;
|
||||||
if (byte < 0x80) {
|
if (byte < 0x80) {
|
||||||
cp = byte;
|
cp = byte;
|
||||||
@@ -143,8 +145,8 @@ auto TextRenderer::flush_font(FontRuntime &rt, FontData &fd) -> void
|
|||||||
{
|
{
|
||||||
rt.glyph_cache.clear();
|
rt.glyph_cache.clear();
|
||||||
fd.glyphs.clear();
|
fd.glyphs.clear();
|
||||||
rt.pen_x = kAtlasPadding;
|
rt.pen_x = ATLAS_PADDING;
|
||||||
rt.pen_y = kAtlasPadding;
|
rt.pen_y = ATLAS_PADDING;
|
||||||
rt.row_height = 0;
|
rt.row_height = 0;
|
||||||
if (fd.atlas_img.data)
|
if (fd.atlas_img.data)
|
||||||
ImageClearBackground(&fd.atlas_img, BLANK);
|
ImageClearBackground(&fd.atlas_img, BLANK);
|
||||||
@@ -156,20 +158,20 @@ auto TextRenderer::allocate_region(FontRuntime &rt, FontData &fd, int width,
|
|||||||
int height) -> std::optional<std::pair<int, int>>
|
int height) -> std::optional<std::pair<int, int>>
|
||||||
{
|
{
|
||||||
(void)fd;
|
(void)fd;
|
||||||
int padded_w = width + kAtlasPadding;
|
int padded_w = width + ATLAS_PADDING;
|
||||||
if (padded_w > rt.atlas_width || height + kAtlasPadding > rt.atlas_height)
|
if (padded_w > rt.atlas_width || height + ATLAS_PADDING > rt.atlas_height)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
if (rt.pen_x + padded_w > rt.atlas_width) {
|
if (rt.pen_x + padded_w > rt.atlas_width) {
|
||||||
rt.pen_x = kAtlasPadding;
|
rt.pen_x = ATLAS_PADDING;
|
||||||
rt.pen_y += rt.row_height;
|
rt.pen_y += rt.row_height;
|
||||||
rt.row_height = 0;
|
rt.row_height = 0;
|
||||||
}
|
}
|
||||||
if (rt.pen_y + height + kAtlasPadding > rt.atlas_height)
|
if (rt.pen_y + height + ATLAS_PADDING > rt.atlas_height)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
int x = rt.pen_x;
|
int x = rt.pen_x;
|
||||||
int y = rt.pen_y;
|
int y = rt.pen_y;
|
||||||
rt.pen_x += padded_w;
|
rt.pen_x += padded_w;
|
||||||
rt.row_height = std::max(rt.row_height, height + kAtlasPadding);
|
rt.row_height = std::max(rt.row_height, height + ATLAS_PADDING);
|
||||||
return std::pair { x, y };
|
return std::pair { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +214,8 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd,
|
|||||||
int bmp_h = std::max(
|
int bmp_h = std::max(
|
||||||
1, static_cast<int>(std::ceil(height_em * scale + 2.0 * rt.px_range)));
|
1, static_cast<int>(std::ceil(height_em * scale + 2.0 * rt.px_range)));
|
||||||
|
|
||||||
if (bmp_w + kAtlasPadding > rt.atlas_width
|
if (bmp_w + ATLAS_PADDING > rt.atlas_width
|
||||||
|| bmp_h + kAtlasPadding > rt.atlas_height) {
|
|| bmp_h + ATLAS_PADDING > rt.atlas_height) {
|
||||||
TraceLog(LOG_WARNING, "Glyph %u bitmap %dx%d exceeds atlas %dx%d",
|
TraceLog(LOG_WARNING, "Glyph %u bitmap %dx%d exceeds atlas %dx%d",
|
||||||
glyph_index, bmp_w, bmp_h, rt.atlas_width, rt.atlas_height);
|
glyph_index, bmp_w, bmp_h, rt.atlas_width, rt.atlas_height);
|
||||||
GlyphCacheEntry too_large {};
|
GlyphCacheEntry too_large {};
|
||||||
@@ -239,7 +241,7 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd,
|
|||||||
msdfgen::generateMSDF(
|
msdfgen::generateMSDF(
|
||||||
msdf_bitmap, shape, rt.px_range, scale_vec, translate);
|
msdf_bitmap, shape, rt.px_range, scale_vec, translate);
|
||||||
|
|
||||||
std::vector<Color> buffer(static_cast<size_t>(bmp_w) * bmp_h);
|
std::vector<Color> buffer(static_cast<usize>(bmp_w) * bmp_h);
|
||||||
// FIXME: Figure out shader
|
// FIXME: Figure out shader
|
||||||
// for (int y = 0; y < bmp_h; ++y) {
|
// for (int y = 0; y < bmp_h; ++y) {
|
||||||
// int const dst_y = bmp_h - 1 - y;
|
// int const dst_y = bmp_h - 1 - y;
|
||||||
@@ -248,11 +250,14 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd,
|
|||||||
// auto const r = msdfgen::pixelFloatToByte(px[0]);
|
// auto const r = msdfgen::pixelFloatToByte(px[0]);
|
||||||
// auto const g = msdfgen::pixelFloatToByte(px[1]);
|
// auto const g = msdfgen::pixelFloatToByte(px[1]);
|
||||||
// auto const b = msdfgen::pixelFloatToByte(px[2]);
|
// auto const b = msdfgen::pixelFloatToByte(px[2]);
|
||||||
// buffer[static_cast<size_t>(dst_y) * bmp_w + x]
|
// buffer[static_cast<usize>(dst_y) * bmp_w + x]
|
||||||
// = Color { r, g, b, 255 };
|
// = 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_white = 0;
|
||||||
auto sum_black = 0;
|
auto sum_black = 0;
|
||||||
for (int y = 0; y < bmp_h; ++y) {
|
for (int y = 0; y < bmp_h; ++y) {
|
||||||
@@ -266,16 +271,23 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bool flip { sum_white > sum_black && (float)bmp_w / (float)bmp_h > 0.6 };
|
||||||
|
if (c1 == c4) {
|
||||||
|
flip = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This really isn't the most accurate thing in the world but should work
|
||||||
|
// for now. Things like commas might be fucked.
|
||||||
for (int y = 0; y < bmp_h; ++y) {
|
for (int y = 0; y < bmp_h; ++y) {
|
||||||
int const dst_y = bmp_h - 1 - y;
|
int const dst_y = bmp_h - 1 - y;
|
||||||
for (int x = 0; x < bmp_w; ++x) {
|
for (int x = 0; x < bmp_w; ++x) {
|
||||||
float const *px = msdf_bitmap(x, y);
|
float const *px = msdf_bitmap(x, y);
|
||||||
auto const r = msdfgen::pixelFloatToByte(px[0]);
|
auto const r = msdfgen::pixelFloatToByte(px[0]);
|
||||||
if (sum_white > sum_black && (float)bmp_w / (float)bmp_h > 0.6) {
|
if (flip) {
|
||||||
buffer[static_cast<size_t>(dst_y) * bmp_w + x] = Color { 255,
|
buffer[static_cast<usize>(dst_y) * bmp_w + x] = Color { 255,
|
||||||
255, 255, static_cast<unsigned char>(255 - r) };
|
255, 255, static_cast<unsigned char>(255 - r) };
|
||||||
} else {
|
} else {
|
||||||
buffer[static_cast<size_t>(dst_y) * bmp_w + x]
|
buffer[static_cast<usize>(dst_y) * bmp_w + x]
|
||||||
= Color { 255, 255, 255, r };
|
= Color { 255, 255, 255, r };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,12 +348,12 @@ auto TextRenderer::ensure_glyph(FontRuntime &rt, FontData &fd, u32 glyph_index,
|
|||||||
TextRenderer::TextRenderer()
|
TextRenderer::TextRenderer()
|
||||||
{
|
{
|
||||||
static char const msdf_vs_data[] {
|
static char const msdf_vs_data[] {
|
||||||
#embed "base.vs"
|
#embed "base.vert"
|
||||||
, 0
|
, 0 // cppcheck-suppress syntaxError
|
||||||
};
|
};
|
||||||
static char const msdf_fs_data[] {
|
static char const msdf_fs_data[] {
|
||||||
#embed "msdf.fs"
|
#embed "msdf.frag"
|
||||||
, 0
|
, 0 // cppcheck-suppress syntaxError
|
||||||
};
|
};
|
||||||
m_msdf_shader = LoadShaderFromMemory(msdf_vs_data, msdf_fs_data);
|
m_msdf_shader = LoadShaderFromMemory(msdf_vs_data, msdf_fs_data);
|
||||||
assert(IsShaderValid(m_msdf_shader));
|
assert(IsShaderValid(m_msdf_shader));
|
||||||
@@ -540,13 +552,13 @@ auto TextRenderer::load_single_font(std::filesystem::path const &path)
|
|||||||
|
|
||||||
auto runtime = std::make_unique<FontRuntime>();
|
auto runtime = std::make_unique<FontRuntime>();
|
||||||
runtime->face = face;
|
runtime->face = face;
|
||||||
runtime->atlas_width = kAtlasDimension;
|
runtime->atlas_width = ATLAS_DIMENSION;
|
||||||
runtime->atlas_height = kAtlasDimension;
|
runtime->atlas_height = ATLAS_DIMENSION;
|
||||||
runtime->pen_x = kAtlasPadding;
|
runtime->pen_x = ATLAS_PADDING;
|
||||||
runtime->pen_y = kAtlasPadding;
|
runtime->pen_y = ATLAS_PADDING;
|
||||||
runtime->row_height = 0;
|
runtime->row_height = 0;
|
||||||
runtime->px_range = 0.05; // kDefaultPxRange;
|
runtime->px_range = 0.05; // kDefaultPxRange;
|
||||||
runtime->em_scale = kDefaultEmScale;
|
runtime->em_scale = DEFAULT_EM_SCALE;
|
||||||
runtime->frame_stamp = 0;
|
runtime->frame_stamp = 0;
|
||||||
runtime->units_per_em
|
runtime->units_per_em
|
||||||
= static_cast<unsigned>(face->units_per_EM ? face->units_per_EM : 2048);
|
= static_cast<unsigned>(face->units_per_EM ? face->units_per_EM : 2048);
|
||||||
@@ -659,9 +671,9 @@ auto TextRenderer::shape_text(FontHandle const font,
|
|||||||
|
|
||||||
constexpr usize kNoFont = std::numeric_limits<usize>::max();
|
constexpr usize kNoFont = std::numeric_limits<usize>::max();
|
||||||
std::vector<usize> selections(codepoints.size(), kNoFont);
|
std::vector<usize> selections(codepoints.size(), kNoFont);
|
||||||
for (size_t i = 0; i < codepoints.size(); ++i) {
|
for (usize i = 0; i < codepoints.size(); ++i) {
|
||||||
bool matched = false;
|
bool matched = false;
|
||||||
for (size_t candidate = 0; candidate < font_set.font_indices.size();
|
for (usize candidate = 0; candidate < font_set.font_indices.size();
|
||||||
++candidate) {
|
++candidate) {
|
||||||
usize runtime_index = font_set.font_indices[candidate];
|
usize runtime_index = font_set.font_indices[candidate];
|
||||||
if (runtime_index >= m_font_runtime.size())
|
if (runtime_index >= m_font_runtime.size())
|
||||||
@@ -681,9 +693,9 @@ auto TextRenderer::shape_text(FontHandle const font,
|
|||||||
selections[i] = kNoFont;
|
selections[i] = kNoFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t idx = 0;
|
usize idx = 0;
|
||||||
while (idx < codepoints.size()) {
|
while (idx < codepoints.size()) {
|
||||||
size_t font_choice = selections[idx];
|
usize font_choice = selections[idx];
|
||||||
if (font_choice == kNoFont) {
|
if (font_choice == kNoFont) {
|
||||||
++idx;
|
++idx;
|
||||||
continue;
|
continue;
|
||||||
@@ -698,9 +710,9 @@ auto TextRenderer::shape_text(FontHandle const font,
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t segment_start = codepoints[idx].start;
|
usize segment_start = codepoints[idx].start;
|
||||||
size_t segment_end = codepoints[idx].end;
|
usize segment_end = codepoints[idx].end;
|
||||||
size_t end_idx = idx + 1;
|
usize end_idx = idx + 1;
|
||||||
while (
|
while (
|
||||||
end_idx < codepoints.size() && selections[end_idx] == font_choice) {
|
end_idx < codepoints.size() && selections[end_idx] == font_choice) {
|
||||||
segment_end = codepoints[end_idx].end;
|
segment_end = codepoints[end_idx].end;
|
||||||
@@ -829,3 +841,5 @@ auto find_font_path(std::string_view path)
|
|||||||
}
|
}
|
||||||
return final_path;
|
return final_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
@@ -21,12 +21,22 @@ namespace msdfgen {
|
|||||||
class FontHandle;
|
class FontHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FontHandle {
|
namespace Waylight {
|
||||||
auto operator()() const -> auto const & { return id; }
|
|
||||||
|
struct FontHandle { // cppcheck-supress noConstructor
|
||||||
|
FontHandle() = default;
|
||||||
|
|
||||||
|
auto operator()() const -> auto const &
|
||||||
|
{
|
||||||
|
if (id == 0xffffffff) {
|
||||||
|
throw std::runtime_error("Uninitialized FontHandle");
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend struct TextRenderer;
|
friend struct TextRenderer;
|
||||||
usize id;
|
usize id { 0xffffffff };
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FontRuntime;
|
struct FontRuntime;
|
||||||
@@ -137,3 +147,5 @@ private:
|
|||||||
|
|
||||||
auto find_font_path(std::string_view path = "sans-serif:style=Regular")
|
auto find_font_path(std::string_view path = "sans-serif:style=Regular")
|
||||||
-> std::optional<std::filesystem::path>;
|
-> std::optional<std::filesystem::path>;
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
@@ -4,8 +4,11 @@
|
|||||||
|
|
||||||
#include "enum_array.hpp"
|
#include "enum_array.hpp"
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
struct ColorScheme {
|
struct ColorScheme {
|
||||||
Color foreground;
|
Color foreground;
|
||||||
|
Color foreground_preedit;
|
||||||
struct {
|
struct {
|
||||||
Color background;
|
Color background;
|
||||||
} window;
|
} window;
|
||||||
@@ -23,6 +26,7 @@ constexpr auto make_default_themes() -> enum_array<Theme, ColorScheme> const
|
|||||||
enum_array<Theme, ColorScheme> array;
|
enum_array<Theme, ColorScheme> array;
|
||||||
array[Theme::Light] = {
|
array[Theme::Light] = {
|
||||||
.foreground = { 0, 0, 0, 255 },
|
.foreground = { 0, 0, 0, 255 },
|
||||||
|
.foreground_preedit = { 0, 0, 0, 255 },
|
||||||
.window =
|
.window =
|
||||||
{
|
{
|
||||||
.background = { 255, 255, 255, 100 },
|
.background = { 255, 255, 255, 100 },
|
||||||
@@ -30,6 +34,7 @@ constexpr auto make_default_themes() -> enum_array<Theme, ColorScheme> const
|
|||||||
};
|
};
|
||||||
array[Theme::Dark] = {
|
array[Theme::Dark] = {
|
||||||
.foreground = { 255, 255, 255, 255 },
|
.foreground = { 255, 255, 255, 255 },
|
||||||
|
.foreground_preedit = { 255, 255, 255, 255 },
|
||||||
.window =
|
.window =
|
||||||
{
|
{
|
||||||
.background = { 0, 0, 0, 100 },
|
.background = { 0, 0, 0, 100 },
|
||||||
@@ -37,3 +42,5 @@ constexpr auto make_default_themes() -> enum_array<Theme, ColorScheme> const
|
|||||||
};
|
};
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
55
src/Tick.cpp
55
src/Tick.cpp
@@ -8,9 +8,17 @@
|
|||||||
#include <rlgl.h>
|
#include <rlgl.h>
|
||||||
#include <xkbcommon/xkbcommon.h>
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
auto App::tick() -> void
|
auto App::tick() -> void
|
||||||
{
|
{
|
||||||
static std::pmr::string text_input_data;
|
static std::pmr::string text_input_data {};
|
||||||
|
|
||||||
|
m_ime.bound_text = &text_input_data;
|
||||||
|
m_ime.bound_id = 1;
|
||||||
|
process_pending_text_input();
|
||||||
|
|
||||||
if (!m_visible || m_gl.edpy == EGL_NO_DISPLAY
|
if (!m_visible || m_gl.edpy == EGL_NO_DISPLAY
|
||||||
|| m_gl.esurf == EGL_NO_SURFACE)
|
|| m_gl.esurf == EGL_NO_SURFACE)
|
||||||
@@ -31,25 +39,50 @@ auto App::tick() -> void
|
|||||||
|
|
||||||
{
|
{
|
||||||
assert(m_gui);
|
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 };
|
u32 rune { 0 };
|
||||||
if (!m_kbd.typing.empty()) {
|
if (!m_kbd.typing.empty()) {
|
||||||
rune = m_kbd.typing.back();
|
rune = m_kbd.typing.back();
|
||||||
m_kbd.typing.clear();
|
m_kbd.typing.clear();
|
||||||
}
|
}
|
||||||
ImGuiGuard gui_scope(m_gui, rune, m_kbd.ctrl(), m_kbd.shift());
|
ImGuiGuard gui_scope(m_gui, rune, m_kbd.ctrl(), m_kbd.shift(),
|
||||||
|
clipboard(),
|
||||||
|
[this](std::string_view const &str) { clipboard(str); });
|
||||||
|
|
||||||
m_gui->text_input(1, text_input_data,
|
Rectangle const input_rect {
|
||||||
{
|
0.0f,
|
||||||
0,
|
0.0f,
|
||||||
0,
|
static_cast<float>(GetScreenWidth()),
|
||||||
static_cast<float>(GetScreenWidth()),
|
static_cast<float>(GetScreenHeight()),
|
||||||
static_cast<float>(GetScreenHeight()),
|
};
|
||||||
});
|
;
|
||||||
|
if (auto const result
|
||||||
|
= m_gui->text_input(1, text_input_data, input_rect);
|
||||||
|
result.test(1)) {
|
||||||
|
m_ime.surrounding_dirty = true;
|
||||||
|
} else if (result.test(0)) {
|
||||||
|
if (text_input_data == "kitty") {
|
||||||
|
execute_command(false, "kitty");
|
||||||
|
} else if (text_input_data == "nvim") {
|
||||||
|
execute_command(true, "nvim");
|
||||||
|
}
|
||||||
|
|
||||||
|
text_input_data = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
update_text_input_state(text_input_data, 1, input_rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DrawTexture(m_ir.lookup("folder", 48).texture(), 48, 48, WHITE);
|
||||||
|
|
||||||
EndDrawing();
|
EndDrawing();
|
||||||
|
|
||||||
eglSwapBuffers(m_gl.edpy, m_gl.esurf);
|
eglSwapBuffers(m_gl.edpy, m_gl.esurf);
|
||||||
m_kbd.typing.clear();
|
|
||||||
m_kbd.clear_transients();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
using u8 = std::uint8_t;
|
using u8 = std::uint8_t;
|
||||||
using i8 = std::int8_t;
|
using i8 = std::int8_t;
|
||||||
using u16 = std::uint16_t;
|
using u16 = std::uint16_t;
|
||||||
@@ -15,9 +19,8 @@ using isize = std::intptr_t;
|
|||||||
|
|
||||||
[[maybe_unused]] static inline auto rune_to_string(uint32_t cp) -> char const *
|
[[maybe_unused]] static inline auto rune_to_string(uint32_t cp) -> char const *
|
||||||
{
|
{
|
||||||
static char utf8[5] = { 0 };
|
static std::array<char, 5> utf8 {};
|
||||||
for (auto &c : utf8)
|
std::fill(utf8.begin(), utf8.end(), 0);
|
||||||
c = 0;
|
|
||||||
|
|
||||||
if (cp < 0x80) {
|
if (cp < 0x80) {
|
||||||
utf8[0] = cp;
|
utf8[0] = cp;
|
||||||
@@ -35,5 +38,7 @@ using isize = std::intptr_t;
|
|||||||
utf8[3] = 0x80 | (cp & 0x3F);
|
utf8[3] = 0x80 | (cp & 0x3F);
|
||||||
}
|
}
|
||||||
|
|
||||||
return utf8;
|
return utf8.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
@@ -5,28 +5,31 @@
|
|||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include "common.hpp"
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
template<class E> struct enum_traits;
|
template<class E> struct enum_traits;
|
||||||
|
|
||||||
template<class E>
|
template<class E>
|
||||||
concept EnumLike = std::is_enum_v<E>;
|
concept EnumLike = std::is_enum_v<E>;
|
||||||
|
|
||||||
template<EnumLike E>
|
template<EnumLike E>
|
||||||
constexpr std::size_t enum_count_v
|
constexpr usize enum_count_v = static_cast<usize>(enum_traits<E>::last)
|
||||||
= static_cast<std::size_t>(enum_traits<E>::last)
|
- static_cast<usize>(enum_traits<E>::first) + 1;
|
||||||
- static_cast<std::size_t>(enum_traits<E>::first) + 1;
|
|
||||||
|
|
||||||
template<EnumLike E, class T> struct enum_array {
|
template<EnumLike E, class T> struct enum_array {
|
||||||
using value_type = T;
|
using value_type = T;
|
||||||
using enum_type = E;
|
using enum_type = E;
|
||||||
using underlying_index_type = std::size_t;
|
using underlying_index_type = usize;
|
||||||
|
|
||||||
static constexpr E first = enum_traits<E>::first;
|
static constexpr E first = enum_traits<E>::first;
|
||||||
static constexpr E last = enum_traits<E>::last;
|
static constexpr E last = enum_traits<E>::last;
|
||||||
static constexpr std::size_t size_value = enum_count_v<E>;
|
static constexpr usize size_value = enum_count_v<E>;
|
||||||
|
|
||||||
std::array<T, size_value> _data {};
|
std::array<T, size_value> _data {};
|
||||||
|
|
||||||
static constexpr std::size_t size() noexcept { return size_value; }
|
static constexpr usize size() noexcept { return size_value; }
|
||||||
constexpr T *data() noexcept { return _data.data(); }
|
constexpr T *data() noexcept { return _data.data(); }
|
||||||
constexpr T const *data() const noexcept { return _data.data(); }
|
constexpr T const *data() const noexcept { return _data.data(); }
|
||||||
constexpr T *begin() noexcept { return _data.begin().operator->(); }
|
constexpr T *begin() noexcept { return _data.begin().operator->(); }
|
||||||
@@ -61,9 +64,9 @@ template<EnumLike E, class T> struct enum_array {
|
|||||||
constexpr void fill(T const &v) { _data.fill(v); }
|
constexpr void fill(T const &v) { _data.fill(v); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr std::size_t to_index(E e) noexcept
|
static constexpr usize to_index(E e) noexcept
|
||||||
{
|
{
|
||||||
return static_cast<std::size_t>(e) - static_cast<std::size_t>(first);
|
return static_cast<usize>(e) - static_cast<usize>(first);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,3 +80,5 @@ constexpr auto make_enum_array(T &&first_val, U &&...rest)
|
|||||||
arr._data = { std::forward<T>(first_val), std::forward<U>(rest)... };
|
arr._data = { std::forward<T>(first_val), std::forward<U>(rest)... };
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace Waylight
|
||||||
|
|||||||
99
src/main.cpp
99
src/main.cpp
@@ -1,57 +1,88 @@
|
|||||||
|
#include <algorithm>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
#include <iostream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <print>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <cpptrace/cpptrace.hpp>
|
||||||
|
#include <cpptrace/from_current.hpp>
|
||||||
|
#include <tinyfiledialogs.h>
|
||||||
|
|
||||||
#include "App.hpp"
|
#include "App.hpp"
|
||||||
|
|
||||||
|
namespace Waylight {
|
||||||
|
|
||||||
bool signal_running();
|
bool signal_running();
|
||||||
|
|
||||||
std::optional<App> g_app{};
|
std::optional<App> g_app {};
|
||||||
|
|
||||||
auto main() -> int {
|
bool signal_running()
|
||||||
if (signal_running()) {
|
{
|
||||||
return 0;
|
char const *lock_path = "/tmp/waylight.lock";
|
||||||
}
|
int fd = open(lock_path, O_CREAT | O_RDWR, 0666);
|
||||||
|
if (fd == -1)
|
||||||
|
return false;
|
||||||
|
|
||||||
std::signal(SIGINT, [](int) {
|
if (flock(fd, LOCK_EX | LOCK_NB) == -1) {
|
||||||
if (g_app)
|
FILE *f = fopen(lock_path, "r");
|
||||||
g_app->stop();
|
if (f) {
|
||||||
});
|
pid_t pid;
|
||||||
|
if (fscanf(f, "%d", &pid) == 1)
|
||||||
|
kill(pid, SIGUSR1);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
g_app.emplace();
|
if (ftruncate(fd, 0) == -1) {
|
||||||
g_app->run();
|
close(fd);
|
||||||
|
unlink(lock_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dprintf(fd, "%d\n", getpid());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool signal_running() {
|
} // namespace Waylight
|
||||||
const char *lock_path = "/tmp/waylight.lock";
|
|
||||||
int fd = open(lock_path, O_CREAT | O_RDWR, 0666);
|
|
||||||
if (fd == -1)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (flock(fd, LOCK_EX | LOCK_NB) == -1) {
|
auto main() -> int
|
||||||
FILE *f = fopen(lock_path, "r");
|
{
|
||||||
if (f) {
|
if (Waylight::signal_running()) {
|
||||||
pid_t pid;
|
return 0;
|
||||||
if (fscanf(f, "%d", &pid) == 1)
|
}
|
||||||
kill(pid, SIGUSR1);
|
|
||||||
fclose(f);
|
|
||||||
}
|
|
||||||
close(fd);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ftruncate(fd, 0) == -1) {
|
std::signal(SIGINT, [](int) {
|
||||||
close(fd);
|
if (Waylight::g_app)
|
||||||
unlink(lock_path);
|
Waylight::g_app->stop();
|
||||||
return false;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
dprintf(fd, "%d\n", getpid());
|
CPPTRACE_TRY
|
||||||
return false;
|
{
|
||||||
|
Waylight::g_app.emplace();
|
||||||
|
Waylight::g_app->run();
|
||||||
|
}
|
||||||
|
CPPTRACE_CATCH(std::exception const &e)
|
||||||
|
{
|
||||||
|
std::string what { e.what() };
|
||||||
|
std::ranges::replace(what, '"', '.');
|
||||||
|
std::ranges::replace(what, '\'', '.');
|
||||||
|
std::ranges::replace(what, '`', '.');
|
||||||
|
if (what.empty()) {
|
||||||
|
std::println(std::cerr, "Unexpected exception!");
|
||||||
|
} else {
|
||||||
|
std::println(std::cerr, "Unexpected exception! Error: {}", what);
|
||||||
|
}
|
||||||
|
cpptrace::from_current_exception().print();
|
||||||
|
tinyfd_messageBox(
|
||||||
|
"Unexpected exception", what.c_str(), "ok", "error", 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
todo.txt
Normal file
5
todo.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
- [-] Fix text moving left and right based on cursor position (right side of cursor is too left)
|
||||||
|
- [ ] Fix selection using wrong offset for text
|
||||||
|
- [x] Implement selection in ImGui::text_edit
|
||||||
|
- [x] Implement clipboard copy/paste
|
||||||
|
|
||||||
1
vendor/CMakeLists.txt
vendored
Normal file
1
vendor/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
add_subdirectory(tinyfiledialogs)
|
||||||
8
vendor/tinyfiledialogs/CMakeLists.txt
vendored
Normal file
8
vendor/tinyfiledialogs/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.30)
|
||||||
|
|
||||||
|
project(tinyfiledialogs C)
|
||||||
|
|
||||||
|
add_library(${PROJECT_NAME} STATIC tinyfiledialogs.c)
|
||||||
|
|
||||||
|
target_include_directories(${PROJECT_NAME} PUBLIC include)
|
||||||
|
|
||||||
314
vendor/tinyfiledialogs/include/tinyfiledialogs.h
vendored
Normal file
314
vendor/tinyfiledialogs/include/tinyfiledialogs.h
vendored
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
/* SPDX-License-Identifier: Zlib
|
||||||
|
Copyright (c) 2014 - 2025 Guillaume Vareille http://ysengrin.com
|
||||||
|
____________________________________________________________________
|
||||||
|
| |
|
||||||
|
| 100% compatible C C++ -> You can rename tinfiledialogs.c as .cpp |
|
||||||
|
|____________________________________________________________________|
|
||||||
|
|
||||||
|
********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
|
||||||
|
_________
|
||||||
|
/ \ tinyfiledialogs.h v3.21.1 [Oct 5, 2025]
|
||||||
|
|tiny file| Unique header file created [November 9, 2014]
|
||||||
|
| dialogs |
|
||||||
|
\____ ___/ http://tinyfiledialogs.sourceforge.net
|
||||||
|
\| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
|
||||||
|
____________________________________________
|
||||||
|
| |
|
||||||
|
| email: tinyfiledialogs at ysengrin.com |
|
||||||
|
|____________________________________________|
|
||||||
|
________________________________________________________________________________
|
||||||
|
| ____________________________________________________________________________ |
|
||||||
|
| | | |
|
||||||
|
| | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | |
|
||||||
|
| | | |
|
||||||
|
| | on windows: | |
|
||||||
|
| | - for UTF-16, use the wchar_t functions at the bottom of the header file | |
|
||||||
|
| | | |
|
||||||
|
| | - _wfopen() requires wchar_t | |
|
||||||
|
| | - fopen() uses char but expects ASCII or MBCS (not UTF-8) | |
|
||||||
|
| | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | |
|
||||||
|
| | | |
|
||||||
|
| | - alternatively, tinyfiledialogs provides | |
|
||||||
|
| | functions to convert between UTF-8, UTF-16 and MBCS | |
|
||||||
|
| |____________________________________________________________________________| |
|
||||||
|
|________________________________________________________________________________|
|
||||||
|
|
||||||
|
If you like tinyfiledialogs, please upvote my stackoverflow answer
|
||||||
|
https://stackoverflow.com/a/47651444
|
||||||
|
|
||||||
|
- License -
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
|
||||||
|
__________________________________________
|
||||||
|
| ______________________________________ |
|
||||||
|
| | | |
|
||||||
|
| | DO NOT USE USER INPUT IN THE DIALOGS | |
|
||||||
|
| |______________________________________| |
|
||||||
|
|__________________________________________|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef TINYFILEDIALOGS_H
|
||||||
|
#define TINYFILEDIALOGS_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/******************************************************************************************************/
|
||||||
|
/**************************************** UTF-8 on Windows ********************************************/
|
||||||
|
/******************************************************************************************************/
|
||||||
|
#ifdef _WIN32
|
||||||
|
/* On windows, if you want to use UTF-8 ( instead of the UTF-16/wchar_t functions at the end of this file )
|
||||||
|
Make sure your code is really prepared for UTF-8 (on windows, functions like fopen() expect MBCS and not UTF-8) */
|
||||||
|
extern int tinyfd_winUtf8; /* on windows char strings can be 1:UTF-8(default) or 0:MBCS */
|
||||||
|
/* for MBCS change this to 0, in tinyfiledialogs.c or in your code */
|
||||||
|
|
||||||
|
/* Here are some functions to help you convert between UTF-16 UTF-8 MBSC */
|
||||||
|
char * tinyfd_utf8toMbcs(char const * aUtf8string);
|
||||||
|
char * tinyfd_utf16toMbcs(wchar_t const * aUtf16string);
|
||||||
|
wchar_t * tinyfd_mbcsTo16(char const * aMbcsString);
|
||||||
|
char * tinyfd_mbcsTo8(char const * aMbcsString);
|
||||||
|
wchar_t * tinyfd_utf8to16(char const * aUtf8string);
|
||||||
|
char * tinyfd_utf16to8(wchar_t const * aUtf16string);
|
||||||
|
#endif
|
||||||
|
/******************************************************************************************************/
|
||||||
|
/******************************************************************************************************/
|
||||||
|
/******************************************************************************************************/
|
||||||
|
|
||||||
|
/************* 3 funtions for C# (you don't need this in C or C++) : */
|
||||||
|
char const * tinyfd_getGlobalChar(char const * aCharVariableName); /* returns NULL on error */
|
||||||
|
int tinyfd_getGlobalInt(char const * aIntVariableName); /* returns -1 on error */
|
||||||
|
int tinyfd_setGlobalInt(char const * aIntVariableName, int aValue); /* returns -1 on error */
|
||||||
|
/* aCharVariableName: "tinyfd_version" "tinyfd_needs" "tinyfd_response"
|
||||||
|
aIntVariableName : "tinyfd_verbose" "tinyfd_silent" "tinyfd_allowCursesDialogs"
|
||||||
|
"tinyfd_forceConsole" "tinyfd_assumeGraphicDisplay" "tinyfd_winUtf8"
|
||||||
|
**************/
|
||||||
|
|
||||||
|
extern char tinyfd_version[8]; /* contains tinyfd current version number */
|
||||||
|
extern char tinyfd_needs[]; /* info about requirements */
|
||||||
|
extern int tinyfd_verbose; /* 0 (default) or 1 : on unix, prints the command line calls */
|
||||||
|
extern int tinyfd_silent; /* 1 (default) or 0 : on unix, hide errors and warnings from called dialogs */
|
||||||
|
|
||||||
|
/** Curses dialogs are difficult to use and counter-intuitive.
|
||||||
|
On windows they are only ascii and still uses the unix backslash ! **/
|
||||||
|
extern int tinyfd_allowCursesDialogs; /* 0 (default) or 1 */
|
||||||
|
|
||||||
|
extern int tinyfd_forceConsole; /* 0 (default) or 1 */
|
||||||
|
/* for unix & windows: 0 (graphic mode) or 1 (console mode).
|
||||||
|
0: try to use a graphic solution, if it fails then it uses console mode.
|
||||||
|
1: forces all dialogs into console mode even when an X server is present.
|
||||||
|
if enabled, it can use the package Dialog or dialog.exe.
|
||||||
|
on windows it only make sense for console applications */
|
||||||
|
|
||||||
|
/* extern int tinyfd_assumeGraphicDisplay; */ /* 0 (default) or 1 */
|
||||||
|
/* some systems don't set the environment variable DISPLAY even when a graphic display is present.
|
||||||
|
set this to 1 to tell tinyfiledialogs to assume the existence of a graphic display */
|
||||||
|
|
||||||
|
extern char tinyfd_response[1024];
|
||||||
|
/* if you pass "tinyfd_query" as aTitle,
|
||||||
|
the functions will not display the dialogs
|
||||||
|
but will return 0 for console mode, 1 for graphic mode.
|
||||||
|
tinyfd_response is then filled with the retain solution.
|
||||||
|
possible values for tinyfd_response are (all lowercase)
|
||||||
|
for graphic mode:
|
||||||
|
windows_wchar windows applescript kdialog zenity zenity3 yad matedialog
|
||||||
|
shellementary qarma shanty boxer python2-tkinter python3-tkinter python-dbus
|
||||||
|
perl-dbus gxmessage gmessage xmessage xdialog gdialog dunst
|
||||||
|
for console mode:
|
||||||
|
dialog whiptail basicinput no_solution */
|
||||||
|
|
||||||
|
void tinyfd_beep(void);
|
||||||
|
|
||||||
|
int tinyfd_notifyPopup(
|
||||||
|
char const * aTitle, /* NULL or "" */
|
||||||
|
char const * aMessage, /* NULL or "" may contain \n \t */
|
||||||
|
char const * aIconType); /* "info" "warning" "error" */
|
||||||
|
/* return has only meaning for tinyfd_query */
|
||||||
|
|
||||||
|
int tinyfd_messageBox(
|
||||||
|
char const * aTitle , /* NULL or "" */
|
||||||
|
char const * aMessage , /* NULL or "" may contain \n \t */
|
||||||
|
char const * aDialogType , /* "ok" "okcancel" "yesno" "yesnocancel" */
|
||||||
|
char const * aIconType , /* "info" "warning" "error" "question" */
|
||||||
|
int aDefaultButton ) ;
|
||||||
|
/* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
|
||||||
|
|
||||||
|
char * tinyfd_inputBox(
|
||||||
|
char const * aTitle , /* NULL or "" */
|
||||||
|
char const * aMessage , /* NULL or "" (\n and \t have no effect) */
|
||||||
|
char const * aDefaultInput ) ; /* NULL = passwordBox, "" = inputbox */
|
||||||
|
/* returns NULL on cancel */
|
||||||
|
|
||||||
|
char * tinyfd_saveFileDialog(
|
||||||
|
char const * aTitle , /* NULL or "" */
|
||||||
|
char const * aDefaultPathAndOrFile , /* NULL or "" , ends with / to set only a directory */
|
||||||
|
int aNumOfFilterPatterns , /* 0 (1 in the following example) */
|
||||||
|
char const * const * aFilterPatterns , /* NULL or char const * lFilterPatterns[1]={"*.txt"} */
|
||||||
|
char const * aSingleFilterDescription ) ; /* NULL or "text files" */
|
||||||
|
/* returns NULL on cancel */
|
||||||
|
|
||||||
|
char * tinyfd_openFileDialog(
|
||||||
|
char const * aTitle, /* NULL or "" */
|
||||||
|
char const * aDefaultPathAndOrFile, /* NULL or "" , ends with / to set only a directory */
|
||||||
|
int aNumOfFilterPatterns , /* 0 (2 in the following example) */
|
||||||
|
char const * const * aFilterPatterns, /* NULL or char const * lFilterPatterns[2]={"*.png","*.jpg"}; */
|
||||||
|
char const * aSingleFilterDescription, /* NULL or "image files" */
|
||||||
|
int aAllowMultipleSelects ) ; /* 0 or 1 */
|
||||||
|
/* in case of multiple files, the separator is | */
|
||||||
|
/* returns NULL on cancel */
|
||||||
|
|
||||||
|
char * tinyfd_selectFolderDialog(
|
||||||
|
char const * aTitle, /* NULL or "" */
|
||||||
|
char const * aDefaultPath); /* NULL or "" */
|
||||||
|
/* returns NULL on cancel */
|
||||||
|
|
||||||
|
char * tinyfd_colorChooser(
|
||||||
|
char const * aTitle, /* NULL or "" */
|
||||||
|
char const * aDefaultHexRGB, /* NULL or "" or "#FF0000" */
|
||||||
|
unsigned char const aDefaultRGB[3] , /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */
|
||||||
|
unsigned char aoResultRGB[3] ) ; /* unsigned char lResultRGB[3]; */
|
||||||
|
/* aDefaultRGB is used only if aDefaultHexRGB is absent */
|
||||||
|
/* aDefaultRGB and aoResultRGB can be the same array */
|
||||||
|
/* returns NULL on cancel */
|
||||||
|
/* returns the hexcolor as a string "#FF0000" */
|
||||||
|
/* aoResultRGB also contains the result */
|
||||||
|
|
||||||
|
|
||||||
|
/************ WINDOWS ONLY SECTION ************************/
|
||||||
|
#ifdef _WIN32
|
||||||
|
|
||||||
|
/* windows only - utf-16 version */
|
||||||
|
int tinyfd_notifyPopupW(
|
||||||
|
wchar_t const * aTitle, /* NULL or L"" */
|
||||||
|
wchar_t const * aMessage, /* NULL or L"" may contain \n \t */
|
||||||
|
wchar_t const * aIconType); /* L"info" L"warning" L"error" */
|
||||||
|
|
||||||
|
/* windows only - utf-16 version */
|
||||||
|
int tinyfd_messageBoxW(
|
||||||
|
wchar_t const * aTitle, /* NULL or L"" */
|
||||||
|
wchar_t const * aMessage, /* NULL or L"" may contain \n \t */
|
||||||
|
wchar_t const * aDialogType, /* L"ok" L"okcancel" L"yesno" */
|
||||||
|
wchar_t const * aIconType, /* L"info" L"warning" L"error" L"question" */
|
||||||
|
int aDefaultButton ); /* 0 for cancel/no , 1 for ok/yes */
|
||||||
|
/* returns 0 for cancel/no , 1 for ok/yes */
|
||||||
|
|
||||||
|
/* windows only - utf-16 version */
|
||||||
|
wchar_t * tinyfd_inputBoxW(
|
||||||
|
wchar_t const * aTitle, /* NULL or L"" */
|
||||||
|
wchar_t const * aMessage, /* NULL or L"" (\n nor \t not respected) */
|
||||||
|
wchar_t const * aDefaultInput); /* NULL passwordBox, L"" inputbox */
|
||||||
|
|
||||||
|
/* windows only - utf-16 version */
|
||||||
|
wchar_t * tinyfd_saveFileDialogW(
|
||||||
|
wchar_t const * aTitle, /* NULL or L"" */
|
||||||
|
wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */
|
||||||
|
int aNumOfFilterPatterns, /* 0 (1 in the following example) */
|
||||||
|
wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[1]={L"*.txt"} */
|
||||||
|
wchar_t const * aSingleFilterDescription); /* NULL or L"text files" */
|
||||||
|
/* returns NULL on cancel */
|
||||||
|
|
||||||
|
/* windows only - utf-16 version */
|
||||||
|
wchar_t * tinyfd_openFileDialogW(
|
||||||
|
wchar_t const * aTitle, /* NULL or L"" */
|
||||||
|
wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */
|
||||||
|
int aNumOfFilterPatterns , /* 0 (2 in the following example) */
|
||||||
|
wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[2]={L"*.png","*.jpg"} */
|
||||||
|
wchar_t const * aSingleFilterDescription, /* NULL or L"image files" */
|
||||||
|
int aAllowMultipleSelects ) ; /* 0 or 1 */
|
||||||
|
/* in case of multiple files, the separator is | */
|
||||||
|
/* returns NULL on cancel */
|
||||||
|
|
||||||
|
/* windows only - utf-16 version */
|
||||||
|
wchar_t * tinyfd_selectFolderDialogW(
|
||||||
|
wchar_t const * aTitle, /* NULL or L"" */
|
||||||
|
wchar_t const * aDefaultPath); /* NULL or L"" */
|
||||||
|
/* returns NULL on cancel */
|
||||||
|
|
||||||
|
/* windows only - utf-16 version */
|
||||||
|
wchar_t * tinyfd_colorChooserW(
|
||||||
|
wchar_t const * aTitle, /* NULL or L"" */
|
||||||
|
wchar_t const * aDefaultHexRGB, /* NULL or L"#FF0000" */
|
||||||
|
unsigned char const aDefaultRGB[3], /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */
|
||||||
|
unsigned char aoResultRGB[3]); /* unsigned char lResultRGB[3]; */
|
||||||
|
/* returns the hexcolor as a string L"#FF0000" */
|
||||||
|
/* aoResultRGB also contains the result */
|
||||||
|
/* aDefaultRGB is used only if aDefaultHexRGB is NULL */
|
||||||
|
/* aDefaultRGB and aoResultRGB can be the same array */
|
||||||
|
/* returns NULL on cancel */
|
||||||
|
|
||||||
|
#endif /*_WIN32 */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} /*extern "C"*/
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* TINYFILEDIALOGS_H */
|
||||||
|
|
||||||
|
/*
|
||||||
|
________________________________________________________________________________
|
||||||
|
| ____________________________________________________________________________ |
|
||||||
|
| | | |
|
||||||
|
| | on windows: | |
|
||||||
|
| | - for UTF-16, use the wchar_t functions at the bottom of the header file | |
|
||||||
|
| | - _wfopen() requires wchar_t | |
|
||||||
|
| | | |
|
||||||
|
| | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | |
|
||||||
|
| | - but fopen() expects MBCS (not UTF-8) | |
|
||||||
|
| | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | |
|
||||||
|
| | | |
|
||||||
|
| | - alternatively, tinyfiledialogs provides | |
|
||||||
|
| | functions to convert between UTF-8, UTF-16 and MBCS | |
|
||||||
|
| |____________________________________________________________________________| |
|
||||||
|
|________________________________________________________________________________|
|
||||||
|
|
||||||
|
- This is not for ios nor android (it works in termux though).
|
||||||
|
- The files can be renamed with extension ".cpp" as the code is 100% compatible C C++
|
||||||
|
(just comment out << extern "C" >> in the header file)
|
||||||
|
- Windows is fully supported from XP to 10 (maybe even older versions)
|
||||||
|
- C# & LUA via dll, see files in the folder EXTRAS
|
||||||
|
- OSX supported from 10.4 to latest (maybe even older versions)
|
||||||
|
- Do not use " and ' as the dialogs will be displayed with a warning
|
||||||
|
instead of the title, message, etc...
|
||||||
|
- There's one file filter only, it may contain several patterns.
|
||||||
|
- If no filter description is provided,
|
||||||
|
the list of patterns will become the description.
|
||||||
|
- On windows link against Comdlg32.lib and Ole32.lib
|
||||||
|
(on windows the no linking claim is a lie)
|
||||||
|
- On unix: it tries command line calls, so no such need (NO LINKING).
|
||||||
|
- On unix you need one of the following:
|
||||||
|
applescript, kdialog, zenity, matedialog, shellementary, qarma, shanty, boxer,
|
||||||
|
yad, python (2 or 3)/tkinter/python-dbus (optional), Xdialog
|
||||||
|
or curses dialogs (opens terminal if running without console).
|
||||||
|
- One of those is already included on most (if not all) desktops.
|
||||||
|
- In the absence of those it will use gdialog, gxmessage or whiptail
|
||||||
|
with a textinputbox. If nothing is found, it switches to basic console input,
|
||||||
|
it opens a console if needed (requires xterm + bash).
|
||||||
|
- for curses dialogs you must set tinyfd_allowCursesDialogs=1
|
||||||
|
- You can query the type of dialog that will be used (pass "tinyfd_query" as aTitle)
|
||||||
|
- String memory is preallocated statically for all the returned values.
|
||||||
|
- File and path names are tested before return, they should be valid.
|
||||||
|
- tinyfd_forceConsole=1; at run time, forces dialogs into console mode.
|
||||||
|
- On windows, console mode only make sense for console applications.
|
||||||
|
- On windows, console mode is not implemented for wchar_T UTF-16.
|
||||||
|
- Mutiple selects are not possible in console mode.
|
||||||
|
- The package dialog must be installed to run in curses dialogs in console mode.
|
||||||
|
It is already installed on most unix systems.
|
||||||
|
- On osx, the package dialog can be installed via
|
||||||
|
http://macappstore.org/dialog or http://macports.org
|
||||||
|
- On windows, for curses dialogs console mode,
|
||||||
|
dialog.exe should be copied somewhere on your executable path.
|
||||||
|
It can be found at the bottom of the following page:
|
||||||
|
http://andrear.altervista.org/home/cdialog.php
|
||||||
|
*/
|
||||||
8397
vendor/tinyfiledialogs/tinyfiledialogs.c
vendored
Normal file
8397
vendor/tinyfiledialogs/tinyfiledialogs.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user