mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-06-12 04:57:06 -04:00
Merge branch 'rmlui-integration'
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
Language: Cpp
|
||||
Standard: C++03
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignOperands: true
|
||||
|
||||
@@ -100,6 +100,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
endif ()
|
||||
set(AURORA_ENABLE_DVD ON CACHE BOOL "Enable DVD API support" FORCE)
|
||||
set(AURORA_ENABLE_CARD ON CACHE BOOL "Enable CARD API support" FORCE)
|
||||
set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE)
|
||||
add_subdirectory(extern/aurora EXCLUDE_FROM_ALL)
|
||||
|
||||
add_subdirectory(libs/freeverb)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
- ### **[Official Website](https://twilitrealm.dev)**
|
||||
- ### **[Discord](https://discord.gg/QACynxeyna)**
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 1bd972429e...1119435dd1
+39
@@ -1462,6 +1462,45 @@ set(DUSK_FILES
|
||||
src/dusk/imgui/ImGuiStateShare.cpp
|
||||
src/dusk/imgui/ImGuiAchievements.hpp
|
||||
src/dusk/imgui/ImGuiAchievements.cpp
|
||||
src/dusk/ui/bool_button.cpp
|
||||
src/dusk/ui/bool_button.hpp
|
||||
src/dusk/ui/button.cpp
|
||||
src/dusk/ui/button.hpp
|
||||
src/dusk/ui/component.cpp
|
||||
src/dusk/ui/component.hpp
|
||||
src/dusk/ui/document.cpp
|
||||
src/dusk/ui/document.hpp
|
||||
src/dusk/ui/editor.cpp
|
||||
src/dusk/ui/editor.hpp
|
||||
src/dusk/ui/event.cpp
|
||||
src/dusk/ui/event.hpp
|
||||
src/dusk/ui/input.cpp
|
||||
src/dusk/ui/input.hpp
|
||||
src/dusk/ui/nav_types.hpp
|
||||
src/dusk/ui/number_button.cpp
|
||||
src/dusk/ui/number_button.hpp
|
||||
src/dusk/ui/overlay.cpp
|
||||
src/dusk/ui/overlay.hpp
|
||||
src/dusk/ui/pane.cpp
|
||||
src/dusk/ui/pane.hpp
|
||||
src/dusk/ui/popup.cpp
|
||||
src/dusk/ui/popup.hpp
|
||||
src/dusk/ui/prelaunch.cpp
|
||||
src/dusk/ui/prelaunch.hpp
|
||||
src/dusk/ui/prelaunch_options.cpp
|
||||
src/dusk/ui/prelaunch_options.hpp
|
||||
src/dusk/ui/select_button.cpp
|
||||
src/dusk/ui/select_button.hpp
|
||||
src/dusk/ui/settings.cpp
|
||||
src/dusk/ui/settings.hpp
|
||||
src/dusk/ui/string_button.cpp
|
||||
src/dusk/ui/string_button.hpp
|
||||
src/dusk/ui/tab_bar.cpp
|
||||
src/dusk/ui/tab_bar.hpp
|
||||
src/dusk/ui/ui.cpp
|
||||
src/dusk/ui/ui.hpp
|
||||
src/dusk/ui/window.cpp
|
||||
src/dusk/ui/window.hpp
|
||||
src/dusk/achievements.cpp
|
||||
src/dusk/iso_validate.cpp
|
||||
src/dusk/livesplit.cpp
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 457 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,147 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-size: 24dp;
|
||||
color: #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.overlay-root {
|
||||
width: 100%;
|
||||
min-height: 45%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
decorator: vertical-gradient(#00000000 #151610F2);
|
||||
padding: 48dp 0 40dp 0;
|
||||
filter: opacity(0);
|
||||
transition: filter 0.2s linear-in-out;
|
||||
}
|
||||
|
||||
.overlay-root[open] {
|
||||
filter: opacity(1);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
width: 100%;
|
||||
max-width: 1216dp;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24dp;
|
||||
padding: 0 32dp;
|
||||
}
|
||||
|
||||
@media (max-height: 800dp) {
|
||||
.overlay-root {
|
||||
min-height: 38%;
|
||||
padding: 32dp 0 28dp 0;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
gap: 16dp;
|
||||
padding: 0 24dp;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24dp;
|
||||
}
|
||||
|
||||
.carousel-container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 18dp;
|
||||
line-height: 22dp;
|
||||
color: rgba(255, 255, 255, 50%);
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 1dp 0;
|
||||
border-top: 1dp rgba(217, 217, 217, 50%);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24dp;
|
||||
}
|
||||
|
||||
footer-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 220dp;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 20dp;
|
||||
line-height: 24dp;
|
||||
text-transform: uppercase;
|
||||
color: #FFFFFF;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
footer-button.return {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
footer-button.reset {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.stepped-carousel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16dp;
|
||||
width: auto;
|
||||
min-width: 246dp;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stepped-carousel-value {
|
||||
line-height: 29dp;
|
||||
min-width: 166dp;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.stepped-carousel-arrow {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
min-width: 24dp;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 18dp;
|
||||
color: #E0DBC8;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
focus: auto;
|
||||
}
|
||||
|
||||
popup {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 64dp;
|
||||
background-color: rgba(21, 22, 16, 80%);
|
||||
border-bottom: 2dp #92875B;
|
||||
backdrop-filter: blur(5dp);
|
||||
transform: translateY(-64dp);
|
||||
transition: transform 0.2s cubic-in-out;
|
||||
}
|
||||
|
||||
popup[open] {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
popup tab-bar {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
popup tab-bar tab {
|
||||
opacity: 0.35;
|
||||
color: #E0DBC8;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "Fira Sans";
|
||||
font-weight: normal;
|
||||
font-size: 20dp;
|
||||
color: #FFFFFF;
|
||||
background-color: #000000;
|
||||
decorator: image(../prelaunch-bg.png cover left center);
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
left: 96dp;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
/* Scale based on a reference screen width, 428/1216 */
|
||||
width: 35.230264vw;
|
||||
min-width: 428dp;
|
||||
max-width: 856dp;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48dp;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 8dp;
|
||||
}
|
||||
|
||||
.hero img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-family: "Alegreya SC";
|
||||
font-size: 32dp;
|
||||
}
|
||||
|
||||
@media (min-width: 1216dp) {
|
||||
.eyebrow {
|
||||
/* Same logic as .menu, 32/1216 */
|
||||
font-size: 2.631579vw;
|
||||
}
|
||||
}
|
||||
|
||||
.eyebrow span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12dp;
|
||||
}
|
||||
|
||||
#menu-list button {
|
||||
width: 428dp;
|
||||
height: 54dp;
|
||||
padding: 8dp 16dp;
|
||||
border-radius: 8dp;
|
||||
text-transform: uppercase;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-size: 32dp;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
/* Define a fully transparent gradient as the default state, otherwise a white flash occurs */
|
||||
decorator: horizontal-gradient(#00000000 #00000000);
|
||||
}
|
||||
|
||||
#menu-list button[anim-done] {
|
||||
transition: decorator color 0.1s linear-in-out;
|
||||
}
|
||||
|
||||
#menu-list button:hover,
|
||||
#menu-list button:focus-visible {
|
||||
color: black;
|
||||
decorator: horizontal-gradient(#FEE685FF #FEE68500);
|
||||
}
|
||||
|
||||
.disk-status {
|
||||
position: absolute;
|
||||
left: 96dp;
|
||||
bottom: 72dp;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8dp;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
position: absolute;
|
||||
right: 96dp;
|
||||
bottom: 72dp;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8dp;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.status,
|
||||
.version {
|
||||
font-size: 24dp;
|
||||
}
|
||||
|
||||
.status,
|
||||
.update {
|
||||
color: #D8F999;
|
||||
}
|
||||
|
||||
.status[bad] {
|
||||
color: #FFC9C9;
|
||||
}
|
||||
|
||||
/* TODO: Hidden until an actual update checker is introduced */
|
||||
.update {
|
||||
display: none;
|
||||
font-size: 16dp;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.detail,
|
||||
.update span {
|
||||
color: #A6A09B;
|
||||
}
|
||||
|
||||
/* Startup animation */
|
||||
.intro-item {
|
||||
opacity: 0;
|
||||
transform: translateY(10dp);
|
||||
transition: opacity transform 0.3s 0.1s cubic-in-out;
|
||||
}
|
||||
|
||||
body.animate-in .intro-item {
|
||||
opacity: 1;
|
||||
transform: translateY(0dp);
|
||||
}
|
||||
|
||||
.delay-0 {
|
||||
transition: opacity transform 0.3s 0.1s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-1 {
|
||||
transition: opacity transform 0.3s 0.2s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-2 {
|
||||
transition: opacity transform 0.3s 0.3s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-3 {
|
||||
transition: opacity transform 0.3s 0.4s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-4 {
|
||||
transition: opacity transform 0.3s 0.5s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-5 {
|
||||
transition: opacity transform 0.3s 0.6s cubic-in-out;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
tab-bar {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
overflow: auto hidden;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
tab-bar tab {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 24dp;
|
||||
line-height: 64dp;
|
||||
white-space: nowrap;
|
||||
decorator: vertical-gradient(#c2a42d00 #c2a42d00);
|
||||
transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tab-bar tab:selected {
|
||||
opacity: 1;
|
||||
border-bottom: 4dp #C2A42D;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
}
|
||||
|
||||
tab-bar tab:focus-visible,
|
||||
tab-bar tab:hover {
|
||||
opacity: 1;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
decorator: vertical-gradient(#c2a42d00 #c2a42d26);
|
||||
}
|
||||
|
||||
tab-bar tab:active {
|
||||
decorator: vertical-gradient(#c2a42d10 #c2a42d40);
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 64dp;
|
||||
font-family: "Fira Sans";
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 15dp;
|
||||
color: #E0DBC8;
|
||||
}
|
||||
|
||||
window {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
max-width: 1088dp;
|
||||
max-height: 768dp;
|
||||
margin: auto;
|
||||
border-radius: 14dp;
|
||||
overflow: hidden;
|
||||
border: 2dp #92875B;
|
||||
backdrop-filter: blur(5dp);
|
||||
box-shadow: 0 0 25dp 5dp;
|
||||
background-color: rgba(21, 22, 16, 90%);
|
||||
filter: opacity(0);
|
||||
transform: scale(0.9);
|
||||
transform-origin: center;
|
||||
transition: filter transform 0.2s cubic-in-out;
|
||||
}
|
||||
|
||||
window[open] {
|
||||
filter: opacity(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@media (max-height: 640dp) {
|
||||
body {
|
||||
padding: 16dp;
|
||||
}
|
||||
window {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
window tab-bar {
|
||||
flex: 0 0 64dp;
|
||||
height: 64dp;
|
||||
background-color: rgba(217, 217, 217, 10%);
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 18dp;
|
||||
border-bottom: 2dp #92875B;
|
||||
}
|
||||
|
||||
window tab-bar tab {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
window content {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
window content pane {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1 1 0;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 24dp;
|
||||
padding-bottom: 0dp;
|
||||
gap: 8dp;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
window content pane:not(:last-of-type) {
|
||||
border-right: 1dp #92875B;
|
||||
}
|
||||
|
||||
window content pane > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
window content pane > spacer {
|
||||
display: block;
|
||||
/* Completes the 24dp bottom inset after the pane's 8dp gap. */
|
||||
flex: 0 0 16dp;
|
||||
height: 16dp;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
scrollbarvertical {
|
||||
width: 8dp;
|
||||
margin: 4dp 4dp 4dp 0;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderarrowdec,
|
||||
scrollbarvertical sliderarrowinc {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbarvertical slidertrack {
|
||||
width: 8dp;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderbar {
|
||||
width: 8dp;
|
||||
min-height: 24dp;
|
||||
background-color: rgba(224, 219, 200, 45%);
|
||||
border-radius: 2dp;
|
||||
transition: background-color 0.2s cubic-in-out;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderbar:hover,
|
||||
scrollbarvertical sliderbar:active {
|
||||
background-color: rgba(194, 164, 45, 80%);
|
||||
}
|
||||
|
||||
scrollbarhorizontal {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbarhorizontal sliderarrowdec,
|
||||
scrollbarhorizontal sliderarrowinc {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbarhorizontal slidertrack,
|
||||
scrollbarhorizontal sliderbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 22dp;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.section-heading:not(:first-of-type) {
|
||||
padding-top: 12dp;
|
||||
}
|
||||
|
||||
button {
|
||||
text-align: center;
|
||||
background-color: rgba(17, 16, 10, 20%);
|
||||
opacity: 0.9;
|
||||
padding: 8dp 16dp;
|
||||
border-radius: 14dp;
|
||||
box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp;
|
||||
font-size: 20dp;
|
||||
transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out;
|
||||
cursor: pointer;
|
||||
focus: auto;
|
||||
}
|
||||
|
||||
button:not(:disabled):hover,
|
||||
button:not(:disabled):focus-visible {
|
||||
background-color: rgba(204, 184, 119, 20%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
button:not(:disabled):selected {
|
||||
opacity: 1;
|
||||
background-color: rgba(204, 184, 119, 40%);
|
||||
}
|
||||
|
||||
button:not(:disabled):active {
|
||||
opacity: 1;
|
||||
background-color: rgba(204, 184, 119, 40%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
select-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8dp;
|
||||
background-color: rgba(17, 16, 10, 20%);
|
||||
opacity: 0.9;
|
||||
padding: 8dp 16dp;
|
||||
border-radius: 14dp;
|
||||
box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp;
|
||||
transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out;
|
||||
cursor: pointer;
|
||||
focus: auto;
|
||||
}
|
||||
|
||||
select-button:not(:disabled):hover,
|
||||
select-button:not(:disabled):focus-visible {
|
||||
background-color: rgba(204, 184, 119, 20%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
select-button:not(:disabled):selected {
|
||||
opacity: 1;
|
||||
background-color: rgba(204, 184, 119, 40%);
|
||||
}
|
||||
|
||||
select-button:not(:disabled):active {
|
||||
opacity: 1;
|
||||
background-color: rgba(204, 184, 119, 40%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
select-button:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
select-button key {
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 18dp;
|
||||
text-transform: uppercase;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
select-button value {
|
||||
margin-left: auto;
|
||||
font-size: 20dp;
|
||||
}
|
||||
|
||||
select-button input {
|
||||
text-align: right;
|
||||
font-size: 20dp;
|
||||
}
|
||||
@@ -324,18 +324,18 @@ namespace dusk {
|
||||
ImGuiMenuGame::ToggleFullscreen();
|
||||
}
|
||||
|
||||
if (!dusk::IsGameLaunched) {
|
||||
m_preLaunchWindow.draw();
|
||||
}
|
||||
// if (!dusk::IsGameLaunched) {
|
||||
// m_preLaunchWindow.draw();
|
||||
// }
|
||||
|
||||
m_isHidden = !getSettings().backend.duskMenuOpen;
|
||||
bool showMenu = !dusk::IsGameLaunched || !CheckMenuViewToggle(ImGuiKey_F1, m_isHidden);
|
||||
if (dusk::IsGameLaunched) {
|
||||
const bool menuOpen = !m_isHidden;
|
||||
if (getSettings().backend.duskMenuOpen != menuOpen) {
|
||||
getSettings().backend.duskMenuOpen.setValue(menuOpen);
|
||||
Save();
|
||||
}
|
||||
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
|
||||
m_isHidden = !m_isHidden;
|
||||
}
|
||||
bool showMenu = !m_isHidden;
|
||||
if (getSettings().backend.duskMenuOpen != showMenu) {
|
||||
getSettings().backend.duskMenuOpen.setValue(showMenu);
|
||||
Save();
|
||||
}
|
||||
|
||||
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
|
||||
|
||||
@@ -101,6 +101,10 @@ ValidationError validate(const char* path) {
|
||||
NodHandleWrapper disc;
|
||||
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
const NodDiscStream nod_stream {
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "bool_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
BoolButton::BoolButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton(parent, {std::move(props.key)}),
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
|
||||
mIsDisabled(std::move(props.isDisabled)) {}
|
||||
|
||||
bool BoolButton::disabled() const {
|
||||
if (mIsDisabled) {
|
||||
return mIsDisabled();
|
||||
}
|
||||
return BaseControlledSelectButton::disabled();
|
||||
}
|
||||
|
||||
Rml::String BoolButton::format_value() {
|
||||
return mGetValue() ? "On" : "Off";
|
||||
}
|
||||
|
||||
bool BoolButton::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
mSetValue(!mGetValue());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class BoolButton : public BaseControlledSelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<bool()> getValue;
|
||||
std::function<void(bool)> setValue;
|
||||
std::function<bool()> isDisabled;
|
||||
};
|
||||
|
||||
BoolButton(Rml::Element* parent, Props props);
|
||||
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override;
|
||||
bool handle_nav_command(NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
std::function<int()> mGetValue;
|
||||
std::function<void(int)> mSetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "button.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent, const Rml::String& tagName) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement(tagName);
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Button::Button(Rml::Element* parent, Props props, const Rml::String& tagName)
|
||||
: FluentComponent(createRoot(parent, tagName)) {
|
||||
update_props(std::move(props));
|
||||
}
|
||||
|
||||
void Button::set_text(const Rml::String& text) {
|
||||
if (mProps.text != text) {
|
||||
mRoot->SetInnerRML(escape(text));
|
||||
mProps.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
Button& Button::on_pressed(ButtonCallback callback) {
|
||||
if (!callback) {
|
||||
return *this;
|
||||
}
|
||||
// TODO: convert this to a FluentComponent method?
|
||||
on_nav_command([callback = std::move(callback)](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
callback();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Button::update_props(Props props) {
|
||||
set_text(props.text);
|
||||
mProps = std::move(props);
|
||||
}
|
||||
|
||||
void ControlledButton::update() {
|
||||
if (mIsSelected) {
|
||||
set_selected(mIsSelected());
|
||||
}
|
||||
Button::update();
|
||||
}
|
||||
|
||||
bool ControlledButton::selected() const {
|
||||
if (mIsSelected) {
|
||||
return mIsSelected();
|
||||
}
|
||||
return Button::selected();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "component.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
using ButtonCallback = std::function<void()>;
|
||||
|
||||
class Button : public FluentComponent<Button> {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String text;
|
||||
};
|
||||
|
||||
Button(Rml::Element* parent, Props props, const Rml::String& tagName = "button");
|
||||
Button(Rml::Element* parent, Rml::String text, const Rml::String& tagName = "button")
|
||||
: Button(parent, Props{std::move(text)}, tagName) {}
|
||||
|
||||
void set_text(const Rml::String& text);
|
||||
Button& on_pressed(ButtonCallback callback);
|
||||
|
||||
const Rml::String& get_text() const { return mProps.text; }
|
||||
|
||||
private:
|
||||
void update_props(Props props);
|
||||
|
||||
Props mProps;
|
||||
};
|
||||
|
||||
class ControlledButton : public Button {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String text;
|
||||
std::function<bool()> isSelected;
|
||||
};
|
||||
|
||||
ControlledButton(Rml::Element* parent, Props props, const Rml::String& tagName = "button")
|
||||
: Button(parent, {std::move(props.text)}, tagName),
|
||||
mIsSelected(std::move(props.isSelected)) {}
|
||||
|
||||
void update() override;
|
||||
bool selected() const override;
|
||||
|
||||
private:
|
||||
std::function<bool()> mIsSelected;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "component.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
Component::Component(Rml::Element* root) : mRoot(root) {}
|
||||
|
||||
Component::~Component() = default;
|
||||
|
||||
void Component::update() {
|
||||
for (const auto& child : mChildren) {
|
||||
child->update();
|
||||
}
|
||||
}
|
||||
|
||||
bool Component::focus() {
|
||||
if (disabled()) {
|
||||
return false;
|
||||
}
|
||||
// Can we focus self?
|
||||
if (mRoot->Focus(true)) {
|
||||
mRoot->ScrollIntoView(Rml::ScrollIntoViewOptions{
|
||||
Rml::ScrollAlignment::Center,
|
||||
Rml::ScrollAlignment::Center,
|
||||
Rml::ScrollBehavior::Smooth,
|
||||
Rml::ScrollParentage::Closest,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// Otherwise, try to focus a child
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Component::set_selected(bool value) {
|
||||
// Subclasses may override selected() to return a dynamic value, but
|
||||
// we're only interested in if the pseudoclass is set or not, so we
|
||||
// use Component::selected() directly rather than selected().
|
||||
if (Component::selected() == value) {
|
||||
return;
|
||||
}
|
||||
mRoot->SetPseudoClass("selected", value);
|
||||
}
|
||||
|
||||
void Component::set_disabled(bool value) {
|
||||
if (Component::disabled() == value) {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
mRoot->SetAttribute("disabled", "");
|
||||
mRoot->SetPseudoClass("disabled", true);
|
||||
mRoot->Blur();
|
||||
} else {
|
||||
mRoot->RemoveAttribute("disabled");
|
||||
mRoot->SetPseudoClass("disabled", false);
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) {
|
||||
if (parent == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
if (doc == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return parent->AppendChild(doc->CreateElement(tag));
|
||||
}
|
||||
void Component::listen(Rml::Element* element, Rml::EventId event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
element = mRoot;
|
||||
}
|
||||
mListeners.emplace_back(
|
||||
std::make_unique<ScopedEventListener>(element, event, std::move(callback), capture));
|
||||
}
|
||||
|
||||
bool Component::contains(Rml::Element* element) const {
|
||||
for (const auto* node = element; node != nullptr; node = node->GetParentNode()) {
|
||||
if (node == mRoot) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Component::clear_children() {
|
||||
mChildren.clear();
|
||||
while (mRoot->GetNumChildren() > 0) {
|
||||
mRoot->RemoveChild(mRoot->GetFirstChild());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "event.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Rml {
|
||||
class Element;
|
||||
}
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Component {
|
||||
public:
|
||||
Component() = default;
|
||||
explicit Component(Rml::Element* root);
|
||||
virtual ~Component();
|
||||
|
||||
Component(const Component&) = delete;
|
||||
Component& operator=(const Component&) = delete;
|
||||
|
||||
virtual void update();
|
||||
virtual bool focus();
|
||||
|
||||
virtual bool selected() const { return mRoot->IsPseudoClassSet("selected"); }
|
||||
virtual void set_selected(bool selected);
|
||||
virtual bool disabled() const { return mRoot->IsPseudoClassSet("disabled"); }
|
||||
virtual void set_disabled(bool disabled);
|
||||
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
bool contains(Rml::Element* element) const;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
auto child = std::make_unique<T>(mRoot, std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mChildren.emplace_back(std::move(child));
|
||||
return ref;
|
||||
}
|
||||
|
||||
Rml::Element* root() const { return mRoot; }
|
||||
|
||||
protected:
|
||||
static Rml::Element* append(Rml::Element* parent, const Rml::String& tag);
|
||||
void clear_children();
|
||||
|
||||
Rml::Element* mRoot = nullptr;
|
||||
std::vector<std::unique_ptr<Component> > mChildren;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
};
|
||||
|
||||
template <class Derived>
|
||||
class FluentComponent : public Component {
|
||||
public:
|
||||
using Component::Component;
|
||||
|
||||
Derived& listen(
|
||||
Rml::EventId event, ScopedEventListener::Callback callback, bool capture = false) {
|
||||
Component::listen(mRoot, event, std::move(callback), capture);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
Derived& on_focus(ScopedEventListener::Callback callback) {
|
||||
return listen(
|
||||
Rml::EventId::Focus, [this, callback = std::move(callback)](Rml::Event& event) {
|
||||
if (!disabled()) {
|
||||
callback(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Derived& on_nav_command(std::function<bool(Rml::Event&, NavCommand)> callback) {
|
||||
listen(Rml::EventId::Click, [this, callback](Rml::Event& event) {
|
||||
if (!disabled() && callback(event, NavCommand::Confirm)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
listen(Rml::EventId::Keydown, [this, callback = std::move(callback)](Rml::Event& event) {
|
||||
if (disabled()) {
|
||||
return;
|
||||
}
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::None && callback(event, cmd)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,105 @@
|
||||
#include "document.hpp"
|
||||
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::ElementDocument* load_document(const Rml::String& source) {
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return context->LoadDocumentFromMemory(source);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Document::Document(const Rml::String& source) : mDocument(load_document(source)) {
|
||||
// Block keydown events while hidden (except for Menu)
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
[this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::Menu && !visible()) {
|
||||
event.StopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
true);
|
||||
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Document::~Document() {
|
||||
mListeners.clear();
|
||||
if (mDocument != nullptr) {
|
||||
mDocument->Close();
|
||||
mDocument = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Document::show() {
|
||||
if (mDocument != nullptr) {
|
||||
// Attempt to preserve the previously focused element
|
||||
mDocument->Show(Rml::ModalFlag::None, Rml::FocusFlag::Keep, Rml::ScrollFlag::None);
|
||||
// If nothing is focused, let the document decide the initial focus
|
||||
auto* leaf = mDocument->GetFocusLeafNode();
|
||||
if (leaf == nullptr || leaf == mDocument) {
|
||||
focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Document::hide() {
|
||||
if (mDocument != nullptr) {
|
||||
mDocument->Hide();
|
||||
}
|
||||
}
|
||||
|
||||
void Document::update() {}
|
||||
|
||||
bool Document::focus() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void Document::listen(Rml::Element* element, Rml::EventId event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
element = mDocument;
|
||||
}
|
||||
if (element == nullptr || !callback) {
|
||||
return;
|
||||
}
|
||||
mListeners.emplace_back(
|
||||
std::make_unique<ScopedEventListener>(element, event, std::move(callback), capture));
|
||||
}
|
||||
|
||||
bool Document::can_destroy() const {
|
||||
if (mDocument == nullptr) {
|
||||
return true;
|
||||
}
|
||||
return *mDocument->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Hidden;
|
||||
}
|
||||
|
||||
bool Document::visible() const {
|
||||
if (mDocument == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return *mDocument->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible;
|
||||
}
|
||||
|
||||
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Menu) {
|
||||
toggle_top_document();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "component.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Document {
|
||||
public:
|
||||
Document(const Rml::String& source);
|
||||
virtual ~Document();
|
||||
|
||||
Document(const Document&) = delete;
|
||||
Document& operator=(const Document&) = delete;
|
||||
|
||||
virtual void show();
|
||||
virtual void hide();
|
||||
virtual void update();
|
||||
virtual bool focus();
|
||||
virtual bool visible() const;
|
||||
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
void listen(Rml::EventId event, ScopedEventListener::Callback callback, bool capture = false) {
|
||||
listen(mDocument, event, std::move(callback), capture);
|
||||
}
|
||||
|
||||
bool can_destroy() const;
|
||||
|
||||
protected:
|
||||
virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd);
|
||||
|
||||
Rml::ElementDocument* mDocument;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class EditorWindow : public Window {
|
||||
public:
|
||||
EditorWindow();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "event.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
ScopedEventListener::ScopedEventListener(
|
||||
Rml::Element* element, Rml::EventId event, Callback callback, bool capture)
|
||||
: mElement(element), mEvent(event), mCapture(capture), mCallback(std::move(callback)) {
|
||||
mElement->AddEventListener(mEvent, this, mCapture);
|
||||
}
|
||||
|
||||
ScopedEventListener::~ScopedEventListener() {
|
||||
if (mElement != nullptr) {
|
||||
mElement->RemoveEventListener(mEvent, this, mCapture);
|
||||
mElement = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ScopedEventListener::ProcessEvent(Rml::Event& event) {
|
||||
if (mCallback) {
|
||||
mCallback(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ScopedEventListener::OnDetach(Rml::Element* element) {
|
||||
if (element == mElement) {
|
||||
mElement = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class ScopedEventListener final : public Rml::EventListener {
|
||||
public:
|
||||
using Callback = std::function<void(Rml::Event&)>;
|
||||
|
||||
ScopedEventListener(
|
||||
Rml::Element* element, Rml::EventId event, Callback callback, bool capture = false);
|
||||
~ScopedEventListener() override;
|
||||
|
||||
ScopedEventListener(const ScopedEventListener&) = delete;
|
||||
ScopedEventListener& operator=(const ScopedEventListener&) = delete;
|
||||
ScopedEventListener(ScopedEventListener&&) = delete;
|
||||
ScopedEventListener& operator=(ScopedEventListener&&) = delete;
|
||||
|
||||
void ProcessEvent(Rml::Event& event) override;
|
||||
void OnDetach(Rml::Element* element) override;
|
||||
|
||||
private:
|
||||
Rml::Element* mElement = nullptr;
|
||||
Rml::EventId mEvent = Rml::EventId::Invalid;
|
||||
bool mCapture = false;
|
||||
Callback mCallback;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,610 @@
|
||||
#include "input.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <aurora/rmlui.hpp>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
constexpr double kGamepadRepeatInitialDelay = 0.32;
|
||||
constexpr double kGamepadRepeatStartInterval = 0.12;
|
||||
constexpr double kGamepadRepeatMinInterval = 0.045;
|
||||
constexpr double kGamepadRepeatRampDuration = 1.0;
|
||||
constexpr double kGamepadMenuChordGraceDuration = 0.12;
|
||||
constexpr Sint16 kGamepadAxisPressThreshold = 16384;
|
||||
constexpr Sint16 kGamepadAxisReleaseThreshold = 12000;
|
||||
constexpr int kGamepadAxisDirectionCount = SDL_GAMEPAD_AXIS_COUNT * 2;
|
||||
|
||||
struct GamepadRepeatState {
|
||||
Rml::Input::KeyIdentifier key = Rml::Input::KI_UNKNOWN;
|
||||
double pressedAt = 0.0;
|
||||
double nextRepeatAt = 0.0;
|
||||
bool held = false;
|
||||
bool repeatable = false;
|
||||
bool pending = false;
|
||||
};
|
||||
|
||||
bool sPadInputBlocked = false;
|
||||
std::array<GamepadRepeatState, SDL_GAMEPAD_BUTTON_COUNT> sGamepadButtonRepeats;
|
||||
std::array<GamepadRepeatState, kGamepadAxisDirectionCount> sGamepadAxisRepeats;
|
||||
std::array<u32, PAD_MAX_CONTROLLERS> sPadHoldMasks;
|
||||
std::array<bool, PAD_MAX_CONTROLLERS> sMenuChordConsumed;
|
||||
|
||||
double now_seconds() noexcept {
|
||||
return static_cast<double>(SDL_GetTicksNS()) / 1000000000.0;
|
||||
}
|
||||
|
||||
bool is_menu_chord_part(PADButton button) noexcept {
|
||||
return button == PAD_TRIGGER_R || button == PAD_BUTTON_START;
|
||||
}
|
||||
|
||||
bool has_menu_chord_part_held(u32 port) noexcept {
|
||||
if (port >= sPadHoldMasks.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const u32 held = sPadHoldMasks[port];
|
||||
return (held & (PAD_TRIGGER_R | PAD_BUTTON_START)) != 0;
|
||||
}
|
||||
|
||||
bool should_block_pad_for_menu_chord() noexcept {
|
||||
for (u32 port = 0; port < sPadHoldMasks.size(); ++port) {
|
||||
if (sMenuChordConsumed[port] && has_menu_chord_part_held(port)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
PADButton pad_button_from_axis(PADAxis axis) noexcept {
|
||||
switch (axis) {
|
||||
case PAD_AXIS_TRIGGER_R:
|
||||
return PAD_TRIGGER_R;
|
||||
case PAD_AXIS_TRIGGER_L:
|
||||
return PAD_TRIGGER_L;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void set_pad_button_held(u32 port, PADButton button, bool held) noexcept {
|
||||
if (port >= sPadHoldMasks.size() || button == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (held) {
|
||||
sPadHoldMasks[port] |= button;
|
||||
} else {
|
||||
sPadHoldMasks[port] &= ~button;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_menu_chord(u32 port) noexcept {
|
||||
if (port >= sPadHoldMasks.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const u32 held = sPadHoldMasks[port];
|
||||
return (held & PAD_TRIGGER_R) != 0 && (held & PAD_BUTTON_START) != 0;
|
||||
}
|
||||
|
||||
bool any_menu_chord() noexcept {
|
||||
return std::any_of(sPadHoldMasks.begin(), sPadHoldMasks.end(),
|
||||
[](u32 held) { return (held & PAD_TRIGGER_R) != 0 && (held & PAD_BUTTON_START) != 0; });
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_pad_button(PADButton button) noexcept {
|
||||
switch (button) {
|
||||
case PAD_BUTTON_UP:
|
||||
return Rml::Input::KI_UP;
|
||||
case PAD_BUTTON_DOWN:
|
||||
return Rml::Input::KI_DOWN;
|
||||
case PAD_BUTTON_LEFT:
|
||||
return Rml::Input::KI_LEFT;
|
||||
case PAD_BUTTON_RIGHT:
|
||||
return Rml::Input::KI_RIGHT;
|
||||
case PAD_BUTTON_B:
|
||||
return Rml::Input::KI_ESCAPE;
|
||||
case PAD_BUTTON_A:
|
||||
return Rml::Input::KI_RETURN;
|
||||
case PAD_TRIGGER_R:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case PAD_TRIGGER_L:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_pad_axis(PADAxis axis) noexcept {
|
||||
switch (axis) {
|
||||
case PAD_AXIS_LEFT_X_POS:
|
||||
return Rml::Input::KI_RIGHT;
|
||||
case PAD_AXIS_LEFT_X_NEG:
|
||||
return Rml::Input::KI_LEFT;
|
||||
case PAD_AXIS_LEFT_Y_POS:
|
||||
return Rml::Input::KI_UP;
|
||||
case PAD_AXIS_LEFT_Y_NEG:
|
||||
return Rml::Input::KI_DOWN;
|
||||
case PAD_AXIS_TRIGGER_R:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case PAD_AXIS_TRIGGER_L:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_raw_gamepad_button(SDL_GamepadButton button) noexcept {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||
return Rml::Input::KI_UP;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||
return Rml::Input::KI_DOWN;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||
return Rml::Input::KI_LEFT;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||
return Rml::Input::KI_RIGHT;
|
||||
case SDL_GAMEPAD_BUTTON_EAST:
|
||||
return Rml::Input::KI_ESCAPE;
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||
return Rml::Input::KI_RETURN;
|
||||
case SDL_GAMEPAD_BUTTON_BACK:
|
||||
return Rml::Input::KI_F1;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_raw_button_alias(SDL_GamepadButton button) noexcept {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_BACK:
|
||||
return Rml::Input::KI_F1;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_raw_gamepad_axis(SDL_GamepadAxis axis, PADAxisSign sign) noexcept {
|
||||
switch (axis) {
|
||||
case SDL_GAMEPAD_AXIS_LEFTX:
|
||||
return sign == AXIS_SIGN_POSITIVE ? Rml::Input::KI_RIGHT : Rml::Input::KI_LEFT;
|
||||
case SDL_GAMEPAD_AXIS_LEFTY:
|
||||
return sign == AXIS_SIGN_NEGATIVE ? Rml::Input::KI_UP : Rml::Input::KI_DOWN;
|
||||
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
|
||||
return sign == AXIS_SIGN_POSITIVE ? Rml::Input::KI_NEXT : Rml::Input::KI_UNKNOWN;
|
||||
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
|
||||
return sign == AXIS_SIGN_POSITIVE ? Rml::Input::KI_PRIOR : Rml::Input::KI_UNKNOWN;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
bool find_event_port(SDL_JoystickID instance, u32& port) noexcept {
|
||||
for (u32 candidate = 0; candidate < PAD_MAX_CONTROLLERS; ++candidate) {
|
||||
const s32 index = PADGetIndexForPort(candidate);
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(static_cast<u32>(index));
|
||||
if (gamepad == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
if (joystick != nullptr && SDL_GetJoystickID(joystick) == instance) {
|
||||
port = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_mapped_pad_button(u32 port, SDL_GamepadButton nativeButton, PADButton& button) noexcept {
|
||||
u32 buttonCount = 0;
|
||||
PADButtonMapping* buttons = PADGetButtonMappings(port, &buttonCount);
|
||||
if (buttons != nullptr) {
|
||||
for (u32 i = 0; i < buttonCount; ++i) {
|
||||
if (buttons[i].nativeButton == static_cast<u32>(nativeButton)) {
|
||||
button = buttons[i].padButton;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u32 axisCount = 0;
|
||||
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
|
||||
if (axes != nullptr) {
|
||||
for (u32 i = 0; i < axisCount; ++i) {
|
||||
if (axes[i].nativeButton == nativeButton) {
|
||||
button = pad_button_from_axis(axes[i].padAxis);
|
||||
return button != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_mapped_pad_axis(
|
||||
u32 port, SDL_GamepadAxis nativeAxis, PADAxisSign sign, PADAxis& axis) noexcept {
|
||||
u32 buttonCount = 0;
|
||||
PADGetButtonMappings(port, &buttonCount);
|
||||
|
||||
u32 axisCount = 0;
|
||||
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
|
||||
if (axes == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < axisCount; ++i) {
|
||||
const PADSignedNativeAxis mappedAxis = axes[i].nativeAxis;
|
||||
if (mappedAxis.nativeAxis == nativeAxis && mappedAxis.sign == sign) {
|
||||
axis = axes[i].padAxis;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_event_pad_button(
|
||||
const SDL_GamepadButtonEvent& event, u32& port, PADButton& button) noexcept {
|
||||
return find_event_port(event.which, port) &&
|
||||
find_mapped_pad_button(port, static_cast<SDL_GamepadButton>(event.button), button);
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_gamepad_button(const SDL_GamepadButtonEvent& event) noexcept {
|
||||
const auto nativeButton = static_cast<SDL_GamepadButton>(event.button);
|
||||
if (nativeButton == SDL_GAMEPAD_BUTTON_BACK) {
|
||||
return Rml::Input::KI_F1;
|
||||
}
|
||||
|
||||
u32 port = 0;
|
||||
if (!find_event_port(event.which, port)) {
|
||||
return map_raw_gamepad_button(nativeButton);
|
||||
}
|
||||
|
||||
PADButton button = 0;
|
||||
if (find_mapped_pad_button(port, nativeButton, button)) {
|
||||
const auto key = map_pad_button(button);
|
||||
return key == Rml::Input::KI_UNKNOWN ? map_raw_button_alias(nativeButton) : key;
|
||||
}
|
||||
|
||||
return map_raw_button_alias(nativeButton);
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_gamepad_axis(
|
||||
const SDL_GamepadAxisEvent& event, PADAxisSign sign) noexcept {
|
||||
u32 port = 0;
|
||||
if (!find_event_port(event.which, port)) {
|
||||
return map_raw_gamepad_axis(static_cast<SDL_GamepadAxis>(event.axis), sign);
|
||||
}
|
||||
|
||||
PADAxis axis = 0;
|
||||
if (find_mapped_pad_axis(port, static_cast<SDL_GamepadAxis>(event.axis), sign, axis)) {
|
||||
return map_pad_axis(axis);
|
||||
}
|
||||
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
|
||||
bool is_repeatable_key(Rml::Input::KeyIdentifier key) noexcept {
|
||||
switch (key) {
|
||||
case Rml::Input::KI_UP:
|
||||
case Rml::Input::KI_DOWN:
|
||||
case Rml::Input::KI_LEFT:
|
||||
case Rml::Input::KI_RIGHT:
|
||||
case Rml::Input::KI_NEXT:
|
||||
case Rml::Input::KI_PRIOR:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
double repeat_interval(double heldFor) noexcept {
|
||||
const double ramp = std::clamp(heldFor / kGamepadRepeatRampDuration, 0.0, 1.0);
|
||||
return kGamepadRepeatStartInterval +
|
||||
(kGamepadRepeatMinInterval - kGamepadRepeatStartInterval) * ramp;
|
||||
}
|
||||
|
||||
GamepadRepeatState* button_repeat_state(SDL_GamepadButton button) noexcept {
|
||||
const auto index = static_cast<int>(button);
|
||||
if (index < 0 || index >= static_cast<int>(sGamepadButtonRepeats.size())) {
|
||||
return nullptr;
|
||||
}
|
||||
return &sGamepadButtonRepeats[index];
|
||||
}
|
||||
|
||||
GamepadRepeatState* axis_repeat_state(SDL_GamepadAxis axis, PADAxisSign sign) noexcept {
|
||||
const auto axisIndex = static_cast<int>(axis);
|
||||
if (axisIndex < 0 || axisIndex >= SDL_GAMEPAD_AXIS_COUNT) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int directionOffset = sign == AXIS_SIGN_POSITIVE ? 0 : 1;
|
||||
return &sGamepadAxisRepeats[axisIndex * 2 + directionOffset];
|
||||
}
|
||||
|
||||
void clear_gamepad_repeats() noexcept {
|
||||
for (auto& repeat : sGamepadButtonRepeats) {
|
||||
repeat = {};
|
||||
}
|
||||
for (auto& repeat : sGamepadAxisRepeats) {
|
||||
repeat = {};
|
||||
}
|
||||
sPadHoldMasks.fill(0);
|
||||
sMenuChordConsumed.fill(false);
|
||||
}
|
||||
|
||||
void begin_gamepad_key(GamepadRepeatState& repeat, Rml::Input::KeyIdentifier key) noexcept {
|
||||
if (repeat.held) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double now = now_seconds();
|
||||
repeat.key = key;
|
||||
repeat.pressedAt = now;
|
||||
repeat.held = true;
|
||||
repeat.repeatable = is_repeatable_key(key);
|
||||
repeat.nextRepeatAt = repeat.repeatable ? now + kGamepadRepeatInitialDelay : 0.0;
|
||||
repeat.pending = false;
|
||||
}
|
||||
|
||||
void begin_pending_gamepad_key(GamepadRepeatState& repeat, Rml::Input::KeyIdentifier key) noexcept {
|
||||
if (repeat.held) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double now = now_seconds();
|
||||
repeat.key = key;
|
||||
repeat.pressedAt = now;
|
||||
repeat.held = true;
|
||||
repeat.repeatable = is_repeatable_key(key);
|
||||
repeat.nextRepeatAt = 0.0;
|
||||
repeat.pending = true;
|
||||
}
|
||||
|
||||
void consume_menu_chord(u32 port, Rml::Context& context) noexcept {
|
||||
if (port < sMenuChordConsumed.size()) {
|
||||
sMenuChordConsumed[port] = true;
|
||||
}
|
||||
|
||||
auto cancel_next = [&context](GamepadRepeatState& repeat) {
|
||||
if (!repeat.held || repeat.key != Rml::Input::KI_NEXT) {
|
||||
return;
|
||||
}
|
||||
if (!repeat.pending) {
|
||||
context.ProcessKeyUp(repeat.key, 0);
|
||||
}
|
||||
repeat = {};
|
||||
};
|
||||
|
||||
for (auto& repeat : sGamepadButtonRepeats) {
|
||||
cancel_next(repeat);
|
||||
}
|
||||
for (auto& repeat : sGamepadAxisRepeats) {
|
||||
cancel_next(repeat);
|
||||
}
|
||||
}
|
||||
|
||||
void update_menu_chord_release(u32 port) noexcept {
|
||||
if (port >= sMenuChordConsumed.size() || has_menu_chord_part_held(port)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sMenuChordConsumed[port] = false;
|
||||
}
|
||||
|
||||
bool should_defer_menu_chord_part(PADButton button, Rml::Input::KeyIdentifier key) noexcept {
|
||||
return button == PAD_TRIGGER_R && key == Rml::Input::KI_NEXT;
|
||||
}
|
||||
|
||||
void process_axis_direction(
|
||||
Rml::Context& context, const SDL_GamepadAxisEvent& event, PADAxisSign sign) noexcept {
|
||||
GamepadRepeatState* repeat = axis_repeat_state(static_cast<SDL_GamepadAxis>(event.axis), sign);
|
||||
if (repeat == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool active = sign == AXIS_SIGN_POSITIVE ? event.value >= kGamepadAxisPressThreshold :
|
||||
event.value <= -kGamepadAxisPressThreshold;
|
||||
const bool released = sign == AXIS_SIGN_POSITIVE ? event.value <= kGamepadAxisReleaseThreshold :
|
||||
event.value >= -kGamepadAxisReleaseThreshold;
|
||||
|
||||
u32 port = 0;
|
||||
PADAxis padAxis = 0;
|
||||
const bool hasPadAxis =
|
||||
find_event_port(event.which, port) &&
|
||||
find_mapped_pad_axis(port, static_cast<SDL_GamepadAxis>(event.axis), sign, padAxis);
|
||||
const PADButton heldPadButton = hasPadAxis ? pad_button_from_axis(padAxis) : 0;
|
||||
|
||||
if (repeat->held) {
|
||||
if (released) {
|
||||
if (!repeat->pending) {
|
||||
context.ProcessKeyUp(repeat->key, 0);
|
||||
}
|
||||
set_pad_button_held(port, heldPadButton, false);
|
||||
*repeat = {};
|
||||
update_menu_chord_release(port);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
set_pad_button_held(port, heldPadButton, true);
|
||||
const bool chorded = heldPadButton == PAD_TRIGGER_R && is_menu_chord(port);
|
||||
if (chorded) {
|
||||
consume_menu_chord(port, context);
|
||||
}
|
||||
const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_axis(event, sign);
|
||||
if (key == Rml::Input::KI_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chorded && should_defer_menu_chord_part(heldPadButton, key)) {
|
||||
begin_pending_gamepad_key(*repeat, key);
|
||||
return;
|
||||
}
|
||||
|
||||
begin_gamepad_key(*repeat, key);
|
||||
context.ProcessMouseLeave();
|
||||
context.ProcessKeyDown(key, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void sync_input_block() noexcept {
|
||||
const bool shouldBlock = any_document_visible() || should_block_pad_for_menu_chord();
|
||||
if (sPadInputBlocked == shouldBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
PADBlockInput(shouldBlock);
|
||||
sPadInputBlocked = shouldBlock;
|
||||
}
|
||||
|
||||
void release_input_block() noexcept {
|
||||
if (!sPadInputBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
PADBlockInput(false);
|
||||
sPadInputBlocked = false;
|
||||
}
|
||||
|
||||
void reset_input_state() noexcept {
|
||||
clear_gamepad_repeats();
|
||||
}
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept {
|
||||
if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
|
||||
reset_input_state();
|
||||
sync_input_block();
|
||||
return;
|
||||
}
|
||||
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
|
||||
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
process_axis_direction(*context, event.gaxis, AXIS_SIGN_POSITIVE);
|
||||
process_axis_direction(*context, event.gaxis, AXIS_SIGN_NEGATIVE);
|
||||
sync_input_block();
|
||||
return;
|
||||
}
|
||||
|
||||
auto* repeat = button_repeat_state(static_cast<SDL_GamepadButton>(event.gbutton.button));
|
||||
u32 port = 0;
|
||||
PADButton button = 0;
|
||||
const bool hasPadButton = find_event_pad_button(event.gbutton, port, button);
|
||||
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
set_pad_button_held(port, button, true);
|
||||
const bool chorded = hasPadButton && is_menu_chord_part(button) && is_menu_chord(port);
|
||||
if (chorded) {
|
||||
consume_menu_chord(port, *context);
|
||||
}
|
||||
const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_button(event.gbutton);
|
||||
if (key != Rml::Input::KI_UNKNOWN) {
|
||||
bool deferred = false;
|
||||
if (repeat != nullptr) {
|
||||
if (!chorded && should_defer_menu_chord_part(button, key)) {
|
||||
begin_pending_gamepad_key(*repeat, key);
|
||||
deferred = true;
|
||||
} else {
|
||||
begin_gamepad_key(*repeat, key);
|
||||
}
|
||||
}
|
||||
if (!deferred) {
|
||||
context->ProcessMouseLeave();
|
||||
context->ProcessKeyDown(key, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto key = repeat != nullptr && repeat->held ? repeat->key : Rml::Input::KI_UNKNOWN;
|
||||
const bool wasPending = repeat != nullptr && repeat->pending;
|
||||
set_pad_button_held(port, button, false);
|
||||
update_menu_chord_release(port);
|
||||
if (key != Rml::Input::KI_UNKNOWN) {
|
||||
if (repeat != nullptr) {
|
||||
*repeat = {};
|
||||
}
|
||||
if (!wasPending) {
|
||||
context->ProcessKeyUp(key, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
sync_input_block();
|
||||
}
|
||||
|
||||
void update_input() noexcept {
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context != nullptr) {
|
||||
const double now = now_seconds();
|
||||
auto process_repeats = [context, now](auto& repeats) {
|
||||
for (auto& repeat : repeats) {
|
||||
if (!repeat.held) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (repeat.pending) {
|
||||
if (now < repeat.pressedAt + kGamepadMenuChordGraceDuration) {
|
||||
continue;
|
||||
}
|
||||
|
||||
repeat.pending = false;
|
||||
repeat.pressedAt = now;
|
||||
repeat.nextRepeatAt =
|
||||
repeat.repeatable ? now + kGamepadRepeatInitialDelay : 0.0;
|
||||
context->ProcessMouseLeave();
|
||||
context->ProcessKeyDown(repeat.key, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!repeat.repeatable || now < repeat.nextRepeatAt ||
|
||||
(repeat.key == Rml::Input::KI_NEXT && any_menu_chord()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
context->ProcessKeyDown(repeat.key, 0);
|
||||
const double heldFor = now - repeat.pressedAt;
|
||||
repeat.nextRepeatAt = now + repeat_interval(heldFor);
|
||||
}
|
||||
};
|
||||
process_repeats(sGamepadButtonRepeats);
|
||||
process_repeats(sGamepadAxisRepeats);
|
||||
} else {
|
||||
reset_input_state();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
void update_input() noexcept;
|
||||
void reset_input_state() noexcept;
|
||||
void sync_input_block() noexcept;
|
||||
void release_input_block() noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
enum class NavCommand {
|
||||
None,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Next, // R1
|
||||
Previous, // L1
|
||||
Confirm, // A
|
||||
Cancel, // B
|
||||
Menu, // Back/Minus, or R + Start
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "number_button.hpp"
|
||||
|
||||
#include <charconv>
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
NumberButton::NumberButton(Rml::Element* parent, Props props)
|
||||
: BaseStringButton(parent, {.key = std::move(props.key), .type = "number"}),
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
|
||||
mIsDisabled(std::move(props.isDisabled)), mMin(props.min), mMax(props.max), mStep(props.step),
|
||||
mPrefix(std::move(props.prefix)), mSuffix(std::move(props.suffix)) {}
|
||||
|
||||
bool NumberButton::disabled() const {
|
||||
if (mIsDisabled) {
|
||||
return mIsDisabled();
|
||||
}
|
||||
return BaseStringButton::disabled();
|
||||
}
|
||||
|
||||
Rml::String NumberButton::format_value() {
|
||||
return fmt::format("{}{}{}", mPrefix, mGetValue(), mSuffix);
|
||||
}
|
||||
|
||||
Rml::String NumberButton::input_value() {
|
||||
return fmt::to_string(mGetValue());
|
||||
}
|
||||
|
||||
void NumberButton::set_value(Rml::String value) {
|
||||
if (!mSetValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
int parsedValue = 0;
|
||||
const char* begin = value.data();
|
||||
const char* end = begin + value.size();
|
||||
const auto result = std::from_chars(begin, end, parsedValue);
|
||||
if (result.ec != std::errc() || result.ptr != end) {
|
||||
return;
|
||||
}
|
||||
|
||||
mSetValue(std::clamp(parsedValue, mMin, mMax));
|
||||
}
|
||||
|
||||
bool NumberButton::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Left) {
|
||||
mSetValue(std::clamp(mGetValue() - mStep, mMin, mMax));
|
||||
return true;
|
||||
} else if (cmd == NavCommand::Right) {
|
||||
mSetValue(std::clamp(mGetValue() + mStep, mMin, mMax));
|
||||
return true;
|
||||
}
|
||||
return BaseStringButton::handle_nav_command(cmd);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "string_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class NumberButton : public BaseStringButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<int()> getValue;
|
||||
std::function<void(int)> setValue;
|
||||
std::function<bool()> isDisabled;
|
||||
int min = 0;
|
||||
int max = INT_MAX;
|
||||
int step = 1;
|
||||
Rml::String prefix;
|
||||
Rml::String suffix;
|
||||
};
|
||||
|
||||
NumberButton(Rml::Element* parent, Props props);
|
||||
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override;
|
||||
Rml::String input_value() override;
|
||||
void set_value(Rml::String value) override;
|
||||
bool handle_nav_command(NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
std::function<int()> mGetValue;
|
||||
std::function<void(int)> mSetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
int mMin;
|
||||
int mMax;
|
||||
int mStep;
|
||||
Rml::String mPrefix;
|
||||
Rml::String mSuffix;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,273 @@
|
||||
#include "overlay.hpp"
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <dolphin/vi.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/overlay.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="overlay-root">
|
||||
<div class="overlay">
|
||||
<div class="header">
|
||||
<div id="title"></div>
|
||||
<div id="carousel-container" class="carousel-container"></div>
|
||||
</div>
|
||||
<div id="description" class="description"></div>
|
||||
<div class="divider"></div>
|
||||
<div id="footer" class="footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
int get_value(GraphicsOption option) {
|
||||
switch (option) {
|
||||
case GraphicsOption::InternalResolution:
|
||||
return getSettings().game.internalResolutionScale.getValue();
|
||||
case GraphicsOption::ShadowResolution:
|
||||
return getSettings().game.shadowResolutionMultiplier.getValue();
|
||||
case GraphicsOption::BloomMode:
|
||||
return static_cast<int>(getSettings().game.bloomMode.getValue());
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
return std::clamp(
|
||||
static_cast<int>(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0,
|
||||
100);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void set_value(GraphicsOption option, int value) {
|
||||
switch (option) {
|
||||
case GraphicsOption::InternalResolution:
|
||||
getSettings().game.internalResolutionScale.setValue(value);
|
||||
VISetFrameBufferScale(static_cast<float>(value));
|
||||
break;
|
||||
case GraphicsOption::ShadowResolution:
|
||||
getSettings().game.shadowResolutionMultiplier.setValue(value);
|
||||
break;
|
||||
case GraphicsOption::BloomMode:
|
||||
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
|
||||
value, static_cast<int>(BloomMode::Off), static_cast<int>(BloomMode::Dusk))));
|
||||
break;
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f);
|
||||
break;
|
||||
}
|
||||
config::Save();
|
||||
}
|
||||
|
||||
Rml::Element* create_stepped_carousel_root(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto root = doc->CreateElement("div");
|
||||
root->SetClass("stepped-carousel", true);
|
||||
root->SetAttribute("tabindex", "0");
|
||||
return parent->AppendChild(std::move(root));
|
||||
}
|
||||
|
||||
Rml::Element* create_stepped_carousel_arrow(
|
||||
Rml::Element* parent, const Rml::String& className, const Rml::String& label) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto button = doc->CreateElement("button");
|
||||
button->SetClass("stepped-carousel-arrow", true);
|
||||
button->SetClass(className, true);
|
||||
button->SetInnerRML(escape(label));
|
||||
return parent->AppendChild(std::move(button));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
|
||||
: Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
|
||||
Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", "<");
|
||||
mValueElem = append(mRoot, "div");
|
||||
mValueElem->SetClass("stepped-carousel-value", true);
|
||||
Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", ">");
|
||||
|
||||
listen(prevElem, Rml::EventId::Click,
|
||||
[this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
|
||||
listen(nextElem, Rml::EventId::Click,
|
||||
[this](Rml::Event&) { handle_nav_command(NavCommand::Right); });
|
||||
listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::None && handle_nav_command(cmd)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool SteppedCarousel::focus() {
|
||||
return Component::focus();
|
||||
}
|
||||
|
||||
void SteppedCarousel::update() {
|
||||
if (mValueElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
const int value = std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
|
||||
if (mProps.formatValue) {
|
||||
mValueElem->SetInnerRML(mProps.formatValue(value));
|
||||
} else {
|
||||
mValueElem->SetInnerRML(std::to_string(value));
|
||||
}
|
||||
}
|
||||
|
||||
bool SteppedCarousel::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Left) {
|
||||
const int value = mProps.getValue ? mProps.getValue() : 0;
|
||||
apply(std::clamp(value - mProps.step, mProps.min, mProps.max));
|
||||
return true;
|
||||
}
|
||||
if (cmd == NavCommand::Right) {
|
||||
const int value = mProps.getValue ? mProps.getValue() : 0;
|
||||
apply(std::clamp(value + mProps.step, mProps.min, mProps.max));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SteppedCarousel::apply(int value) {
|
||||
const int nextValue = std::clamp(value, mProps.min, mProps.max);
|
||||
const int currentValue =
|
||||
std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
|
||||
if (nextValue == currentValue) {
|
||||
return;
|
||||
}
|
||||
if (mProps.onChange) {
|
||||
mProps.onChange(nextValue);
|
||||
}
|
||||
}
|
||||
|
||||
Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
|
||||
switch (option) {
|
||||
case GraphicsOption::InternalResolution:
|
||||
if (value <= 0) {
|
||||
return "Auto";
|
||||
} else {
|
||||
u32 width = 0;
|
||||
u32 height = 0;
|
||||
AuroraGetRenderSize(&width, &height);
|
||||
return fmt::format("{}x ({}x{})", value, width, height);
|
||||
}
|
||||
case GraphicsOption::ShadowResolution:
|
||||
return fmt::format("{}x", value);
|
||||
case GraphicsOption::BloomMode:
|
||||
switch (static_cast<BloomMode>(value)) {
|
||||
case BloomMode::Off:
|
||||
return "Off";
|
||||
case BloomMode::Classic:
|
||||
return "Classic";
|
||||
case BloomMode::Dusk:
|
||||
return "Dusk";
|
||||
}
|
||||
break;
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
return fmt::format("{}%", value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Overlay::Overlay(OverlayProps props)
|
||||
: Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
|
||||
mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* title = mDocument->GetElementById("title")) {
|
||||
title->SetInnerRML(escape(props.title));
|
||||
}
|
||||
if (auto* description = mDocument->GetElementById("description")) {
|
||||
description->SetInnerRML(escape(props.helpText));
|
||||
}
|
||||
if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
|
||||
add_component<SteppedCarousel>(carouselParent,
|
||||
SteppedCarousel::Props{
|
||||
.min = mValueMin,
|
||||
.max = mValueMax,
|
||||
.step = 1,
|
||||
.getValue = [this] { return get_value(mOption); },
|
||||
.onChange = [this](int value) { set_value(mOption, value); },
|
||||
.formatValue =
|
||||
[this](int value) { return format_graphics_setting_value(mOption, value); },
|
||||
});
|
||||
}
|
||||
|
||||
if (auto* footer = mDocument->GetElementById("footer")) {
|
||||
auto& returnButton = add_component<Button>(footer, "\xE2\x86\x90 Return", "footer-button")
|
||||
.on_pressed(pop_document);
|
||||
returnButton.root()->SetClass("return", true);
|
||||
auto& resetButton =
|
||||
add_component<Button>(footer, "Reset to default", "footer-button").on_pressed([this] {
|
||||
reset_default();
|
||||
});
|
||||
resetButton.root()->SetClass("reset", true);
|
||||
}
|
||||
|
||||
// Hide document after transition completion
|
||||
mRoot = mDocument->GetElementById("root");
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot &&
|
||||
*mRoot->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible &&
|
||||
!mRoot->HasAttribute("open"))
|
||||
{
|
||||
Document::hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Overlay::show() {
|
||||
Document::show();
|
||||
mRoot->SetAttribute("open", "");
|
||||
}
|
||||
|
||||
void Overlay::hide() {
|
||||
mRoot->RemoveAttribute("open");
|
||||
}
|
||||
|
||||
void Overlay::update() {
|
||||
for (const auto& component : mComponents) {
|
||||
component->update();
|
||||
}
|
||||
Document::update();
|
||||
}
|
||||
|
||||
bool Overlay::focus() {
|
||||
for (const auto& component : mComponents) {
|
||||
if (component->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Overlay::visible() const {
|
||||
return mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
bool Overlay::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Cancel) {
|
||||
pop_document();
|
||||
return true;
|
||||
}
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
void Overlay::reset_default() {
|
||||
set_value(mOption, mDefaultValue);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "document.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SteppedCarousel : public Component {
|
||||
public:
|
||||
struct Props {
|
||||
int min = 0;
|
||||
int max = 0;
|
||||
int step = 1;
|
||||
std::function<int()> getValue;
|
||||
std::function<void(int)> onChange;
|
||||
std::function<Rml::String(int)> formatValue;
|
||||
};
|
||||
|
||||
SteppedCarousel(Rml::Element* parent, Props props);
|
||||
|
||||
bool focus() override;
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
bool handle_nav_command(NavCommand cmd);
|
||||
void apply(int value);
|
||||
|
||||
Props mProps;
|
||||
Rml::Element* mValueElem = nullptr;
|
||||
};
|
||||
|
||||
enum class GraphicsOption {
|
||||
InternalResolution,
|
||||
ShadowResolution,
|
||||
BloomMode,
|
||||
BloomMultiplier,
|
||||
};
|
||||
|
||||
Rml::String format_graphics_setting_value(GraphicsOption option, int value);
|
||||
|
||||
struct OverlayProps {
|
||||
GraphicsOption option;
|
||||
Rml::String title;
|
||||
Rml::String helpText;
|
||||
int valueMin = 0;
|
||||
int valueMax = 0;
|
||||
int defaultValue = 0;
|
||||
};
|
||||
|
||||
class Overlay : public Document {
|
||||
public:
|
||||
explicit Overlay(OverlayProps props);
|
||||
|
||||
void show() override;
|
||||
void hide() override;
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_component(Args&&... args) {
|
||||
auto child = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mComponents.emplace_back(std::move(child));
|
||||
return ref;
|
||||
}
|
||||
|
||||
void reset_default();
|
||||
|
||||
GraphicsOption mOption;
|
||||
int mValueMin = 0;
|
||||
int mValueMax = 0;
|
||||
int mDefaultValue = 0;
|
||||
std::vector<std::unique_ptr<Component> > mComponents;
|
||||
Rml::Element* mRoot;
|
||||
bool mDismissed = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,151 @@
|
||||
#include "pane.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement("pane");
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Pane::Pane(Rml::Element* parent, Type type) : FluentComponent(createRoot(parent)), mType(type) {
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
|
||||
// If navigating to the next pane, select the focused item
|
||||
if (mType == Type::Controlled && cmd == NavCommand::Right) {
|
||||
auto* target = event.GetTargetElement();
|
||||
int focusedChild = -1;
|
||||
for (size_t i = 0; i < mChildren.size(); ++i) {
|
||||
if (mChildren[i]->contains(target)) {
|
||||
focusedChild = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (focusedChild == -1) {
|
||||
return;
|
||||
}
|
||||
set_selected_item(focusedChild);
|
||||
return;
|
||||
}
|
||||
|
||||
int direction = 0;
|
||||
if (cmd == NavCommand::Down) {
|
||||
direction = 1;
|
||||
} else if (cmd == NavCommand::Up) {
|
||||
direction = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
auto* target = event.GetTargetElement();
|
||||
int focusedChild = -1;
|
||||
for (size_t i = 0; i < mChildren.size(); ++i) {
|
||||
if (mChildren[i]->contains(target)) {
|
||||
focusedChild = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (focusedChild == -1) {
|
||||
return;
|
||||
}
|
||||
int i = focusedChild + direction;
|
||||
while (i >= 0 && i < mChildren.size()) {
|
||||
if (mChildren[i]->focus()) {
|
||||
event.StopPropagation();
|
||||
break;
|
||||
}
|
||||
i += direction;
|
||||
}
|
||||
});
|
||||
|
||||
if (type == Type::Controlled) {
|
||||
// For controlled panes, handle SelectButton Submit events for item selection
|
||||
listen(Rml::EventId::Submit, [this](Rml::Event& event) {
|
||||
int childIndex = -1;
|
||||
for (int i = 0; i < mChildren.size(); ++i) {
|
||||
if (event.GetTargetElement() == mChildren[i]->root()) {
|
||||
childIndex = i;
|
||||
}
|
||||
}
|
||||
set_selected_item(childIndex);
|
||||
// If the selection was handled locally, don't allow it to bubble up to window
|
||||
if (event.GetParameter("handled", false)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Pane::update() {
|
||||
finalize();
|
||||
Component::update();
|
||||
}
|
||||
|
||||
void Pane::set_selected_item(int index) {
|
||||
if (mType == Type::Uncontrolled) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < mChildren.size(); ++i) {
|
||||
mChildren[i]->set_selected(i == index);
|
||||
}
|
||||
}
|
||||
|
||||
bool Pane::focus() {
|
||||
// Focus the first selected child
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->selected() && child->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Otherwise, focus the first focusable child
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_section(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetClass("section-heading", true);
|
||||
elem->SetInnerRML(escape(text));
|
||||
return elem;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_text(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetInnerRML(escape(text));
|
||||
return elem;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_rml(const Rml::String& rml) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetInnerRML(rml);
|
||||
return elem;
|
||||
}
|
||||
|
||||
void Pane::finalize() {
|
||||
if (finalized) {
|
||||
return;
|
||||
}
|
||||
finalized = true;
|
||||
|
||||
// Append spacer element to the bottom. RmlUi does not properly handle
|
||||
// padding-bottom or margin-bottom on a scrollable flex container, so
|
||||
// we need to create a fake spacer with an actual layout height to get
|
||||
// padding at the bottom of a scrollable container.
|
||||
append(mRoot, "spacer");
|
||||
}
|
||||
|
||||
void Pane::clear() {
|
||||
clear_children();
|
||||
finalized = false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Pane : public FluentComponent<Pane> {
|
||||
public:
|
||||
enum class Type {
|
||||
Controlled,
|
||||
Uncontrolled,
|
||||
};
|
||||
|
||||
explicit Pane(Rml::Element* parent, Type type);
|
||||
|
||||
bool focus() override;
|
||||
void update() override;
|
||||
|
||||
void set_selected_item(int index);
|
||||
|
||||
Rml::Element* add_section(const Rml::String& text);
|
||||
ControlledButton& add_button(ControlledButton::Props props) {
|
||||
return add_child<ControlledButton>(std::move(props));
|
||||
}
|
||||
Button& add_button(Rml::String text) { return add_child<Button>(std::move(text)); }
|
||||
ControlledSelectButton& add_select_button(ControlledSelectButton::Props props) {
|
||||
return add_child<ControlledSelectButton>(std::move(props));
|
||||
}
|
||||
Rml::Element* add_text(const Rml::String& text);
|
||||
Rml::Element* add_rml(const Rml::String& rml);
|
||||
void finalize();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
Type mType;
|
||||
bool finalized = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,126 @@
|
||||
#include "popup.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "editor.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "dusk/main.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/tabbing.rcss" />
|
||||
<link type="text/rcss" href="res/rml/popup.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<popup id="popup"></div>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
}
|
||||
|
||||
Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
|
||||
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{.autoSelect = false});
|
||||
mTabBar->add_tab("Settings", [] { push_document(std::make_unique<SettingsWindow>()); });
|
||||
mTabBar->add_tab("Warp", [] {
|
||||
// TODO
|
||||
});
|
||||
mTabBar->add_tab("Editor", [] { push_document(std::make_unique<EditorWindow>()); });
|
||||
mTabBar->add_tab("Reset", [this] {
|
||||
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
|
||||
mTabBar->set_active_tab(-1);
|
||||
hide();
|
||||
});
|
||||
mTabBar->add_tab("Exit", [] { IsRunning = false; });
|
||||
|
||||
// Hide document after transition completion
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot &&
|
||||
*mRoot->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible &&
|
||||
!mRoot->HasAttribute("open"))
|
||||
{
|
||||
Document::hide();
|
||||
}
|
||||
});
|
||||
|
||||
// We start hidden, but want focus for an open nav event
|
||||
mDocument->Focus();
|
||||
}
|
||||
|
||||
void Popup::show() {
|
||||
Document::show();
|
||||
mRoot->SetAttribute("open", "");
|
||||
mTabBar->set_active_tab(-1);
|
||||
}
|
||||
|
||||
void Popup::hide() {
|
||||
mRoot->RemoveAttribute("open");
|
||||
}
|
||||
|
||||
void Popup::update() {
|
||||
update_safe_area();
|
||||
Document::update();
|
||||
}
|
||||
|
||||
void Popup::update_safe_area() noexcept {
|
||||
if (mDocument == nullptr || mTabBar == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rml::Context* context = mDocument->GetContext();
|
||||
Insets safeInsets = safe_area_insets(context);
|
||||
safeInsets = {
|
||||
0.0f,
|
||||
std::round(safeInsets.right),
|
||||
0.0f,
|
||||
std::round(safeInsets.left),
|
||||
};
|
||||
if (safeInsets == mTabBarPadding) {
|
||||
return;
|
||||
}
|
||||
|
||||
mTabBarPadding = safeInsets;
|
||||
auto* tabBar = mTabBar->root();
|
||||
tabBar->SetProperty(
|
||||
Rml::PropertyId::PaddingRight, Rml::Property(safeInsets.right, Rml::Unit::PX));
|
||||
tabBar->SetProperty(
|
||||
Rml::PropertyId::PaddingLeft, Rml::Property(safeInsets.left, Rml::Unit::PX));
|
||||
}
|
||||
|
||||
void Popup::toggle() {
|
||||
if (visible()) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
bool Popup::visible() const {
|
||||
return mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
bool Popup::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Cancel) {
|
||||
hide();
|
||||
return true;
|
||||
}
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
bool Popup::focus() {
|
||||
return mTabBar->focus();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "document.hpp"
|
||||
#include "tab_bar.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Popup : public Document {
|
||||
public:
|
||||
Popup();
|
||||
|
||||
Popup(const Popup&) = delete;
|
||||
Popup& operator=(const Popup&) = delete;
|
||||
|
||||
void show() override;
|
||||
void hide() override;
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
|
||||
void toggle();
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
void update_safe_area() noexcept;
|
||||
|
||||
Rml::Element* mRoot;
|
||||
std::unique_ptr<TabBar> mTabBar;
|
||||
std::unique_ptr<Button> mCloseButton;
|
||||
Insets mTabBarPadding;
|
||||
bool mVisible = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,230 @@
|
||||
#include "prelaunch.hpp"
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/file_select.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/ui/prelaunch_options.hpp"
|
||||
#include "version.h"
|
||||
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/prelaunch.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="menu">
|
||||
<div class="hero intro-item delay-0">
|
||||
<div class="eyebrow"><span>Twilit Realm</span> presents</div>
|
||||
<img src="res/logo-mascot.png" />
|
||||
</div>
|
||||
<div id="menu-list" />
|
||||
</div>
|
||||
<div class="disk-status intro-item delay-4">
|
||||
<span id="status" class="status" />
|
||||
<span id="detail" class="detail" />
|
||||
</div>
|
||||
<div class="version-info intro-item delay-5">
|
||||
<div class="version">Version <span id="version-text"></span></div>
|
||||
<div class="update"><span>Update available!</span> Download</div>
|
||||
</div>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
static constexpr std::array<SDL_DialogFileFilter, 2> kDiscFileFilters{{
|
||||
{"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"},
|
||||
{"All Files", "*"},
|
||||
}};
|
||||
|
||||
void file_dialog_callback(void*, const char* path, const char* error) {
|
||||
auto& state = prelaunch_state();
|
||||
if (error != nullptr) {
|
||||
return;
|
||||
}
|
||||
if (path == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedIsoPath = path;
|
||||
state.errorString.clear();
|
||||
refresh_path_state();
|
||||
getSettings().backend.isoPath.setValue(state.selectedIsoPath);
|
||||
config::Save();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PrelaunchState sPrelaunchState;
|
||||
|
||||
PrelaunchState& prelaunch_state() noexcept {
|
||||
return sPrelaunchState;
|
||||
}
|
||||
|
||||
void refresh_path_state() noexcept {
|
||||
auto& state = prelaunch_state();
|
||||
state.isPal = !state.selectedIsoPath.empty() && iso::isPal(state.selectedIsoPath.c_str());
|
||||
}
|
||||
|
||||
void ensure_initialized() noexcept {
|
||||
auto& state = prelaunch_state();
|
||||
if (state.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedIsoPath = getSettings().backend.isoPath;
|
||||
state.initialGraphicsBackend = getSettings().backend.graphicsBackend;
|
||||
state.errorString.clear();
|
||||
state.initialized = true;
|
||||
refresh_path_state();
|
||||
}
|
||||
|
||||
bool is_selected_path_valid() noexcept {
|
||||
return !prelaunch_state().selectedIsoPath.empty() && SDL_GetPathInfo(prelaunch_state().selectedIsoPath.c_str(), nullptr);
|
||||
}
|
||||
|
||||
void open_iso_picker() noexcept {
|
||||
ensure_initialized();
|
||||
ShowFileSelect(&file_dialog_callback, nullptr, aurora::window::get_sdl_window(),
|
||||
kDiscFileFilters.data(), static_cast<int>(kDiscFileFilters.size()), nullptr, false);
|
||||
}
|
||||
|
||||
void apply_intro_animation(Rml::Element* element, const char* delay_class) {
|
||||
if (element == nullptr || delay_class == nullptr) {
|
||||
return;
|
||||
}
|
||||
element->SetClass("intro-item", true);
|
||||
element->SetClass(delay_class, true);
|
||||
}
|
||||
|
||||
Prelaunch::Prelaunch() : Document(kDocumentSource) {
|
||||
ensure_initialized();
|
||||
|
||||
if (auto* menuList = mDocument->GetElementById("menu-list")) {
|
||||
const bool hasValidPath = is_selected_path_valid();
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, hasValidPath ? "Start Game" : "Select Disk Image"));
|
||||
mMenuButtons.back()->on_pressed([] {
|
||||
if (!is_selected_path_valid()) {
|
||||
open_iso_picker();
|
||||
return;
|
||||
}
|
||||
pop_document();
|
||||
IsGameLaunched = true;
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Options"));
|
||||
mMenuButtons.back()->on_pressed([] { push_document(std::make_unique<PrelaunchOptions>());});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit To Desktop"));
|
||||
mMenuButtons.back()->on_pressed([] { IsRunning = false; });
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
|
||||
}
|
||||
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
|
||||
int direction = 0;
|
||||
if (cmd == NavCommand::Down) {
|
||||
direction = 1;
|
||||
} else if (cmd == NavCommand::Up) {
|
||||
direction = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
auto* target = event.GetTargetElement();
|
||||
int focusedButton = -1;
|
||||
for (size_t i = 0; i < mMenuButtons.size(); ++i) {
|
||||
if (mMenuButtons[i]->contains(target)) {
|
||||
focusedButton = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (focusedButton == -1) {
|
||||
return;
|
||||
}
|
||||
int i = focusedButton + direction;
|
||||
while (i >= 0 && i < mMenuButtons.size()) {
|
||||
if (mMenuButtons[i]->focus()) {
|
||||
event.StopPropagation();
|
||||
break;
|
||||
}
|
||||
i += direction;
|
||||
}
|
||||
});
|
||||
|
||||
mDiscStatus = mDocument->GetElementById("status");
|
||||
mDiscDetail = mDocument->GetElementById("detail");
|
||||
mVersion = mDocument->GetElementById("version-text");
|
||||
|
||||
listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
auto* target = event.GetTargetElement();
|
||||
if (target != nullptr && target->GetTagName() == "button") {
|
||||
target->SetClass("anim-done", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Prelaunch::update() {
|
||||
ensure_initialized();
|
||||
refresh_path_state();
|
||||
|
||||
if (!mEntranceAnimationStarted && mDocument != nullptr) {
|
||||
mDocument->SetClass("animate-in", true);
|
||||
mEntranceAnimationStarted = true;
|
||||
}
|
||||
|
||||
auto& state = prelaunch_state();
|
||||
const bool hasValidPath = is_selected_path_valid();
|
||||
if (hasValidPath && getSettings().backend.skipPreLaunchUI) {
|
||||
pop_document();
|
||||
IsGameLaunched = true;
|
||||
}
|
||||
|
||||
if (!mMenuButtons.empty()) {
|
||||
mMenuButtons[0]->set_text(hasValidPath ? "Start Game" : "Select Disk Image");
|
||||
}
|
||||
if (mDiscStatus != nullptr) {
|
||||
if (hasValidPath) {
|
||||
mDiscStatus->RemoveAttribute("bad");
|
||||
mDiscStatus->SetInnerRML("Disc Ready");
|
||||
} else {
|
||||
mDiscStatus->SetAttribute("bad", "");
|
||||
mDiscStatus->SetInnerRML("Disk Not Found");
|
||||
}
|
||||
}
|
||||
if (mDiscDetail != nullptr) {
|
||||
if (hasValidPath) {
|
||||
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Block);
|
||||
mDiscDetail->SetInnerRML(prelaunch_state().isPal ? "GameCube • PAL" : "GameCube • USA");
|
||||
} else {
|
||||
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::None);
|
||||
}
|
||||
}
|
||||
if (mVersion != nullptr) {
|
||||
mVersion->SetInnerRML(escape(DUSK_WC_DESCRIBE));
|
||||
}
|
||||
|
||||
Document::update();
|
||||
}
|
||||
|
||||
bool Prelaunch::focus() {
|
||||
if (mMenuButtons.empty()) {
|
||||
return false;
|
||||
}
|
||||
return mMenuButtons[0]->focus();
|
||||
}
|
||||
|
||||
bool Prelaunch::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "document.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Prelaunch : public Document {
|
||||
public:
|
||||
Prelaunch();
|
||||
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
bool mEntranceAnimationStarted = false;
|
||||
std::vector<std::unique_ptr<Button>> mMenuButtons;
|
||||
Rml::Element* mDiscStatus = nullptr;
|
||||
Rml::Element* mDiscDetail = nullptr;
|
||||
Rml::Element* mVersion = nullptr;
|
||||
};
|
||||
|
||||
class PrelaunchOptions;
|
||||
|
||||
struct PrelaunchState {
|
||||
std::string selectedIsoPath;
|
||||
std::string errorString;
|
||||
std::string initialGraphicsBackend;
|
||||
bool isPal = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
PrelaunchState& prelaunch_state() noexcept;
|
||||
void ensure_initialized() noexcept;
|
||||
void refresh_path_state() noexcept;
|
||||
bool is_selected_path_valid() noexcept;
|
||||
void open_iso_picker() noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,263 @@
|
||||
#include "prelaunch_options.hpp"
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "pane.hpp"
|
||||
#include "prelaunch.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
static constexpr std::array<const char*, 5> kLanguageNames = {
|
||||
"English", "German", "French", "Spanish", "Italian",
|
||||
};
|
||||
|
||||
// TODO: Copied from ImGui prelaunch. Needs a refactor?
|
||||
bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
|
||||
if (backend == "auto") {
|
||||
outBackend = BACKEND_AUTO;
|
||||
return true;
|
||||
}
|
||||
if (backend == "d3d11") {
|
||||
outBackend = BACKEND_D3D11;
|
||||
return true;
|
||||
}
|
||||
if (backend == "d3d12") {
|
||||
outBackend = BACKEND_D3D12;
|
||||
return true;
|
||||
}
|
||||
if (backend == "metal") {
|
||||
outBackend = BACKEND_METAL;
|
||||
return true;
|
||||
}
|
||||
if (backend == "vulkan") {
|
||||
outBackend = BACKEND_VULKAN;
|
||||
return true;
|
||||
}
|
||||
if (backend == "opengl") {
|
||||
outBackend = BACKEND_OPENGL;
|
||||
return true;
|
||||
}
|
||||
if (backend == "opengles") {
|
||||
outBackend = BACKEND_OPENGLES;
|
||||
return true;
|
||||
}
|
||||
if (backend == "webgpu") {
|
||||
outBackend = BACKEND_WEBGPU;
|
||||
return true;
|
||||
}
|
||||
if (backend == "null") {
|
||||
outBackend = BACKEND_NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string_view backend_name(AuroraBackend backend) {
|
||||
switch (backend) {
|
||||
default:
|
||||
return "Auto";
|
||||
case BACKEND_D3D12:
|
||||
return "D3D12";
|
||||
case BACKEND_D3D11:
|
||||
return "D3D11";
|
||||
case BACKEND_METAL:
|
||||
return "Metal";
|
||||
case BACKEND_VULKAN:
|
||||
return "Vulkan";
|
||||
case BACKEND_OPENGL:
|
||||
return "OpenGL";
|
||||
case BACKEND_OPENGLES:
|
||||
return "OpenGL ES";
|
||||
case BACKEND_WEBGPU:
|
||||
return "WebGPU";
|
||||
case BACKEND_NULL:
|
||||
return "Null";
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view backend_id(AuroraBackend backend) {
|
||||
switch (backend) {
|
||||
default:
|
||||
return "auto";
|
||||
case BACKEND_D3D12:
|
||||
return "d3d12";
|
||||
case BACKEND_D3D11:
|
||||
return "d3d11";
|
||||
case BACKEND_METAL:
|
||||
return "metal";
|
||||
case BACKEND_VULKAN:
|
||||
return "vulkan";
|
||||
case BACKEND_OPENGL:
|
||||
return "opengl";
|
||||
case BACKEND_OPENGLES:
|
||||
return "opengles";
|
||||
case BACKEND_WEBGPU:
|
||||
return "webgpu";
|
||||
case BACKEND_NULL:
|
||||
return "null";
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<AuroraBackend> available_backends() {
|
||||
std::vector<AuroraBackend> backends;
|
||||
backends.emplace_back(BACKEND_AUTO);
|
||||
size_t backendCount = 0;
|
||||
const AuroraBackend* raw = aurora_get_available_backends(&backendCount);
|
||||
for (size_t i = 0; i < backendCount; ++i) {
|
||||
// Do not expose NULL or D3D11
|
||||
if (raw[i] != BACKEND_NULL || raw[i] == BACKEND_D3D11) {
|
||||
backends.emplace_back(raw[i]);
|
||||
}
|
||||
}
|
||||
return backends;
|
||||
}
|
||||
|
||||
class LanguageSelect final : public SelectButton {
|
||||
public:
|
||||
explicit LanguageSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Language"}) {}
|
||||
|
||||
void update() override {
|
||||
ensure_initialized();
|
||||
refresh_path_state();
|
||||
|
||||
const bool validPath = is_selected_path_valid();
|
||||
const bool ntscDiscLocked = validPath && !prelaunch_state().isPal;
|
||||
|
||||
if (ntscDiscLocked) {
|
||||
if (getSettings().game.language.getValue() != GameLanguage::English) {
|
||||
getSettings().game.language.setValue(GameLanguage::English);
|
||||
config::Save();
|
||||
}
|
||||
set_disabled(true);
|
||||
} else {
|
||||
set_disabled(false);
|
||||
}
|
||||
|
||||
const auto lang = getSettings().game.language.getValue();
|
||||
auto value = static_cast<u8>(lang);
|
||||
if (value >= kLanguageNames.size()) {
|
||||
getSettings().game.language.setValue(GameLanguage::English);
|
||||
config::Save();
|
||||
value = static_cast<u8>(getSettings().game.language.getValue());
|
||||
}
|
||||
set_value_label(kLanguageNames[value]);
|
||||
SelectButton::update();
|
||||
}
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(NavCommand cmd) override {
|
||||
if (disabled()) {
|
||||
return false;
|
||||
}
|
||||
if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr int n = static_cast<int>(kLanguageNames.size());
|
||||
int idx = static_cast<int>(getSettings().game.language.getValue());
|
||||
const int dir = (cmd == NavCommand::Left) ? -1 : 1;
|
||||
idx = ((idx + dir) % n + n) % n;
|
||||
getSettings().game.language.setValue(static_cast<GameLanguage>(idx));
|
||||
config::Save();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class BackendSelect final : public SelectButton {
|
||||
public:
|
||||
explicit BackendSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Graphics Backend"}) {}
|
||||
|
||||
void update() override {
|
||||
AuroraBackend configuredBackend = BACKEND_AUTO;
|
||||
const auto configuredId = getSettings().backend.graphicsBackend.getValue();
|
||||
if (!try_parse_backend(configuredId, configuredBackend)) {
|
||||
configuredBackend = BACKEND_AUTO;
|
||||
}
|
||||
// Do not expose NULL or D3D11
|
||||
if (configuredBackend == BACKEND_NULL || configuredBackend == BACKEND_D3D11) {
|
||||
getSettings().backend.graphicsBackend.setValue("auto");
|
||||
config::Save();
|
||||
configuredBackend = BACKEND_AUTO;
|
||||
}
|
||||
|
||||
const auto backend = getSettings().backend.graphicsBackend.getValue();
|
||||
Rml::String value = backend_name(configuredBackend).data();
|
||||
if (backend != prelaunch_state().initialGraphicsBackend) {
|
||||
value += " (restart required)";
|
||||
}
|
||||
set_value_label(value);
|
||||
SelectButton::update();
|
||||
}
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(NavCommand cmd) override {
|
||||
if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto backends = available_backends();
|
||||
const int n = static_cast<int>(backends.size());
|
||||
if (n <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AuroraBackend configuredBackend = BACKEND_AUTO;
|
||||
const auto configuredId = getSettings().backend.graphicsBackend.getValue();
|
||||
if (!try_parse_backend(configuredId, configuredBackend)) {
|
||||
configuredBackend = BACKEND_AUTO;
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (backends[static_cast<size_t>(i)] == configuredBackend) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const int dir = (cmd == NavCommand::Left) ? -1 : 1;
|
||||
idx = ((idx + dir) % n + n) % n;
|
||||
getSettings().backend.graphicsBackend.setValue(std::string(backend_id(backends[static_cast<size_t>(idx)])));
|
||||
config::Save();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class SaveTypeSelect final : public SelectButton {
|
||||
public:
|
||||
explicit SaveTypeSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Save File Type"}) {}
|
||||
|
||||
void update() override {
|
||||
const CARDFileType cft = static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
|
||||
set_value_label(cft == CARD_GCIFOLDER ? "GCI Folder" : "Card Image");
|
||||
SelectButton::update();
|
||||
}
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(NavCommand cmd) override {
|
||||
if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CARDFileType cft = static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
|
||||
const CARDFileType newValue = cft == CARD_GCIFOLDER ? CARD_RAWIMAGE : CARD_GCIFOLDER;
|
||||
getSettings().backend.cardFileType.setValue(newValue);
|
||||
config::Save();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
PrelaunchOptions::PrelaunchOptions() {
|
||||
add_tab("Options", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
leftPane.add_child<LanguageSelect>();
|
||||
leftPane.add_child<BackendSelect>();
|
||||
leftPane.add_child<SaveTypeSelect>();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class PrelaunchOptions : public Window {
|
||||
public:
|
||||
PrelaunchOptions();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "select_button.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement("select-button");
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SelectButton::SelectButton(Rml::Element* parent, Props props)
|
||||
: FluentComponent(createRoot(parent)) {
|
||||
mKeyElem = append(mRoot, "key");
|
||||
mValueElem = append(mRoot, "value");
|
||||
update_props(std::move(props));
|
||||
on_nav_command([this](Rml::Event&, NavCommand cmd) { return handle_nav_command(cmd); });
|
||||
}
|
||||
|
||||
void SelectButton::set_value_label(const Rml::String& value) {
|
||||
if (mProps.value != value) {
|
||||
mValueElem->SetInnerRML(escape(value));
|
||||
mProps.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
void SelectButton::update_props(Props props) {
|
||||
if (mProps.key != props.key) {
|
||||
mKeyElem->SetInnerRML(escape(props.key));
|
||||
}
|
||||
set_value_label(props.value);
|
||||
mProps = std::move(props);
|
||||
}
|
||||
|
||||
bool SelectButton::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
mRoot->DispatchEvent(Rml::EventId::Submit, {});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BaseControlledSelectButton::update() {
|
||||
set_disabled(disabled());
|
||||
set_value_label(format_value());
|
||||
SelectButton::update();
|
||||
}
|
||||
|
||||
bool ControlledSelectButton::disabled() const {
|
||||
if (mIsDisabled) {
|
||||
return mIsDisabled();
|
||||
}
|
||||
return BaseControlledSelectButton::disabled();
|
||||
}
|
||||
|
||||
Rml::String ControlledSelectButton::format_value() {
|
||||
if (!mGetValue) {
|
||||
return "";
|
||||
}
|
||||
return mGetValue();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "component.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SelectButton : public FluentComponent<SelectButton> {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
Rml::String value;
|
||||
};
|
||||
|
||||
SelectButton(Rml::Element* parent, Props props);
|
||||
|
||||
void set_value_label(const Rml::String& value);
|
||||
|
||||
protected:
|
||||
void update_props(Props props);
|
||||
virtual bool handle_nav_command(NavCommand cmd);
|
||||
|
||||
Props mProps;
|
||||
Rml::Element* mKeyElem = nullptr;
|
||||
Rml::Element* mValueElem = nullptr;
|
||||
std::function<void()> mOnHover;
|
||||
};
|
||||
|
||||
class BaseControlledSelectButton : public SelectButton {
|
||||
public:
|
||||
BaseControlledSelectButton(Rml::Element* parent, Props props)
|
||||
: SelectButton(parent, std::move(props)) {}
|
||||
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
virtual Rml::String format_value() = 0;
|
||||
};
|
||||
|
||||
class ControlledSelectButton : public BaseControlledSelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<bool()> isDisabled;
|
||||
};
|
||||
|
||||
ControlledSelectButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton(parent, {std::move(props.key)}),
|
||||
mGetValue(std::move(props.getValue)), mIsDisabled(std::move(props.isDisabled)) {}
|
||||
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override;
|
||||
|
||||
std::function<Rml::String()> mGetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,581 @@
|
||||
#include "settings.hpp"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "aurora/gfx.h"
|
||||
#include "bool_button.hpp"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
#include "number_button.hpp"
|
||||
#include "overlay.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
void reset_for_speedrun_mode() {
|
||||
mDoMain::developmentMode = -1;
|
||||
|
||||
getSettings().game.damageMultiplier.setValue(1);
|
||||
getSettings().game.instantDeath.setValue(false);
|
||||
getSettings().game.noHeartDrops.setValue(false);
|
||||
|
||||
getSettings().game.infiniteHearts.setValue(false);
|
||||
getSettings().game.infiniteArrows.setValue(false);
|
||||
getSettings().game.infiniteBombs.setValue(false);
|
||||
getSettings().game.infiniteOil.setValue(false);
|
||||
getSettings().game.infiniteOxygen.setValue(false);
|
||||
getSettings().game.infiniteRupees.setValue(false);
|
||||
getSettings().game.enableIndefiniteItemDrops.setValue(false);
|
||||
|
||||
getSettings().game.moonJump.setValue(false);
|
||||
getSettings().game.superClawshot.setValue(false);
|
||||
getSettings().game.alwaysGreatspin.setValue(false);
|
||||
getSettings().game.enableFastIronBoots.setValue(false);
|
||||
getSettings().game.canTransformAnywhere.setValue(false);
|
||||
getSettings().game.fastSpinner.setValue(false);
|
||||
getSettings().game.freeMagicArmor.setValue(false);
|
||||
|
||||
getSettings().game.enableTurboKeybind.setValue(false);
|
||||
}
|
||||
|
||||
const Rml::String kInternalResolutionHelpText =
|
||||
"Configure the resolution used for rendering the game. Higher values are more demanding on "
|
||||
"your graphics hardware.";
|
||||
const Rml::String kShadowResolutionHelpText =
|
||||
"Configure the shadow-map resolution. Higher values improve shadow quality but increase GPU "
|
||||
"and memory usage.";
|
||||
const Rml::String kBloomHelpText =
|
||||
"Configure the post-processing bloom effect. Classic uses the original bloom pass; Dusk uses "
|
||||
"a higher-quality bloom pass.";
|
||||
const Rml::String kBloomBrightnessHelpText =
|
||||
"Configure bloom intensity. Higher values make bright areas glow more strongly.";
|
||||
|
||||
int bloom_multiplier_percent() {
|
||||
return std::clamp(
|
||||
static_cast<int>(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0, 100);
|
||||
}
|
||||
|
||||
int float_setting_percent(ConfigVar<float>& var) {
|
||||
return static_cast<int>(var.getValue() * 100.0f + 0.5f);
|
||||
}
|
||||
|
||||
bool gyro_enabled() {
|
||||
return getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal;
|
||||
}
|
||||
|
||||
struct ConfigBoolProps {
|
||||
Rml::String key;
|
||||
Rml::String helpText;
|
||||
std::function<void(bool)> onChange;
|
||||
std::function<bool()> isDisabled;
|
||||
};
|
||||
|
||||
SelectButton& config_bool_select(
|
||||
Pane& leftPane, Pane& rightPane, ConfigVar<bool>& var, ConfigBoolProps props) {
|
||||
return leftPane
|
||||
.add_child<BoolButton>(BoolButton::Props{
|
||||
.key = std::move(props.key),
|
||||
.getValue = [&var] { return var.getValue(); },
|
||||
.setValue =
|
||||
[&var, callback = std::move(props.onChange)](bool value) {
|
||||
if (value == var.getValue()) {
|
||||
return;
|
||||
}
|
||||
var.setValue(value);
|
||||
config::Save();
|
||||
if (callback) {
|
||||
callback(value);
|
||||
}
|
||||
},
|
||||
.isDisabled = std::move(props.isDisabled),
|
||||
})
|
||||
.on_focus([&rightPane, helpText = std::move(props.helpText)](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_rml(helpText);
|
||||
});
|
||||
}
|
||||
|
||||
SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar<float>& var,
|
||||
Rml::String key, Rml::String helpText, int min, int max, int step = 5,
|
||||
std::function<bool()> isDisabled = {}) {
|
||||
return leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = std::move(key),
|
||||
.getValue = [&var] { return float_setting_percent(var); },
|
||||
.setValue =
|
||||
[&var, min, max](int value) {
|
||||
var.setValue(std::clamp(value, min, max) / 100.0f);
|
||||
config::Save();
|
||||
},
|
||||
.isDisabled = std::move(isDisabled),
|
||||
.min = min,
|
||||
.max = max,
|
||||
.step = step,
|
||||
.suffix = "%",
|
||||
})
|
||||
.on_focus([&rightPane, helpText = std::move(helpText)](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(helpText);
|
||||
});
|
||||
}
|
||||
|
||||
class ControllerConfigWindow : public Window {
|
||||
public:
|
||||
ControllerConfigWindow() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
add_tab(fmt::format("Port {}", i + 1), [this](Rml::Element* content) {
|
||||
auto& pane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
pane.add_section("Coming soon");
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsWindow::SettingsWindow() {
|
||||
add_tab("Audio", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
leftPane.add_section("Volume");
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Master Volume",
|
||||
.getValue = [] { return getSettings().audio.masterVolume.getValue(); },
|
||||
.setValue =
|
||||
[](int value) {
|
||||
getSettings().audio.masterVolume.setValue(value);
|
||||
config::Save();
|
||||
audio::SetMasterVolume(value / 100.f);
|
||||
},
|
||||
.max = 100,
|
||||
.suffix = "%",
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Adjusts the volume of all sounds in the game.");
|
||||
});
|
||||
|
||||
leftPane.add_section("Effects");
|
||||
config_bool_select(leftPane, rightPane, getSettings().audio.enableReverb,
|
||||
{
|
||||
.key = "Enable Reverb",
|
||||
.helpText = "Enables the reverb effect in game audio.",
|
||||
.onChange = [](bool value) { audio::SetEnableReverb(value); },
|
||||
});
|
||||
|
||||
leftPane.add_section("Tweaks");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.noLowHpSound,
|
||||
{
|
||||
.key = "No Low HP Sound",
|
||||
.helpText = "Disable the beeping sound when having low health.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.midnasLamentNonStop,
|
||||
{
|
||||
.key = "Non-Stop Midna's Lament",
|
||||
.helpText = "Prevents enemy music while Midna's Lament is playing.",
|
||||
});
|
||||
});
|
||||
|
||||
add_tab("Cheats", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
auto addCheat = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
});
|
||||
};
|
||||
|
||||
leftPane.add_section("Resources");
|
||||
addCheat("Infinite Hearts", getSettings().game.infiniteHearts, "Keeps your health full.");
|
||||
addCheat(
|
||||
"Infinite Arrows", getSettings().game.infiniteArrows, "Keeps your arrow count full.");
|
||||
addCheat("Infinite Bombs", getSettings().game.infiniteBombs, "Keeps all bomb bags full.");
|
||||
addCheat("Infinite Oil", getSettings().game.infiniteOil, "Keeps your lantern oil full.");
|
||||
addCheat("Infinite Oxygen", getSettings().game.infiniteOxygen,
|
||||
"Keeps your underwater oxygen meter full.");
|
||||
addCheat(
|
||||
"Infinite Rupees", getSettings().game.infiniteRupees, "Keeps your rupee count full.");
|
||||
addCheat("No Item Timer", getSettings().game.enableIndefiniteItemDrops,
|
||||
"Item drops such as rupees and hearts will never disappear after they drop.");
|
||||
|
||||
leftPane.add_section("Abilities");
|
||||
addCheat(
|
||||
"Moon Jump (R+A)", getSettings().game.moonJump, "Hold R and A to rise into the air.");
|
||||
addCheat("Super Clawshot", getSettings().game.superClawshot,
|
||||
"Extends clawshot behavior beyond the normal game rules.");
|
||||
addCheat("Always Greatspin", getSettings().game.alwaysGreatspin,
|
||||
"Allows the Great Spin attack without requiring full health.");
|
||||
addCheat("Fast Iron Boots", getSettings().game.enableFastIronBoots,
|
||||
"Speeds up movement while wearing the Iron Boots.");
|
||||
addCheat("Can Transform Anywhere", getSettings().game.canTransformAnywhere,
|
||||
"Allows transforming even if NPCs are looking.");
|
||||
addCheat("Fast Spinner", getSettings().game.fastSpinner,
|
||||
"Speeds up Spinner movement while holding R.");
|
||||
addCheat("Free Magic Armor", getSettings().game.freeMagicArmor,
|
||||
"Lets the magic armor work without consuming rupees.");
|
||||
});
|
||||
|
||||
add_tab("Gameplay", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
auto addOption = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
});
|
||||
};
|
||||
auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
});
|
||||
};
|
||||
|
||||
leftPane.add_section("General");
|
||||
addOption("Mirror Mode", getSettings().game.enableMirrorMode,
|
||||
"Mirrors the world horizontally, matching the Wii version of the game.");
|
||||
addOption("Disable Main HUD", getSettings().game.disableMainHUD,
|
||||
"Disables the main HUD of the game.<br/>Useful for recording or a more immersive "
|
||||
"experience.");
|
||||
addOption("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches,
|
||||
"Restores patched glitches from Wii USA 1.0, the first released version.");
|
||||
addOption("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation,
|
||||
"Enables rotating Link in the collection menu with the C-Stick.");
|
||||
|
||||
leftPane.add_section("Difficulty");
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Damage Multiplier",
|
||||
.getValue = [] { return getSettings().game.damageMultiplier.getValue(); },
|
||||
.setValue =
|
||||
[](int value) {
|
||||
getSettings().game.damageMultiplier.setValue(value);
|
||||
config::Save();
|
||||
},
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
.min = 1,
|
||||
.max = 8,
|
||||
.prefix = "x",
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Multiplies incoming damage.");
|
||||
});
|
||||
addSpeedrunDisabledOption(
|
||||
"Instant Death", getSettings().game.instantDeath, "Any hit will instantly kill you.");
|
||||
addSpeedrunDisabledOption("No Heart Drops", getSettings().game.noHeartDrops,
|
||||
"Hearts will never drop from enemies, pots, and various other places.");
|
||||
|
||||
leftPane.add_section("Quality of Life");
|
||||
addOption("Bigger Wallets", getSettings().game.biggerWallets,
|
||||
"Wallet sizes are like in the HD version. (500, 1000, 2000)");
|
||||
addOption("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes,
|
||||
"Rupees will not play cutscenes after you have collected them the first time.");
|
||||
addOption("Faster Climbing", getSettings().game.fastClimbing,
|
||||
"Quicker climbing on ladders and vines like the HD version.");
|
||||
addOption("Faster Tears of Light", getSettings().game.fastTears,
|
||||
"Tears of Light dropped by Shadow Insects pop out faster like the HD version.");
|
||||
addOption("Instant Saves", getSettings().game.instantSaves,
|
||||
"Skips the delay when writing to the Memory Card.");
|
||||
addOption("Hold B for Instant Text", getSettings().game.instantText,
|
||||
"Makes text scroll immediately by holding B.");
|
||||
addOption("No Climbing Miss Animation", getSettings().game.noMissClimbing,
|
||||
"Prevents Link from playing a struggle animation when grabbing ledges or climbing on "
|
||||
"vines.");
|
||||
addOption("No Rupee Returns", getSettings().game.noReturnRupees,
|
||||
"Always collect Rupees even if your Wallet is too full.");
|
||||
addOption("No Sword Recoil", getSettings().game.noSwordRecoil,
|
||||
"Link will not recoil when his sword hits walls.");
|
||||
addOption("No 2nd Fish for Cat", getSettings().game.no2ndFishForCat,
|
||||
"Skip needing to catch a second fish for Sera's cat.");
|
||||
addOption("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen,
|
||||
"Skips the TV calibration screen shown when loading a save.");
|
||||
addOption("Skip Warning Screen", getSettings().game.skipWarningScreen,
|
||||
"Skips the warning screen shown when starting the game.");
|
||||
addOption("Sun's Song (R+X)", getSettings().game.sunsSong,
|
||||
"Allows Wolf Link to howl and change the time of day.");
|
||||
addOption("Quick Transform (R+Y)", getSettings().game.enableQuickTransform,
|
||||
"Transform instantly by pressing R and Y simultaneously.");
|
||||
|
||||
leftPane.add_section("Speedrunning");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.speedrunMode,
|
||||
{
|
||||
.key = "Speedrun Mode",
|
||||
.helpText =
|
||||
"Enables speedrunning options while restricting certain gameplay modifiers.",
|
||||
.onChange = [](bool) { reset_for_speedrun_mode(); },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled,
|
||||
{
|
||||
.key = "LiveSplit Connection",
|
||||
.helpText = "Connect to LiveSplit server on localhost:16834.",
|
||||
.onChange =
|
||||
[](bool enabled) {
|
||||
if (enabled) {
|
||||
speedrun::connectLiveSplit();
|
||||
} else {
|
||||
speedrun::disconnectLiveSplit();
|
||||
}
|
||||
},
|
||||
.isDisabled = [] { return !getSettings().game.speedrunMode; },
|
||||
});
|
||||
});
|
||||
|
||||
add_tab("Input", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
auto addOption = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText, std::function<bool()> isDisabled = {}) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = std::move(isDisabled),
|
||||
});
|
||||
};
|
||||
|
||||
leftPane.add_section("Controller");
|
||||
leftPane.add_button("Configure Controller")
|
||||
.on_pressed([] { push_document(std::make_unique<ControllerConfigWindow>()); })
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Open controller binding configuration.");
|
||||
});
|
||||
|
||||
leftPane.add_section("Camera");
|
||||
addOption("Free Camera", getSettings().game.freeCamera,
|
||||
"Enables twin-stick camera control, letting the C-Stick move the camera vertically as "
|
||||
"well as horizontally.");
|
||||
addOption("Invert Camera X Axis", getSettings().game.invertCameraXAxis,
|
||||
"Invert horizontal camera movement.");
|
||||
addOption("Invert Camera Y Axis", getSettings().game.invertCameraYAxis,
|
||||
"Invert vertical camera movement when Free Camera is enabled.",
|
||||
[] { return !getSettings().game.freeCamera; });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.freeCameraSensitivity,
|
||||
"Free Camera Sensitivity", "Adjusts twin-stick camera sensitivity.", 50, 200, 5,
|
||||
[] { return !getSettings().game.freeCamera; });
|
||||
|
||||
leftPane.add_section("Gyro");
|
||||
addOption("Gyro Aim", getSettings().game.enableGyroAim,
|
||||
"Enables gyro aiming on supported controllers while in look mode and while aiming "
|
||||
"items.");
|
||||
addOption("Gyro Rollgoal", getSettings().game.enableGyroRollgoal,
|
||||
"Enables gyro controls for Rollgoal in Hena's Cabin.");
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityY,
|
||||
"Gyro Pitch Sensitivity", "Controls vertical gyro aiming sensitivity.", 25, 400, 5,
|
||||
[] { return !gyro_enabled(); });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityX,
|
||||
"Gyro Yaw Sensitivity", "Controls horizontal gyro aiming sensitivity.", 25, 400, 5,
|
||||
[] { return !gyro_enabled(); });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityRollgoal,
|
||||
"Rollgoal Sensitivity", "Controls how strongly gyro input tilts the Rollgoal table.",
|
||||
25, 400, 5, [] { return !getSettings().game.enableGyroRollgoal; });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroDeadband, "Gyro Deadband",
|
||||
"Ignores small gyro movement to reduce drift and jitter.", 0, 50, 1,
|
||||
[] { return !gyro_enabled(); });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroSmoothing,
|
||||
"Gyro Smoothing", "Higher values smooth gyro input over time.", 0, 100, 1,
|
||||
[] { return !gyro_enabled(); });
|
||||
addOption("Invert Gyro Pitch", getSettings().game.gyroInvertPitch,
|
||||
"Invert vertical gyro aiming.", [] { return !gyro_enabled(); });
|
||||
addOption("Invert Gyro Yaw", getSettings().game.gyroInvertYaw,
|
||||
"Invert horizontal gyro aiming.", [] { return !gyro_enabled(); });
|
||||
|
||||
leftPane.add_section("Tools");
|
||||
addOption("Turbo Key", getSettings().game.enableTurboKeybind,
|
||||
"Hold Tab to increase game speed by up to 4x.",
|
||||
[] { return getSettings().game.speedrunMode; });
|
||||
});
|
||||
|
||||
add_tab("Graphics", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
leftPane.add_section("Display");
|
||||
|
||||
leftPane.add_button("Toggle Fullscreen").on_pressed([] {
|
||||
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
|
||||
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
||||
config::Save();
|
||||
});
|
||||
leftPane.add_button("Restore Default Window Size").on_pressed([] {
|
||||
getSettings().video.enableFullscreen.setValue(false);
|
||||
VISetWindowFullscreen(false);
|
||||
VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2);
|
||||
VICenterWindow();
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().video.enableVsync,
|
||||
{
|
||||
.key = "Enable VSync",
|
||||
.helpText = "Synchronizes the frame rate to your monitor's refresh rate.",
|
||||
.onChange = [](bool value) { aurora_enable_vsync(value); },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().video.lockAspectRatio,
|
||||
{
|
||||
.key = "Lock 4:3 Aspect Ratio",
|
||||
.helpText = "Lock the game's aspect ratio to the original.",
|
||||
.onChange =
|
||||
[](bool value) {
|
||||
AuroraSetViewportPolicy(
|
||||
value ? AURORA_VIEWPORT_FIT : AURORA_VIEWPORT_STRETCH);
|
||||
},
|
||||
});
|
||||
|
||||
leftPane.add_section("Resolution");
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Internal Resolution",
|
||||
.getValue =
|
||||
[] {
|
||||
return format_graphics_setting_value(GraphicsOption::InternalResolution,
|
||||
getSettings().game.internalResolutionScale.getValue());
|
||||
},
|
||||
})
|
||||
.on_nav_command([](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
push_document(std::make_unique<Overlay>(OverlayProps{
|
||||
.option = GraphicsOption::InternalResolution,
|
||||
.title = "Internal Resolution",
|
||||
.helpText = kInternalResolutionHelpText,
|
||||
.valueMin = 0,
|
||||
.valueMax = 12,
|
||||
.defaultValue = 0,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(kInternalResolutionHelpText);
|
||||
});
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Shadow Resolution",
|
||||
.getValue =
|
||||
[] {
|
||||
return format_graphics_setting_value(GraphicsOption::ShadowResolution,
|
||||
getSettings().game.shadowResolutionMultiplier.getValue());
|
||||
},
|
||||
})
|
||||
.on_nav_command([](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
push_document(std::make_unique<Overlay>(OverlayProps{
|
||||
.option = GraphicsOption::ShadowResolution,
|
||||
.title = "Shadow Resolution",
|
||||
.helpText = kShadowResolutionHelpText,
|
||||
.valueMin = 1,
|
||||
.valueMax = 8,
|
||||
.defaultValue = 1,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(kShadowResolutionHelpText);
|
||||
});
|
||||
|
||||
leftPane.add_section("Post-Processing");
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Bloom",
|
||||
.getValue =
|
||||
[] {
|
||||
return format_graphics_setting_value(GraphicsOption::BloomMode,
|
||||
static_cast<int>(getSettings().game.bloomMode.getValue()));
|
||||
},
|
||||
})
|
||||
.on_nav_command([](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
push_document(std::make_unique<Overlay>(OverlayProps{
|
||||
.option = GraphicsOption::BloomMode,
|
||||
.title = "Bloom",
|
||||
.helpText = kBloomHelpText,
|
||||
.valueMin = static_cast<int>(BloomMode::Off),
|
||||
.valueMax = static_cast<int>(BloomMode::Dusk),
|
||||
.defaultValue = static_cast<int>(BloomMode::Classic),
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(kBloomHelpText);
|
||||
});
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Bloom Brightness",
|
||||
.getValue =
|
||||
[] {
|
||||
return format_graphics_setting_value(
|
||||
GraphicsOption::BloomMultiplier, bloom_multiplier_percent());
|
||||
},
|
||||
.isDisabled =
|
||||
[] { return getSettings().game.bloomMode.getValue() == BloomMode::Off; },
|
||||
})
|
||||
.on_nav_command([](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
push_document(std::make_unique<Overlay>(OverlayProps{
|
||||
.option = GraphicsOption::BloomMultiplier,
|
||||
.title = "Bloom Brightness",
|
||||
.helpText = kBloomBrightnessHelpText,
|
||||
.valueMin = 0,
|
||||
.valueMax = 100,
|
||||
.defaultValue = 100,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(kBloomBrightnessHelpText);
|
||||
});
|
||||
|
||||
leftPane.add_section("Rendering");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation,
|
||||
{
|
||||
.key = "Unlock Framerate",
|
||||
.helpText =
|
||||
"Uses inter-frame interpolation to enable higher frame rates.<br/><br/>Visual "
|
||||
"artifacts, animation glitches, or instability may occur.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableDepthOfField,
|
||||
{
|
||||
.key = "Enable Depth of Field",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground,
|
||||
{
|
||||
.key = "Enable Mini-Map Shadows",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SettingsWindow : public Window {
|
||||
public:
|
||||
SettingsWindow();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "string_button.hpp"
|
||||
|
||||
#include <aurora/rmlui.hpp>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
BaseStringButton::BaseStringButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton(parent, {std::move(props.key)}), mType(std::move(props.type)),
|
||||
mMaxLength(props.maxLength) {
|
||||
mInputListeners.reserve(3);
|
||||
}
|
||||
|
||||
void BaseStringButton::update() {
|
||||
if (mPendingStopEditing) {
|
||||
stop_editing(mPendingCommit, mPendingRefocusRoot);
|
||||
}
|
||||
if (mPendingInputFocusFrames > 0) {
|
||||
--mPendingInputFocusFrames;
|
||||
if (mPendingInputFocusFrames == 0) {
|
||||
focus_input();
|
||||
}
|
||||
}
|
||||
BaseControlledSelectButton::update();
|
||||
}
|
||||
|
||||
void BaseStringButton::start_editing() {
|
||||
if (mInputElem != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create input element
|
||||
auto* doc = mRoot->GetOwnerDocument();
|
||||
auto elemPtr = doc->CreateElement("input");
|
||||
mInputElem = rmlui_dynamic_cast<Rml::ElementFormControlInput*>(elemPtr.get());
|
||||
if (mInputElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
mInputElem->SetAttribute("type", mType);
|
||||
mInputElem->SetAttribute("value", input_value());
|
||||
if (mMaxLength > -1) {
|
||||
mInputElem->SetAttribute("maxlength", mMaxLength);
|
||||
}
|
||||
mRoot->AppendChild(std::move(elemPtr));
|
||||
|
||||
// Hide value element
|
||||
mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden);
|
||||
|
||||
// RmlUi lays out the new input during render. Wait one full frame before focusing it so
|
||||
// mobile keyboard placement gets a valid caret rectangle.
|
||||
mPendingInputFocusFrames = 2;
|
||||
|
||||
// Dispatch a submit event so the pane can handle item selection
|
||||
// However, mark it as "handled" to ensure that we don't steal focus away
|
||||
mRoot->DispatchEvent(Rml::EventId::Submit, {{"handled", Rml::Variant{true}}});
|
||||
|
||||
// Register input listeners
|
||||
mInputListeners.emplace_back(std::make_unique<ScopedEventListener>(
|
||||
mInputElem, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
request_stop_editing(true, true);
|
||||
event.StopImmediatePropagation();
|
||||
} else if (cmd == NavCommand::Cancel) {
|
||||
request_stop_editing(false, true);
|
||||
event.StopImmediatePropagation();
|
||||
}
|
||||
}));
|
||||
mInputListeners.emplace_back(std::make_unique<ScopedEventListener>(
|
||||
mInputElem, Rml::EventId::Click, [](Rml::Event& event) { event.StopPropagation(); }));
|
||||
mInputListeners.emplace_back(std::make_unique<ScopedEventListener>(mInputElem,
|
||||
Rml::EventId::Blur, [this](Rml::Event&) { request_stop_editing(true, false); }));
|
||||
}
|
||||
|
||||
void BaseStringButton::request_stop_editing(bool commit, bool refocusRoot) {
|
||||
mPendingStopEditing = true;
|
||||
mPendingCommit = commit;
|
||||
mPendingRefocusRoot = refocusRoot;
|
||||
}
|
||||
|
||||
bool BaseStringButton::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
if (mInputElem == nullptr) {
|
||||
start_editing();
|
||||
} else {
|
||||
request_stop_editing(true, true);
|
||||
}
|
||||
return true;
|
||||
} else if (cmd == NavCommand::Cancel) {
|
||||
if (mInputElem != nullptr) {
|
||||
request_stop_editing(false, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BaseStringButton::focus_input() {
|
||||
if (mInputElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
aurora::rmlui::set_input_type(
|
||||
mType == "number" ? aurora::rmlui::InputType::Number : aurora::rmlui::InputType::Text);
|
||||
|
||||
if (mInputElem->Focus(true)) {
|
||||
const int end = static_cast<int>(Rml::StringUtilities::LengthUTF8(mInputElem->GetValue()));
|
||||
mInputElem->SetSelectionRange(0, end);
|
||||
}
|
||||
}
|
||||
|
||||
void BaseStringButton::stop_editing(bool commit, bool refocusRoot) {
|
||||
mPendingStopEditing = false;
|
||||
mPendingInputFocusFrames = 0;
|
||||
if (mInputElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (commit) {
|
||||
set_value(mInputElem->GetValue());
|
||||
}
|
||||
mInputListeners.clear();
|
||||
mRoot->RemoveChild(mInputElem);
|
||||
mInputElem = nullptr;
|
||||
|
||||
// Restore value element
|
||||
mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible);
|
||||
|
||||
set_selected(false);
|
||||
if (refocusRoot) {
|
||||
mRoot->Focus(true);
|
||||
}
|
||||
}
|
||||
|
||||
StringButton::StringButton(Rml::Element* parent, Props props)
|
||||
: BaseStringButton(parent, {.key = std::move(props.key), .maxLength = props.maxLength}),
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)) {}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "select_button.hpp"
|
||||
|
||||
#include <RmlUi/Config/Config.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class BaseStringButton : public BaseControlledSelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
Rml::String type = "text";
|
||||
int maxLength = -1;
|
||||
};
|
||||
|
||||
BaseStringButton(Rml::Element* parent, Props props);
|
||||
void update() override;
|
||||
void start_editing();
|
||||
void request_stop_editing(bool commit, bool refocusRoot);
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(NavCommand cmd) override;
|
||||
virtual void set_value(Rml::String value) = 0;
|
||||
virtual Rml::String input_value() { return format_value(); }
|
||||
|
||||
private:
|
||||
void focus_input();
|
||||
void stop_editing(bool commit = true, bool refocusRoot = false);
|
||||
|
||||
Rml::ElementFormControlInput* mInputElem = nullptr;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mInputListeners;
|
||||
Rml::String mType;
|
||||
int mMaxLength;
|
||||
int mPendingInputFocusFrames = 0;
|
||||
bool mPendingStopEditing = false;
|
||||
bool mPendingCommit = true;
|
||||
bool mPendingRefocusRoot = false;
|
||||
};
|
||||
|
||||
class StringButton : public BaseStringButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<void(Rml::String)> setValue;
|
||||
int maxLength = -1;
|
||||
};
|
||||
|
||||
StringButton(Rml::Element* parent, Props props);
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override { return mGetValue(); }
|
||||
void set_value(Rml::String value) override {
|
||||
if (mSetValue) {
|
||||
mSetValue(std::move(value));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<Rml::String()> mGetValue;
|
||||
std::function<void(Rml::String)> mSetValue;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,128 @@
|
||||
#include "tab_bar.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement("tab-bar");
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TabBar::TabBar(Rml::Element* parent, Props props)
|
||||
: FluentComponent(createRoot(parent)), mProps(std::move(props)) {
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool TabBar::focus() {
|
||||
if (mProps.selectedTabIndex >= 0 && mProps.selectedTabIndex < mTabs.size()) {
|
||||
// Try to focus the currently selected tab
|
||||
if (mTabs[mProps.selectedTabIndex].button.focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Otherwise, focus the first enabled tab
|
||||
for (const auto& tab : mTabs) {
|
||||
if (tab.button.focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
|
||||
const int index = static_cast<int>(mTabs.size());
|
||||
const bool selected = index == mProps.selectedTabIndex;
|
||||
if (selected && callback) {
|
||||
callback();
|
||||
}
|
||||
auto& button = add_child<Button>(Button::Props{title}, "tab");
|
||||
button.on_pressed([this, index] { set_active_tab(index); });
|
||||
if (selected) {
|
||||
button.set_selected(true);
|
||||
}
|
||||
mTabs.emplace_back(Tab{
|
||||
.title = title,
|
||||
.button = button,
|
||||
.callback = std::move(callback),
|
||||
});
|
||||
}
|
||||
|
||||
bool TabBar::set_active_tab(int index) {
|
||||
if (index == -1) {
|
||||
// Clear currently selected tab
|
||||
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
|
||||
mTabs[i].button.set_selected(false);
|
||||
}
|
||||
mProps.selectedTabIndex = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= mTabs.size() || index == mProps.selectedTabIndex) {
|
||||
return false;
|
||||
}
|
||||
const auto& tab = mTabs[index];
|
||||
if (tab.button.focus()) {
|
||||
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
|
||||
mTabs[i].button.set_selected(i == index);
|
||||
}
|
||||
mProps.selectedTabIndex = index;
|
||||
if (tab.callback) {
|
||||
tab.callback();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TabBar::focus_tab(int index) {
|
||||
if (index < 0 || index >= mTabs.size() || index == mProps.selectedTabIndex) {
|
||||
return false;
|
||||
}
|
||||
return mTabs[index].button.focus();
|
||||
}
|
||||
|
||||
int TabBar::tab_containing(Rml::Element* element) const {
|
||||
for (int i = 0; i < mTabs.size(); ++i) {
|
||||
if (mTabs[i].button.contains(element)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool TabBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Left || cmd == NavCommand::Right || cmd == NavCommand::Next ||
|
||||
cmd == NavCommand::Previous)
|
||||
{
|
||||
bool isNext = cmd == NavCommand::Right || cmd == NavCommand::Next;
|
||||
int currentComponent = tab_containing(event.GetTargetElement());
|
||||
int direction = isNext ? 1 : -1;
|
||||
int i = currentComponent + direction;
|
||||
if (currentComponent == -1) {
|
||||
// If the container itself is focused and right is pressed, focus the first element
|
||||
if (isNext) {
|
||||
i = 0;
|
||||
} else {
|
||||
// Otherwise, allow event to bubble
|
||||
return false;
|
||||
}
|
||||
}
|
||||
while (i >= 0 && i < mTabs.size()) {
|
||||
if (mProps.autoSelect ? set_active_tab(i) : focus_tab(i)) {
|
||||
return true;
|
||||
}
|
||||
i += direction;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
using TabCallback = std::function<void()>;
|
||||
|
||||
struct Tab {
|
||||
Rml::String title;
|
||||
Button& button;
|
||||
TabCallback callback;
|
||||
};
|
||||
|
||||
class TabBar : public FluentComponent<TabBar> {
|
||||
public:
|
||||
struct Props {
|
||||
std::function<void()> onClose;
|
||||
int selectedTabIndex = -1;
|
||||
bool autoSelect = true;
|
||||
};
|
||||
|
||||
explicit TabBar(Rml::Element* parent, Props props);
|
||||
|
||||
bool focus() override;
|
||||
|
||||
void add_tab(const Rml::String& title, TabCallback callback);
|
||||
bool set_active_tab(int index);
|
||||
bool focus_tab(int index);
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd);
|
||||
|
||||
private:
|
||||
int tab_containing(Rml::Element* element) const;
|
||||
|
||||
Props mProps;
|
||||
std::vector<Tab> mTabs;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,217 @@
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <aurora/rmlui.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
|
||||
#include "aurora/lib/window.hpp"
|
||||
#include "input.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
void load_font(const char* filename, bool fallback = false) {
|
||||
Rml::LoadFontFace(resource_path(filename).string(), fallback);
|
||||
}
|
||||
|
||||
bool sInitialized = false;
|
||||
|
||||
struct OpenDocument {
|
||||
std::unique_ptr<Document> doc;
|
||||
bool pendingDestroy = false;
|
||||
};
|
||||
std::vector<OpenDocument> sDocuments;
|
||||
|
||||
} // namespace
|
||||
|
||||
bool initialize() noexcept {
|
||||
if (sInitialized) {
|
||||
return true;
|
||||
}
|
||||
if (!aurora::rmlui::is_initialized()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
load_font("FiraSans-Regular.ttf", true);
|
||||
load_font("FiraSansCondensed-Regular.ttf");
|
||||
load_font("FiraSansCondensed-Bold.ttf");
|
||||
load_font("AlegreyaSC-Regular.ttf");
|
||||
load_font("AlegreyaSC-Bold.ttf");
|
||||
|
||||
sInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown() noexcept {
|
||||
sDocuments.clear();
|
||||
reset_input_state();
|
||||
release_input_block();
|
||||
sInitialized = false;
|
||||
}
|
||||
|
||||
Document& push_document(std::unique_ptr<Document> doc, bool show) noexcept {
|
||||
if (auto* doc = top_document()) {
|
||||
doc->hide();
|
||||
}
|
||||
Document& ret = *doc;
|
||||
sDocuments.push_back({std::move(doc)});
|
||||
if (show) {
|
||||
ret.show();
|
||||
}
|
||||
sync_input_block();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void pop_document() noexcept {
|
||||
for (auto it = sDocuments.rbegin(); it != sDocuments.rend(); ++it) {
|
||||
if (!it->pendingDestroy) {
|
||||
it->doc->hide();
|
||||
it->pendingDestroy = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (auto* doc = top_document()) {
|
||||
doc->show();
|
||||
}
|
||||
sync_input_block();
|
||||
}
|
||||
|
||||
void toggle_top_document() noexcept {
|
||||
auto* doc = top_document();
|
||||
if (doc == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (doc->visible()) {
|
||||
doc->hide();
|
||||
} else {
|
||||
doc->show();
|
||||
}
|
||||
sync_input_block();
|
||||
}
|
||||
|
||||
bool any_document_visible() noexcept {
|
||||
return std::any_of(sDocuments.begin(), sDocuments.end(),
|
||||
[](const OpenDocument& doc) { return doc.doc != nullptr && doc.doc->visible(); });
|
||||
}
|
||||
|
||||
Document* top_document() noexcept {
|
||||
for (auto it = sDocuments.rbegin(); it != sDocuments.rend(); ++it) {
|
||||
if (!it->pendingDestroy) {
|
||||
return it->doc.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void update() noexcept {
|
||||
update_input();
|
||||
for (const auto& doc : sDocuments) {
|
||||
doc.doc->update();
|
||||
}
|
||||
sDocuments.erase(
|
||||
std::remove_if(sDocuments.begin(), sDocuments.end(),
|
||||
[](const OpenDocument& doc) { return doc.pendingDestroy && doc.doc->can_destroy(); }),
|
||||
sDocuments.end());
|
||||
sync_input_block();
|
||||
}
|
||||
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept {
|
||||
const char* basePath = SDL_GetBasePath();
|
||||
if (basePath == nullptr) {
|
||||
return std::filesystem::path("res") / filename;
|
||||
}
|
||||
return std::filesystem::path(basePath) / "res" / filename;
|
||||
}
|
||||
|
||||
std::string escape(std::string_view str) noexcept {
|
||||
std::string result;
|
||||
result.reserve(str.size());
|
||||
for (const char c : str) {
|
||||
switch (c) {
|
||||
case '&':
|
||||
result += "&";
|
||||
break;
|
||||
case '<':
|
||||
result += "<";
|
||||
break;
|
||||
case '>':
|
||||
result += ">";
|
||||
break;
|
||||
case '"':
|
||||
result += """;
|
||||
break;
|
||||
default:
|
||||
result += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
NavCommand map_nav_event(const Rml::Event& event) noexcept {
|
||||
const auto key = static_cast<Rml::Input::KeyIdentifier>(
|
||||
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
|
||||
switch (key) {
|
||||
case Rml::Input::KeyIdentifier::KI_UP:
|
||||
return NavCommand::Up;
|
||||
case Rml::Input::KeyIdentifier::KI_DOWN:
|
||||
return NavCommand::Down;
|
||||
case Rml::Input::KeyIdentifier::KI_LEFT:
|
||||
return NavCommand::Left;
|
||||
case Rml::Input::KeyIdentifier::KI_RIGHT:
|
||||
return NavCommand::Right;
|
||||
case Rml::Input::KeyIdentifier::KI_ESCAPE:
|
||||
return NavCommand::Cancel;
|
||||
case Rml::Input::KeyIdentifier::KI_RETURN:
|
||||
return NavCommand::Confirm;
|
||||
case Rml::Input::KeyIdentifier::KI_F1:
|
||||
return event.GetParameter<int>("shift_key", 0) ? NavCommand::None : NavCommand::Menu;
|
||||
case Rml::Input::KeyIdentifier::KI_NEXT:
|
||||
return NavCommand::Next;
|
||||
case Rml::Input::KeyIdentifier::KI_PRIOR:
|
||||
return NavCommand::Previous;
|
||||
default:
|
||||
return NavCommand::None;
|
||||
}
|
||||
}
|
||||
|
||||
Insets safe_area_insets(Rml::Context* context) noexcept {
|
||||
if (context == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto* window = aurora::window::get_sdl_window();
|
||||
if (window == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const AuroraWindowSize windowSize = aurora::window::get_window_size();
|
||||
if (windowSize.width == 0 || windowSize.height == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
SDL_Rect safeRect{};
|
||||
if (!SDL_GetWindowSafeArea(window, &safeRect)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const Rml::Vector2i contextSize = context->GetDimensions();
|
||||
const float scaleX = static_cast<float>(contextSize.x) / static_cast<float>(windowSize.width);
|
||||
const float scaleY = static_cast<float>(contextSize.y) / static_cast<float>(windowSize.height);
|
||||
|
||||
const float safeRight = static_cast<float>(safeRect.x + safeRect.w);
|
||||
const float safeBottom = static_cast<float>(safeRect.y + safeRect.h);
|
||||
return {
|
||||
.top = std::max(0.0f, static_cast<float>(safeRect.y)) * scaleY,
|
||||
.right = std::max(0.0f, static_cast<float>(windowSize.width) - safeRight) * scaleX,
|
||||
.bottom = std::max(0.0f, static_cast<float>(windowSize.height) - safeBottom) * scaleY,
|
||||
.left = std::max(0.0f, static_cast<float>(safeRect.x)) * scaleX,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "nav_types.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
class Document;
|
||||
class Popup;
|
||||
|
||||
struct Insets {
|
||||
float top = 0.0f;
|
||||
float right = 0.0f;
|
||||
float bottom = 0.0f;
|
||||
float left = 0.0f;
|
||||
|
||||
bool operator==(const Insets& other) const noexcept {
|
||||
return top == other.top && right == other.right && bottom == other.bottom &&
|
||||
left == other.left;
|
||||
}
|
||||
};
|
||||
|
||||
bool initialize() noexcept;
|
||||
void shutdown() noexcept;
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept;
|
||||
void update() noexcept;
|
||||
|
||||
Document& push_document(std::unique_ptr<Document> doc, bool show = true) noexcept;
|
||||
void pop_document() noexcept;
|
||||
void toggle_top_document() noexcept;
|
||||
bool any_document_visible() noexcept;
|
||||
Document* top_document() noexcept;
|
||||
|
||||
Popup& add_popup(std::unique_ptr<Popup> popup) noexcept;
|
||||
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept;
|
||||
std::string escape(std::string_view str) noexcept;
|
||||
|
||||
NavCommand map_nav_event(const Rml::Event& event) noexcept;
|
||||
Insets safe_area_insets(Rml::Context* context) noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,239 @@
|
||||
#include "window.hpp"
|
||||
|
||||
#include "aurora/lib/window.hpp"
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "magic_enum.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
float base_body_padding(Rml::Context* context) noexcept {
|
||||
if (context == nullptr) {
|
||||
return 64.0f;
|
||||
}
|
||||
const float dpRatio = std::max(context->GetDensityIndependentPixelRatio(), 0.001f);
|
||||
const float heightDp = static_cast<float>(context->GetDimensions().y) / dpRatio;
|
||||
if (heightDp <= 640.0f) {
|
||||
return 16.0f * dpRatio;
|
||||
}
|
||||
return 64.0f * dpRatio;
|
||||
}
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/tabbing.rcss" />
|
||||
<link type="text/rcss" href="res/rml/window.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<window id="window"></window>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
} // namespace
|
||||
|
||||
Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("window")) {
|
||||
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
|
||||
.selectedTabIndex = 0,
|
||||
.autoSelect = true,
|
||||
});
|
||||
|
||||
auto elem = mDocument->CreateElement("content");
|
||||
elem->SetAttribute("id", "content");
|
||||
mContentRoot = mRoot->AppendChild(std::move(elem));
|
||||
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
// 1-9 for quick switching tabs
|
||||
const auto key = static_cast<Rml::Input::KeyIdentifier>(
|
||||
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
|
||||
if (key >= Rml::Input::KeyIdentifier::KI_1 && key <= Rml::Input::KeyIdentifier::KI_9) {
|
||||
if (set_active_tab(key - Rml::Input::KeyIdentifier::KI_1)) {
|
||||
if (!mContentComponents.empty()) {
|
||||
mContentComponents.front()->focus();
|
||||
}
|
||||
event.StopPropagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hide document after transition completion
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot &&
|
||||
*mRoot->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible &&
|
||||
!mRoot->HasAttribute("open"))
|
||||
{
|
||||
Document::hide();
|
||||
}
|
||||
});
|
||||
|
||||
// If an item is selected in a pane, focus the next pane in the tree
|
||||
listen(mRoot, Rml::EventId::Submit, [this](Rml::Event& event) {
|
||||
int paneIndex = -1;
|
||||
for (int i = 0; i < mContentComponents.size(); i++) {
|
||||
if (mContentComponents[i]->contains(event.GetTargetElement())) {
|
||||
paneIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (paneIndex >= 0 && paneIndex < mContentComponents.size() - 1) {
|
||||
mContentComponents[paneIndex + 1]->focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Window::show() {
|
||||
Document::show();
|
||||
mRoot->SetAttribute("open", "");
|
||||
}
|
||||
|
||||
void Window::hide() {
|
||||
mRoot->RemoveAttribute("open");
|
||||
// Document will be hidden after transition
|
||||
}
|
||||
|
||||
void Window::update() {
|
||||
update_safe_area();
|
||||
for (const auto& component : mContentComponents) {
|
||||
component->update();
|
||||
}
|
||||
Document::update();
|
||||
}
|
||||
|
||||
void Window::update_safe_area() noexcept {
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rml::Context* context = mDocument->GetContext();
|
||||
const float basePadding = base_body_padding(context);
|
||||
Insets safeInsets = safe_area_insets(context);
|
||||
safeInsets = {
|
||||
std::round(std::max(basePadding, safeInsets.top)),
|
||||
std::round(std::max(basePadding, safeInsets.right)),
|
||||
std::round(std::max(basePadding, safeInsets.bottom)),
|
||||
std::round(std::max(basePadding, safeInsets.left)),
|
||||
};
|
||||
if (safeInsets == mBodyPadding) {
|
||||
return;
|
||||
}
|
||||
|
||||
mBodyPadding = safeInsets;
|
||||
mDocument->SetProperty(
|
||||
Rml::PropertyId::PaddingTop, Rml::Property(safeInsets.top, Rml::Unit::PX));
|
||||
mDocument->SetProperty(
|
||||
Rml::PropertyId::PaddingRight, Rml::Property(safeInsets.right, Rml::Unit::PX));
|
||||
mDocument->SetProperty(
|
||||
Rml::PropertyId::PaddingBottom, Rml::Property(safeInsets.bottom, Rml::Unit::PX));
|
||||
mDocument->SetProperty(
|
||||
Rml::PropertyId::PaddingLeft, Rml::Property(safeInsets.left, Rml::Unit::PX));
|
||||
}
|
||||
|
||||
bool Window::set_active_tab(int index) {
|
||||
return mTabBar->set_active_tab(index);
|
||||
}
|
||||
|
||||
void Window::add_tab(const Rml::String& title, TabBuilder builder) {
|
||||
mTabBar->add_tab(title, [this, builder = std::move(builder)] {
|
||||
clear_content();
|
||||
if (builder) {
|
||||
builder(mContentRoot);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Window::clear_content() noexcept {
|
||||
mContentComponents.clear();
|
||||
while (mContentRoot->GetNumChildren() != 0) {
|
||||
mContentRoot->RemoveChild(mContentRoot->GetFirstChild());
|
||||
}
|
||||
}
|
||||
|
||||
bool Window::focus() {
|
||||
return mTabBar->focus();
|
||||
}
|
||||
|
||||
bool Window::visible() const {
|
||||
return mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
auto* target = event.GetTargetElement();
|
||||
if (cmd != NavCommand::Next && cmd != NavCommand::Previous && target->Closest("content")) {
|
||||
if (handle_content_nav(event, cmd)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Down) {
|
||||
if (!mContentComponents.empty()) {
|
||||
return mContentComponents.front()->focus();
|
||||
}
|
||||
}
|
||||
if (cmd == NavCommand::Cancel) {
|
||||
pop_document();
|
||||
return true;
|
||||
}
|
||||
if (mTabBar->handle_nav_command(event, cmd)) {
|
||||
return true;
|
||||
}
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
if (cmd == NavCommand::Up) {
|
||||
return focus();
|
||||
} else if (cmd == NavCommand::Cancel) {
|
||||
int currentComponent = -1;
|
||||
for (int i = 0; i < mContentComponents.size(); ++i) {
|
||||
if (mContentComponents[i]->contains(event.GetTargetElement())) {
|
||||
currentComponent = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (; currentComponent > 0; --currentComponent) {
|
||||
if (mContentComponents[currentComponent - 1]->focus()) {
|
||||
// When returning to a previous pane, deselect the item after focusing
|
||||
if (auto* pane =
|
||||
dynamic_cast<Pane*>(mContentComponents[currentComponent - 1].get()))
|
||||
{
|
||||
pane->set_selected_item(-1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return focus();
|
||||
} else if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
int currentComponent = -1;
|
||||
for (int i = 0; i < mContentComponents.size(); ++i) {
|
||||
if (mContentComponents[i]->contains(event.GetTargetElement())) {
|
||||
currentComponent = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int direction = cmd == NavCommand::Right ? 1 : -1;
|
||||
int i = currentComponent + direction;
|
||||
if (currentComponent == -1) {
|
||||
if (cmd == NavCommand::Right) {
|
||||
return mContentComponents.front()->focus();
|
||||
}
|
||||
} else if (i >= 0 && i < mContentComponents.size()) {
|
||||
if (mContentComponents[i]->focus()) {
|
||||
if (direction == -1) {
|
||||
// When returning to a previous pane, deselect the item after focusing
|
||||
if (auto* pane = dynamic_cast<Pane*>(mContentComponents[i].get())) {
|
||||
pane->set_selected_item(-1);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "document.hpp"
|
||||
#include "nav_types.hpp"
|
||||
#include "tab_bar.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Window : public Document {
|
||||
public:
|
||||
using TabBuilder = std::function<void(Rml::Element*)>;
|
||||
struct Tab {
|
||||
Rml::String title;
|
||||
std::unique_ptr<Button> button;
|
||||
TabBuilder builder;
|
||||
};
|
||||
|
||||
Window();
|
||||
|
||||
Window(const Window&) = delete;
|
||||
Window& operator=(const Window&) = delete;
|
||||
|
||||
void show() override;
|
||||
void hide() override;
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
bool set_active_tab(int index);
|
||||
|
||||
protected:
|
||||
void add_tab(const Rml::String& title, TabBuilder builder);
|
||||
void update_safe_area() noexcept;
|
||||
void clear_content() noexcept;
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
bool handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
auto child = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mContentComponents.emplace_back(std::move(child));
|
||||
return ref;
|
||||
}
|
||||
|
||||
Rml::Element* mRoot;
|
||||
Rml::Element* mContentRoot;
|
||||
std::unique_ptr<TabBar> mTabBar;
|
||||
std::vector<std::unique_ptr<Component> > mContentComponents;
|
||||
Insets mBodyPadding;
|
||||
bool mVisible = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+20
-2
@@ -56,6 +56,11 @@
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "dusk/ui/editor.hpp"
|
||||
#include "dusk/ui/popup.hpp"
|
||||
#include "dusk/ui/prelaunch.hpp"
|
||||
#include "dusk/ui/settings.hpp"
|
||||
#include "version.h"
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
@@ -70,13 +75,13 @@
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/audio/DuskDsp.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/version.hpp"
|
||||
#include "dusk/discord_presence.hpp"
|
||||
#include "tracy/Tracy.hpp"
|
||||
#include "f_pc/f_pc_draw.h"
|
||||
#include "tracy/Tracy.hpp"
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
// --- GLOBALS ---
|
||||
s8 mDoMain::developmentMode = -1;
|
||||
@@ -140,6 +145,7 @@ bool launchUILoop() {
|
||||
while (event != nullptr && event->type != AURORA_NONE) {
|
||||
switch (event->type) {
|
||||
case AURORA_SDL_EVENT:
|
||||
dusk::ui::handle_event(event->sdl);
|
||||
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
|
||||
break;
|
||||
case AURORA_DISPLAY_SCALE_CHANGED:
|
||||
@@ -157,8 +163,9 @@ bool launchUILoop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
dusk::g_imguiConsole.PreDraw();
|
||||
dusk::ui::update();
|
||||
|
||||
dusk::g_imguiConsole.PreDraw();
|
||||
dusk::g_imguiConsole.PostDraw();
|
||||
|
||||
aurora_end_frame();
|
||||
@@ -215,6 +222,7 @@ void main01(void) {
|
||||
case AURORA_NONE:
|
||||
goto eventsDone;
|
||||
case AURORA_SDL_EVENT:
|
||||
dusk::ui::handle_event(event->sdl);
|
||||
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
|
||||
if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_LOST &&
|
||||
dusk::getSettings().game.pauseOnFocusLost) {
|
||||
@@ -254,6 +262,8 @@ void main01(void) {
|
||||
|
||||
mDoGph_gInf_c::updateRenderSize();
|
||||
|
||||
dusk::ui::update();
|
||||
|
||||
const auto pacing = dusk::game_clock::advance_main_loop();
|
||||
if (pacing.is_interpolating) {
|
||||
if (pacing.sim_ticks_to_run > 0) {
|
||||
@@ -305,6 +315,7 @@ void main01(void) {
|
||||
} while (dusk::IsRunning);
|
||||
|
||||
exit:;
|
||||
dusk::ui::shutdown();
|
||||
}
|
||||
|
||||
static bool IsBackendAvailable(AuroraBackend backend) {
|
||||
@@ -580,6 +591,8 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb);
|
||||
dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf;
|
||||
|
||||
dusk::ui::initialize();
|
||||
|
||||
std::string dvd_path;
|
||||
bool dvd_opened = false;
|
||||
if (parsed_arg_options.count("dvd")) {
|
||||
@@ -596,6 +609,8 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
if (!dvd_opened) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
|
||||
|
||||
// pre game launch ui main loop
|
||||
if (!launchUILoop()) {
|
||||
dusk::ShutdownCrashReporting();
|
||||
@@ -617,6 +632,8 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Popup>(), false);
|
||||
|
||||
dusk::version::init();
|
||||
LanguageInit();
|
||||
|
||||
@@ -657,6 +674,7 @@ int game_main(int argc, char* argv[]) {
|
||||
#ifdef DUSK_DISCORD_RPC
|
||||
dusk::discord::Shutdown();
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
aurora_shutdown();
|
||||
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user