mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-10-22 17:22:06 +00:00
feat(versionsview): almost done rewriting
This commit is contained in:
parent
b0f8e3cf86
commit
ba17a2a001
|
|
@ -51,28 +51,22 @@
|
|||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.gear-btn-open {
|
||||
background-image: /images/checkbox_highlighted.png;
|
||||
}
|
||||
.download-percent {
|
||||
font-size: 12px;
|
||||
color: #ffff55;
|
||||
text-shadow: #000000 1px 1px;
|
||||
}
|
||||
.play-btn {
|
||||
width: 96px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
border-image: url(/images/Button_Background.png) 13 13 13 13 fill / 13px;
|
||||
.desc-bg {
|
||||
border-image: url(/images/helpbg.png) 7 16 16 7 fill / 7px 16px 16px 7px;
|
||||
border-width: 12px;
|
||||
}
|
||||
.play-btn:hover {
|
||||
border-image: url(/images/button_highlighted.png) 13 13 13 13 fill / 13px;
|
||||
color: #ffff55;
|
||||
}
|
||||
.stop-btn { color: #ff5555; }
|
||||
.game-error {
|
||||
color: #ff5555;
|
||||
font-size: 16px;
|
||||
text-shadow: #000000 1px 1px;
|
||||
.desc-label {
|
||||
font-size: 20px;
|
||||
color: #ffffff;
|
||||
text-shadow: #3f3f3f 1px 1px;
|
||||
}
|
||||
.backdrop { background: rgba(0, 0, 0, 0.8); }
|
||||
.modal-frame {
|
||||
|
|
@ -93,20 +87,57 @@
|
|||
border-image: url(/images/button_highlighted.png) 13 13 13 13 fill / 13px;
|
||||
color: #ffff55;
|
||||
}
|
||||
.log-text {
|
||||
width: 760px;
|
||||
height: 360px;
|
||||
font-size: 14px;
|
||||
color: #e6f2e6;
|
||||
.modal-btn-danger { color: #ff5555; }
|
||||
.modal-btn-danger:hover { color: #ff8080; }
|
||||
.gear-menu-shell {
|
||||
background: #4b4b4b;
|
||||
}
|
||||
.gear-menu {
|
||||
background: #1b1b1b;
|
||||
padding: 6px;
|
||||
gap: 2px;
|
||||
}
|
||||
.gear-menu-title {
|
||||
font-size: 16px;
|
||||
color: #ffff55;
|
||||
text-shadow: #000000 1px 1px;
|
||||
}
|
||||
.gear-menu-sep {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #3a3a3a;
|
||||
}
|
||||
.gear-menu-item {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
gap: 10px;
|
||||
color: #dddddd;
|
||||
text-shadow: #000000 1px 1px;
|
||||
}
|
||||
.gear-menu-item:hover {
|
||||
background: #3c3c3c;
|
||||
color: #ffff55;
|
||||
}
|
||||
.gear-menu-item-danger { color: #ff6b6b; }
|
||||
.gear-menu-item-danger:hover {
|
||||
background: #4a2222;
|
||||
color: #ff9d9d;
|
||||
}
|
||||
.gear-menu-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.gear-menu-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<template name="EditionRow">
|
||||
<row class="edition-row {{rowClass}}" onclick="versions.select:{{id}}" align="stretch">
|
||||
<row id="edition-{{id}}" class="edition-row {{rowClass}}" onclick="versions.select:{{id}}" align="stretch">
|
||||
<image class="edition-logo" src="{{logo}}" fit="contain" align="center" />
|
||||
<column gap="4" align="center" flex="1">
|
||||
<row gap="8" align="start">
|
||||
<label class="edition-name" text="{{name}}" />
|
||||
<label if="{{isCustom}}" class="badge-custom" text="{{t:versions.custom}}" />
|
||||
</row>
|
||||
</column>
|
||||
<row gap="8" align="center">
|
||||
|
|
@ -117,8 +148,9 @@
|
|||
<button if="{{downloading}}" class="action-btn" onclick="versions.cancel:{{id}}">
|
||||
<image class="action-icon" src="/images/Trash_Bin_Icon.png" />
|
||||
</button>
|
||||
<button class="action-btn" onclick="versions.settings:{{id}}">
|
||||
<image class="action-icon" src="/images/gear.png" />
|
||||
<button class="action-btn {{gearClass}}" onclick="versions.settings:{{id}}">
|
||||
<image if="{{updateAvailable}}" class="action-icon" src="/images/Update_Icon.png" />
|
||||
<image if="{{showGear}}" class="action-icon" src="/images/gear.png" />
|
||||
</button>
|
||||
</row>
|
||||
</row>
|
||||
|
|
@ -128,15 +160,66 @@
|
|||
<tooltip icon="B">Back</tooltip>
|
||||
<tooltip icon="X">Install</tooltip>
|
||||
<column width="1280px" height="720px" align="center" padding="40">
|
||||
<use template="TitleImage"/>
|
||||
<column height="40px"/>
|
||||
<use template="TitleImage"/>
|
||||
<column height="20px"/>
|
||||
<column class="options-bg" padding="4" width="1024px" height="372px" align="center">
|
||||
<scroll class="version-recess" width="100%" height="100%" id="edition-list">
|
||||
<scroll class="version-recess" width="100%" height="100%" id="edition-list" hide-partial="true">
|
||||
<column gap="4" width="97.9%" foreach="{{editions}}" as="edition">
|
||||
<use template="EditionRow" id="{{edition.id}}" name="{{edition.name}}" logo="{{edition.logo}}" notInstalled="{{edition.notInstalled}}" showInstall="{{edition.showInstall}}" showPlay="{{edition.showPlay}}" showStop="{{edition.showStop}}" downloading="{{edition.downloading}}" downloadPercentText="{{edition.downloadPercentText}}" rowClass="{{edition.rowClass}}" />
|
||||
<use template="EditionRow"
|
||||
id="{{edition.id}}"
|
||||
name="{{edition.name}}"
|
||||
logo="{{edition.logo}}"
|
||||
isCustom="{{edition.isCustom}}"
|
||||
notInstalled="{{edition.notInstalled}}"
|
||||
showInstall="{{edition.showInstall}}"
|
||||
showGear="{{edition.showGear}}"
|
||||
updateAvailable="{{edition.updateAvailable}}"
|
||||
downloading="{{edition.downloading}}"
|
||||
downloadPercentText="{{edition.downloadPercentText}}"
|
||||
rowClass="{{edition.rowClass}}"
|
||||
gearClass="{{edition.gearClass}}" />
|
||||
</column>
|
||||
</scroll>
|
||||
</column>
|
||||
<column height="8px"/>
|
||||
<column class="desc-bg" width="1024px" height="60px" align="center">
|
||||
<label class="desc-label" text="{{selectedDesc}}" />
|
||||
</column>
|
||||
<column class="backdrop" if="{{settingsMenu}}" style="position:absolute;left:0px;top:0px;width:1280px;height:720px" onclick="versions.menu.close">
|
||||
<column class="gear-menu-shell" style="position:absolute;left:841px;top:{{menuTop}}px;width:250px" padding="2px">
|
||||
<column id="gear-menu" class="gear-menu" gap="2px">
|
||||
<label class="gear-menu-title" text="{{menuTitle}}" />
|
||||
<column class="gear-menu-sep" />
|
||||
<column foreach="{{menuItems}}" as="item" gap="2px">
|
||||
<row class="gear-menu-item {{item.itemClass}}" onclick="{{item.action}}" gap="10px">
|
||||
<image class="gear-menu-icon" src="{{item.icon}}" fit="contain" align="center" />
|
||||
<label class="gear-menu-label" text="{{item.label}}" align="center" />
|
||||
</row>
|
||||
</column>
|
||||
<column class="gear-menu-sep" />
|
||||
<column foreach="{{menuDangerItems}}" as="item" gap="2px">
|
||||
<row class="gear-menu-item {{item.itemClass}}" onclick="{{item.action}}" gap="10px">
|
||||
<image class="gear-menu-icon" src="{{item.icon}}" fit="contain" align="center" />
|
||||
<label class="gear-menu-label" text="{{item.label}}" align="center" />
|
||||
</row>
|
||||
</column>
|
||||
</column>
|
||||
</column>
|
||||
</column>
|
||||
<column class="backdrop" if="{{deleteModal}}" style="position:absolute;left:0px;top:0px;width:1280px;height:720px">
|
||||
<column id="delete-modal" class="modal-frame" style="position:absolute;left:440px;top:220px;width:400px" align="center" gap="8">
|
||||
<label class="modal-title" text="{{deleteTitle}}" />
|
||||
<label text="{{t:versions.deleteWarning}}" />
|
||||
<row align="center" gap="20">
|
||||
<button class="modal-btn" onclick="versions.delete.cancel">
|
||||
<label text="{{t:common.cancel}}"/>
|
||||
</button>
|
||||
<button class="modal-btn modal-btn-danger" onclick="versions.delete.confirm">
|
||||
<label text="{{t:common.delete}}"/>
|
||||
</button>
|
||||
</row>
|
||||
</column>
|
||||
</column>
|
||||
</column>
|
||||
</view>
|
||||
</ui>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
"unknown": "Unknown",
|
||||
"all": "All",
|
||||
"noResults": "No results",
|
||||
"set": "set"
|
||||
"set": "set",
|
||||
"close": "Close"
|
||||
},
|
||||
"header": {
|
||||
"title": "Emerald Legacy Launcher"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
"unknown": "Inconnu",
|
||||
"all": "Tous",
|
||||
"noResults": "Aucun résultat",
|
||||
"set": "placer"
|
||||
"set": "placer",
|
||||
"close": "Fermer"
|
||||
},
|
||||
"header": {
|
||||
"title": "Lanceur Emerald Legacy"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
"unknown": "Неизвестно",
|
||||
"all": "Все",
|
||||
"noResults": "Без результатов",
|
||||
"set": "Поставить"
|
||||
"set": "Поставить",
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"header": {
|
||||
"title": "Emerald Legacy Launcher"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <archive_entry.h>
|
||||
#include <string_view>
|
||||
#include <sys/stat.h>
|
||||
#include <vector>
|
||||
namespace emerald::instances {
|
||||
namespace {
|
||||
bool safe_path_within(const std::filesystem::path &base,
|
||||
|
|
@ -74,6 +75,29 @@ bool is_workshop_path(const std::filesystem::path &child,
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool move_entry(const std::filesystem::path &source,
|
||||
const std::filesystem::path &target) {
|
||||
std::error_code error;
|
||||
std::filesystem::rename(source, target, error);
|
||||
if (!error) {
|
||||
return true;
|
||||
}
|
||||
error.clear();
|
||||
if (std::filesystem::is_directory(source, error)) {
|
||||
error.clear();
|
||||
if (!core::copy_directory(source, target)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!core::copy_file(source, target)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::error_code remove_error;
|
||||
std::filesystem::remove_all(source, remove_error);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool extract_archive(const std::filesystem::path &archive,
|
||||
|
|
@ -181,23 +205,50 @@ void flatten_instance(const std::filesystem::path &instance_dir) {
|
|||
if (error) {
|
||||
return;
|
||||
}
|
||||
std::filesystem::path only_child;
|
||||
std::size_t count = 0;
|
||||
|
||||
std::filesystem::path inner_dir;
|
||||
for (const std::filesystem::directory_entry &entry : it) {
|
||||
++count;
|
||||
only_child = entry.path();
|
||||
const std::filesystem::path &child = entry.path();
|
||||
if (!std::filesystem::is_directory(child, error)) {
|
||||
continue;
|
||||
}
|
||||
error.clear();
|
||||
if (!std::filesystem::exists(child / std::string(kExecutableMarker),
|
||||
error)) {
|
||||
continue;
|
||||
}
|
||||
inner_dir = child;
|
||||
break;
|
||||
}
|
||||
if (count != 1 || !std::filesystem::is_directory(only_child, error)) {
|
||||
if (inner_dir.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code move_error;
|
||||
std::vector<std::filesystem::path> inner_entries;
|
||||
for (const std::filesystem::directory_entry &entry :
|
||||
std::filesystem::directory_iterator(only_child, move_error)) {
|
||||
const std::filesystem::path target = instance_dir / entry.path().filename();
|
||||
std::filesystem::rename(entry.path(), target, move_error);
|
||||
move_error.clear();
|
||||
std::filesystem::directory_iterator(inner_dir, move_error)) {
|
||||
inner_entries.push_back(entry.path());
|
||||
}
|
||||
remove_all(only_child, move_error);
|
||||
move_error.clear();
|
||||
|
||||
bool complete = true;
|
||||
for (const std::filesystem::path &source : inner_entries) {
|
||||
const std::filesystem::path target = instance_dir / source.filename();
|
||||
if (names_match_keep(source, keep_list()) &&
|
||||
std::filesystem::exists(target, move_error)) {
|
||||
move_error.clear();
|
||||
continue;
|
||||
}
|
||||
if (!move_entry(source, target)) {
|
||||
complete = false;
|
||||
}
|
||||
}
|
||||
if (!complete) {
|
||||
return;
|
||||
}
|
||||
std::error_code remove_error;
|
||||
std::filesystem::remove_all(inner_dir, remove_error);
|
||||
}
|
||||
|
||||
DownloadStatus install_instance_archive(
|
||||
|
|
|
|||
|
|
@ -22,6 +22,47 @@ void arrange_absolute(const ElementPtr &child, const Rect &container) {
|
|||
style.height.value_or(child->measured().height)});
|
||||
}
|
||||
|
||||
bool fully_inside(const Rect &bounds, const Rect &view) {
|
||||
return bounds.y >= view.y - 0.5F &&
|
||||
bounds.y + bounds.height <= view.y + view.height + 0.5F;
|
||||
}
|
||||
|
||||
bool outside_view(const Rect &bounds, const Rect &view) {
|
||||
return bounds.y + bounds.height <= view.y + 0.5F ||
|
||||
bounds.y >= view.y + view.height - 0.5F;
|
||||
}
|
||||
|
||||
bool spans_view(const Rect &bounds, const Rect &view) {
|
||||
return bounds.y <= view.y + 0.5F &&
|
||||
bounds.y + bounds.height >= view.y + view.height - 0.5F;
|
||||
}
|
||||
|
||||
void render_visible_children(const Element *parent, RenderContext &context,
|
||||
const TextStyle &parent_text, const Rect &view) {
|
||||
const TextStyle child_text =
|
||||
resolve_text_style(parent->visual_style(), parent_text);
|
||||
for (const ElementPtr &child : parent->children()) {
|
||||
if (!child->style().visible) {
|
||||
continue;
|
||||
}
|
||||
if (child->style().absolute) {
|
||||
child->render(context, child_text);
|
||||
continue;
|
||||
}
|
||||
const Rect &bounds = child->bounds();
|
||||
if (outside_view(bounds, view)) {
|
||||
continue;
|
||||
}
|
||||
if (fully_inside(bounds, view)) {
|
||||
child->render(context, child_text);
|
||||
continue;
|
||||
}
|
||||
if (spans_view(bounds, view)) {
|
||||
render_visible_children(child.get(), context, child_text, view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void render_box(RenderContext &context, const Element &element) {
|
||||
const ComputedStyle &style = element.visual_style();
|
||||
const float alpha = style.opacity;
|
||||
|
|
@ -284,8 +325,8 @@ public:
|
|||
sizing.letterSpacing = resolved.letterSpacing;
|
||||
sizing.bold = resolved.bold;
|
||||
const Size natural = context.text_size(text_, sizing);
|
||||
line_height_ = resolved.lineHeight > 0.0F ? resolved.lineHeight
|
||||
: natural.height;
|
||||
line_height_ =
|
||||
resolved.lineHeight > 0.0F ? resolved.lineHeight : natural.height;
|
||||
leading_ = std::max(0.0F, (line_height_ - natural.height) * 0.5F);
|
||||
text_align_ = resolved.textAlign;
|
||||
lines_.clear();
|
||||
|
|
@ -332,8 +373,8 @@ public:
|
|||
} else if (text_align_ == Align::End) {
|
||||
x = bounds_.x + bounds_.width - line_width;
|
||||
}
|
||||
const float y = bounds_.y + line_height_ * static_cast<float>(i) +
|
||||
leading_;
|
||||
const float y =
|
||||
bounds_.y + line_height_ * static_cast<float>(i) + leading_;
|
||||
context.draw_text(line, Rect{x, y, line_width, line_height_}, effective);
|
||||
}
|
||||
}
|
||||
|
|
@ -433,6 +474,7 @@ public:
|
|||
autoscroll_duration_ = 0.0F;
|
||||
autoscroll_from_ = 50.0F;
|
||||
autoscroll_to_ = -200.0F;
|
||||
hide_partial_ = attribute("hide-partial") == "true";
|
||||
if (const std::string &auto_scroll = attribute("autoscroll");
|
||||
!auto_scroll.empty()) {
|
||||
autoscroll_duration_ = std::atof(auto_scroll.c_str());
|
||||
|
|
@ -515,13 +557,17 @@ public:
|
|||
}
|
||||
render_box(context, *this);
|
||||
const float in = inset();
|
||||
context.push_clip(Rect{bounds_.x + in, bounds_.y + in,
|
||||
bounds_.width - in * 2.0F,
|
||||
bounds_.height - in * 2.0F});
|
||||
const Rect view{bounds_.x + in, bounds_.y + in, bounds_.width - in * 2.0F,
|
||||
bounds_.height - in * 2.0F};
|
||||
context.push_clip(view);
|
||||
const TextStyle childText = resolve_text_style(visual_style(), text);
|
||||
for (const ElementPtr &child : children_) {
|
||||
if (child->style().visible) {
|
||||
child->render(context, childText);
|
||||
if (hide_partial_) {
|
||||
render_visible_children(this, context, childText, view);
|
||||
} else {
|
||||
for (const ElementPtr &child : children_) {
|
||||
if (child->style().visible) {
|
||||
child->render(context, childText);
|
||||
}
|
||||
}
|
||||
}
|
||||
context.pop_clip();
|
||||
|
|
@ -538,6 +584,13 @@ public:
|
|||
arrange(bounds_);
|
||||
}
|
||||
|
||||
bool hides_partial_items() const override { return hide_partial_; }
|
||||
Rect scroll_viewport() const override {
|
||||
const float in = inset();
|
||||
return Rect{bounds_.x + in, bounds_.y + in, bounds_.width - in * 2.0F,
|
||||
bounds_.height - in * 2.0F};
|
||||
}
|
||||
|
||||
Element *scroll_hit_test(const Vec2 &point) {
|
||||
return bounds_.contains(point) ? this : nullptr;
|
||||
}
|
||||
|
|
@ -574,21 +627,20 @@ public:
|
|||
if (autoscroll_duration_ <= 0.0F) {
|
||||
return active;
|
||||
}
|
||||
const float view_height =
|
||||
bounds_.height - inset() * 2.0F;
|
||||
const float view_height = bounds_.height - inset() * 2.0F;
|
||||
const uint64_t duration_ms =
|
||||
static_cast<uint64_t>(autoscroll_duration_ * 1000.0F);
|
||||
const uint64_t elapsed =
|
||||
now_ticks >= start_ticks_ ? now_ticks - start_ticks_ : 0U;
|
||||
const float progress = duration_ms > 0U
|
||||
? std::min(1.0F, static_cast<float>(elapsed) /
|
||||
static_cast<float>(duration_ms))
|
||||
: 1.0F;
|
||||
const float progress =
|
||||
duration_ms > 0U ? std::min(1.0F, static_cast<float>(elapsed) /
|
||||
static_cast<float>(duration_ms))
|
||||
: 1.0F;
|
||||
const float fraction_percent =
|
||||
autoscroll_from_ + (autoscroll_to_ - autoscroll_from_) * progress;
|
||||
const float fraction = fraction_percent * 0.01F;
|
||||
scroll_offset_ = (content_height_ - view_height) * 0.5F -
|
||||
fraction * content_height_;
|
||||
scroll_offset_ =
|
||||
(content_height_ - view_height) * 0.5F - fraction * content_height_;
|
||||
return elapsed < duration_ms;
|
||||
}
|
||||
|
||||
|
|
@ -599,15 +651,12 @@ private:
|
|||
float autoscroll_duration_ = 0.0F;
|
||||
float autoscroll_from_ = 50.0F;
|
||||
float autoscroll_to_ = -200.0F;
|
||||
bool hide_partial_ = false;
|
||||
uint64_t start_ticks_ = 0U;
|
||||
};
|
||||
} // namespace
|
||||
TextInputElement::TextInputElement() {
|
||||
set_interactive(true);
|
||||
}
|
||||
SliderElement::SliderElement() {
|
||||
set_interactive(true);
|
||||
}
|
||||
TextInputElement::TextInputElement() { set_interactive(true); }
|
||||
SliderElement::SliderElement() { set_interactive(true); }
|
||||
void TextInputElement::end_edit(bool commit) {
|
||||
if (!editing_) {
|
||||
return;
|
||||
|
|
@ -663,9 +712,7 @@ void TextInputElement::measure(const MeasureContext &context,
|
|||
measured_.width = clamped_width(content_width);
|
||||
measured_.height = clamped_height(content_height);
|
||||
}
|
||||
void TextInputElement::arrange(const Rect &rect) {
|
||||
bounds_ = rect;
|
||||
}
|
||||
void TextInputElement::arrange(const Rect &rect) { bounds_ = rect; }
|
||||
void TextInputElement::render(RenderContext &context,
|
||||
const TextStyle &inherited) const {
|
||||
if (!style_.visible) {
|
||||
|
|
@ -704,7 +751,8 @@ void TextInputElement::render(RenderContext &context,
|
|||
}
|
||||
const float line_height = static_cast<float>(resolved.fontSize) + 4.0F;
|
||||
std::string_view remaining = display;
|
||||
float line_y = box.y + (box.height - std::min(line_height, box.height)) * 0.5F;
|
||||
float line_y =
|
||||
box.y + (box.height - std::min(line_height, box.height)) * 0.5F;
|
||||
if (multiline()) {
|
||||
line_y = box.y + box.height - line_height;
|
||||
}
|
||||
|
|
@ -712,9 +760,9 @@ void TextInputElement::render(RenderContext &context,
|
|||
const std::size_t nl = remaining.find('\n');
|
||||
const std::string_view line =
|
||||
nl == std::string_view::npos ? remaining : remaining.substr(0, nl);
|
||||
context.draw_text(std::string(line),
|
||||
Rect{box.x + 8.0F, line_y, box.width - 16.0F, line_height},
|
||||
text_style);
|
||||
context.draw_text(
|
||||
std::string(line),
|
||||
Rect{box.x + 8.0F, line_y, box.width - 16.0F, line_height}, text_style);
|
||||
if (nl == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -741,16 +789,14 @@ void SliderElement::measure(const MeasureContext &context,
|
|||
measured_.width = clamped_width(style_.width.value_or(600.0F));
|
||||
measured_.height = clamped_height(style_.height.value_or(40.0F));
|
||||
}
|
||||
void SliderElement::arrange(const Rect &rect) {
|
||||
bounds_ = rect;
|
||||
}
|
||||
void SliderElement::arrange(const Rect &rect) { bounds_ = rect; }
|
||||
void SliderElement::adjust(int delta) {
|
||||
value_ = std::clamp(value_ + delta, 0, 100);
|
||||
}
|
||||
void SliderElement::click_at(float x) {
|
||||
const float width = std::max(1.0F, bounds_.width);
|
||||
value_ = std::clamp(
|
||||
static_cast<int>((x - bounds_.x) / width * 100.0F), 0, 100);
|
||||
value_ =
|
||||
std::clamp(static_cast<int>((x - bounds_.x) / width * 100.0F), 0, 100);
|
||||
}
|
||||
void SliderElement::render(RenderContext &context,
|
||||
const TextStyle &inherited) const {
|
||||
|
|
@ -761,10 +807,10 @@ void SliderElement::render(RenderContext &context,
|
|||
const float handle_width = 20.0F;
|
||||
const float fraction =
|
||||
static_cast<float>(value_) / 100.0F * (bounds_.width - handle_width);
|
||||
context.draw_image("/images/Slider_Handle.png",
|
||||
Rect{bounds_.x + fraction, bounds_.y, handle_width,
|
||||
bounds_.height},
|
||||
style_.opacity);
|
||||
context.draw_image(
|
||||
"/images/Slider_Handle.png",
|
||||
Rect{bounds_.x + fraction, bounds_.y, handle_width, bounds_.height},
|
||||
style_.opacity);
|
||||
const TextStyle resolved = resolve_text_style(visual_style(), inherited);
|
||||
ComputedStyle label_style = visual_style();
|
||||
label_style.fontFamily = resolved.fontFamily;
|
||||
|
|
@ -772,12 +818,11 @@ void SliderElement::render(RenderContext &context,
|
|||
label_style.color = resolved.color;
|
||||
label_style.textShadow = resolved.textShadow;
|
||||
if (label_width_ > 0.0F) {
|
||||
context.draw_text(
|
||||
attribute("label"),
|
||||
Rect{bounds_.x + (bounds_.width - label_width_) * 0.5F,
|
||||
bounds_.y + (bounds_.height - label_height_) * 0.5F, label_width_,
|
||||
label_height_},
|
||||
label_style);
|
||||
context.draw_text(attribute("label"),
|
||||
Rect{bounds_.x + (bounds_.width - label_width_) * 0.5F,
|
||||
bounds_.y + (bounds_.height - label_height_) * 0.5F,
|
||||
label_width_, label_height_},
|
||||
label_style);
|
||||
}
|
||||
}
|
||||
Element::Element() = default;
|
||||
|
|
@ -864,6 +909,28 @@ Element *Element::scroll_hit_test(const Vec2 &point) {
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
bool Element::hidden_by_scroll() const {
|
||||
for (const Element *ancestor = parent_; ancestor != nullptr;
|
||||
ancestor = ancestor->parent_) {
|
||||
if (!ancestor->hides_partial_items()) {
|
||||
continue;
|
||||
}
|
||||
const Rect view = ancestor->scroll_viewport();
|
||||
for (const Element *current = this; current != ancestor;
|
||||
current = current->parent_) {
|
||||
const Rect &bounds = current->bounds();
|
||||
if (outside_view(bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
if (!fully_inside(bounds, view) && !spans_view(bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Element *Element::find_by_id(const std::string &id) {
|
||||
if (id_ == id) {
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ public:
|
|||
bool has_click_handler() const { return !click_handler_.empty(); }
|
||||
bool interactive() const { return interactive_; }
|
||||
void set_interactive(bool value) { interactive_ = value; }
|
||||
virtual bool hides_partial_items() const { return false; }
|
||||
virtual Rect scroll_viewport() const { return bounds_; }
|
||||
bool hidden_by_scroll() const;
|
||||
void set_hovered(bool h) { hovered_ = h; }
|
||||
bool is_hovered() const { return hovered_; }
|
||||
void set_hover_style(ComputedStyle s) { hover_style_ = std::move(s); }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "Ui/Views/SettingsView.hpp"
|
||||
#include "Ui/Views/VersionsView.hpp"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
namespace emerald::ui {
|
||||
namespace {
|
||||
ControllerType controller_type_from_sdl(SDL_GamepadType type) {
|
||||
|
|
@ -31,7 +32,8 @@ ControllerType controller_type_from_sdl(SDL_GamepadType type) {
|
|||
}
|
||||
}
|
||||
|
||||
std::string tooltip_set_for(ControllerType type) {
|
||||
std::string tooltip_set_for(
|
||||
ControllerType type) { // neo: TODO: update controller tooltip types
|
||||
switch (type) {
|
||||
case ControllerType::Orbis:
|
||||
return "Durango";
|
||||
|
|
@ -106,8 +108,24 @@ void UiSystem::detect_controller_type() {
|
|||
set_controller_type(detected);
|
||||
}
|
||||
|
||||
void UiSystem::refresh_tooltips() {
|
||||
void UiSystem::refresh_tooltips(const Json &data) {
|
||||
std::vector<TooltipHint> hints;
|
||||
if (data.is_object()) {
|
||||
const auto found = data.find("tooltips");
|
||||
if (found != data.end() && found->is_array()) {
|
||||
for (const Json &entry : *found) {
|
||||
if (!entry.is_object()) {
|
||||
continue;
|
||||
}
|
||||
TooltipHint hint;
|
||||
hint.icon = entry.value("icon", "");
|
||||
hint.label = entry.value("label", "");
|
||||
hints.push_back(std::move(hint));
|
||||
}
|
||||
renderer_.set_tooltips(std::move(hints));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (const ViewDefinition *definition = document_.find_view(current_view_)) {
|
||||
hints.reserve(definition->tooltips.size());
|
||||
for (const TooltipDefinition &tooltip : definition->tooltips) {
|
||||
|
|
@ -117,6 +135,21 @@ void UiSystem::refresh_tooltips() {
|
|||
renderer_.set_tooltips(std::move(hints));
|
||||
}
|
||||
|
||||
void UiSystem::focus_element(const std::string &id) {
|
||||
pending_focus_id_ = id;
|
||||
pending_focus_ = true;
|
||||
focus_engaged_ = true;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void UiSystem::set_focus_scope(const std::string &id) {
|
||||
focus_scope_id_ = id;
|
||||
pending_focus_id_.clear();
|
||||
pending_focus_ = true;
|
||||
focus_engaged_ = true;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void UiSystem::register_handlers() {
|
||||
handlers_.bind("go", [this](std::string_view argument) {
|
||||
navigate(std::string(argument));
|
||||
|
|
@ -200,13 +233,13 @@ void UiSystem::play_sound_for(const std::string &handler) {
|
|||
void UiSystem::rebuild() {
|
||||
hover_chain_.clear();
|
||||
active_text_ = nullptr;
|
||||
refresh_tooltips();
|
||||
const auto view = views_.find(current_view_);
|
||||
if (view == views_.end()) {
|
||||
return;
|
||||
}
|
||||
update_panorama();
|
||||
const Json data = view->second->data(services_);
|
||||
refresh_tooltips(data);
|
||||
DataContext context(data, &i18n_);
|
||||
ViewBuilder builder(document_, registry_);
|
||||
tree_ = builder.build(current_view_, context);
|
||||
|
|
@ -222,7 +255,33 @@ void UiSystem::rebuild() {
|
|||
static_cast<float>(logical_height_)});
|
||||
tree_->seed_clock(view_entered_ticks_);
|
||||
focus_order_.clear();
|
||||
collect_focusables(tree_.get());
|
||||
Element *scope_root = tree_.get();
|
||||
if (!focus_scope_id_.empty()) {
|
||||
Element *scope = tree_->find_by_id(focus_scope_id_);
|
||||
if (scope == nullptr) {
|
||||
focus_scope_id_.clear();
|
||||
} else {
|
||||
scope_root = scope;
|
||||
}
|
||||
}
|
||||
collect_focusables(scope_root);
|
||||
if (pending_focus_) {
|
||||
pending_focus_ = false;
|
||||
int target = 0;
|
||||
if (!pending_focus_id_.empty()) {
|
||||
for (std::size_t index = 0; index < focus_order_.size(); ++index) {
|
||||
if (focus_order_[index]->id() == pending_focus_id_) {
|
||||
target = static_cast<int>(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pending_focus_id_.clear();
|
||||
if (!focus_order_.empty()) {
|
||||
focus_ = std::clamp(target, 0, static_cast<int>(focus_order_.size()) - 1);
|
||||
focus_engaged_ = true;
|
||||
}
|
||||
}
|
||||
if (focus_ >= static_cast<int>(focus_order_.size())) {
|
||||
focus_ =
|
||||
focus_order_.empty() ? 0 : static_cast<int>(focus_order_.size()) - 1;
|
||||
|
|
@ -322,6 +381,19 @@ void UiSystem::activate_focus() {
|
|||
dispatch(handler);
|
||||
}
|
||||
|
||||
bool UiSystem::handle_back() {
|
||||
const auto view = views_.find(current_view_);
|
||||
if (view != views_.end() && view->second->on_escape(services_)) {
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
if (services_.audio != nullptr) {
|
||||
services_.audio->play_back();
|
||||
}
|
||||
navigate_back();
|
||||
return true;
|
||||
}
|
||||
|
||||
TextInputElement *UiSystem::active_text_input() {
|
||||
if (active_text_ == nullptr) {
|
||||
return nullptr;
|
||||
|
|
@ -457,7 +529,8 @@ Element *UiSystem::find_clickable(Element *element, const Vec2 &point) const {
|
|||
return found;
|
||||
}
|
||||
}
|
||||
if (element->interactive() && element->bounds().contains(point)) {
|
||||
if (element->interactive() && element->bounds().contains(point) &&
|
||||
!element->hidden_by_scroll()) {
|
||||
return element;
|
||||
}
|
||||
return nullptr;
|
||||
|
|
@ -604,17 +677,7 @@ void UiSystem::handle_event(const SDL_Event &event) {
|
|||
end_text_edit(false);
|
||||
break;
|
||||
}
|
||||
{
|
||||
const auto view = views_.find(current_view_);
|
||||
if (view != views_.end() && view->second->on_escape(services_)) {
|
||||
dirty_ = true;
|
||||
break;
|
||||
}
|
||||
if (services_.audio != nullptr) {
|
||||
services_.audio->play_back();
|
||||
}
|
||||
navigate_back();
|
||||
}
|
||||
handle_back();
|
||||
break;
|
||||
case SDLK_LEFT:
|
||||
if (editing) {
|
||||
|
|
@ -707,6 +770,13 @@ void UiSystem::handle_event(const SDL_Event &event) {
|
|||
}
|
||||
activate_focus();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_EAST:
|
||||
if (editing) {
|
||||
end_text_edit(false);
|
||||
break;
|
||||
}
|
||||
handle_back();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ public:
|
|||
focus_ = 0;
|
||||
dirty_ = true;
|
||||
};
|
||||
services_.focus_element = [this](const std::string &id) {
|
||||
focus_element(id);
|
||||
};
|
||||
services_.focus_scope = [this](const std::string &id) {
|
||||
set_focus_scope(id);
|
||||
};
|
||||
services_.read_text_input = [this]() { return text_result_; };
|
||||
services_.text_edited = [this]() { return text_edited_; };
|
||||
}
|
||||
|
|
@ -43,6 +49,8 @@ public:
|
|||
void invalidate() { dirty_ = true; }
|
||||
const std::string ¤t_view() const { return current_view_; }
|
||||
void set_controller_type(ControllerType type);
|
||||
void focus_element(const std::string &id);
|
||||
void set_focus_scope(const std::string &id);
|
||||
std::vector<Element *> hover_chain_;
|
||||
void handle_event(const SDL_Event &event);
|
||||
void render();
|
||||
|
|
@ -50,7 +58,7 @@ public:
|
|||
private:
|
||||
void register_handlers();
|
||||
void rebuild();
|
||||
void refresh_tooltips();
|
||||
void refresh_tooltips(const Json &data);
|
||||
void detect_controller_type();
|
||||
void update_panorama();
|
||||
void dispatch(const std::string &handler);
|
||||
|
|
@ -61,6 +69,7 @@ private:
|
|||
void apply_focus();
|
||||
void move_focus(int direction);
|
||||
void activate_focus();
|
||||
bool handle_back();
|
||||
TextInputElement *active_text_input();
|
||||
void begin_text_edit(Element *element);
|
||||
void end_text_edit(bool commit);
|
||||
|
|
@ -90,6 +99,9 @@ private:
|
|||
std::vector<Element *> focus_order_;
|
||||
int focus_ = 0;
|
||||
bool focus_engaged_ = false;
|
||||
std::string focus_scope_id_;
|
||||
std::string pending_focus_id_;
|
||||
bool pending_focus_ = false;
|
||||
bool gamepad_ = true;
|
||||
ControllerType controller_type_ = ControllerType::Unknown;
|
||||
bool dirty_ = true;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ struct UiServices {
|
|||
std::function<void(const std::string &)> navigate;
|
||||
std::function<void(const std::string &)> change_language;
|
||||
std::function<void()> reset_focus;
|
||||
std::function<void(const std::string &)> focus_element;
|
||||
std::function<void(const std::string &)> focus_scope;
|
||||
std::function<std::string()> read_text_input;
|
||||
std::function<bool()> text_edited;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "Ui/Views/VersionsView.hpp"
|
||||
#include "Audio/AudioEngine.hpp"
|
||||
#include "Config/ConfigSchema.hpp"
|
||||
#include "Core/Fs.hpp"
|
||||
#include "Core/Log.hpp"
|
||||
|
|
@ -8,13 +9,21 @@
|
|||
#include "Instances/InstanceManager.hpp"
|
||||
#include "Launch/Servers.hpp"
|
||||
#include "Ui/EditionResolver.hpp"
|
||||
#include "Ui/I18n.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
namespace emerald::ui {
|
||||
namespace {
|
||||
constexpr const char *kArgsSchemaFile = "Arguments.Schema.json";
|
||||
|
||||
constexpr const char *kGearMenuScope = "gear-menu";
|
||||
constexpr const char *kDeleteModalScope = "delete-modal";
|
||||
constexpr const char *kEditionRowPrefix = "edition-";
|
||||
constexpr int kMenuFirstRowTop = 244;
|
||||
constexpr int kMenuRowStep = 76;
|
||||
constexpr int kMenuMinTop = 220;
|
||||
constexpr int kMenuMaxTop = 380;
|
||||
bool has_args_schema(const instances::InstanceManager &instances,
|
||||
const std::string &instance_id) {
|
||||
const std::optional<std::string> text =
|
||||
|
|
@ -42,6 +51,7 @@ std::string state_name(instances::DownloadState state) {
|
|||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Json VersionsView::data(const UiServices &services) const {
|
||||
Json data = Json::object();
|
||||
data["titleImage"] = "/images/MenuTitle.png";
|
||||
|
|
@ -51,6 +61,18 @@ Json VersionsView::data(const UiServices &services) const {
|
|||
data["status"] = status_;
|
||||
data["editions"] = Json::array();
|
||||
data["selected"] = "legacy_evolved";
|
||||
data["selectedDesc"] = "";
|
||||
data["deleteModal"] = !delete_pending_id_.empty();
|
||||
data["deleteTitle"] = "";
|
||||
data["settingsMenu"] = !open_menu_id_.empty();
|
||||
data["menuId"] = open_menu_id_;
|
||||
data["menuTitle"] = "";
|
||||
data["menuDeleteLabel"] = "";
|
||||
data["menuTop"] = kMenuFirstRowTop;
|
||||
Json items = Json::array();
|
||||
Json danger_items = Json::array();
|
||||
data["menuItems"] = Json::array();
|
||||
data["menuDangerItems"] = Json::array();
|
||||
if (services.config == nullptr) {
|
||||
return data;
|
||||
}
|
||||
|
|
@ -71,11 +93,17 @@ Json VersionsView::data(const UiServices &services) const {
|
|||
}
|
||||
}
|
||||
|
||||
int menu_row_index = -1;
|
||||
int visible_index = 0;
|
||||
for (const instances::Edition &edition : editions) {
|
||||
const bool installed = instance_manager.is_installed(edition.id);
|
||||
if (edition.hidden && !installed) {
|
||||
continue;
|
||||
}
|
||||
if (edition.id == open_menu_id_) {
|
||||
menu_row_index = visible_index;
|
||||
}
|
||||
++visible_index;
|
||||
|
||||
Json entry = Json::object();
|
||||
entry["id"] = edition.id;
|
||||
|
|
@ -85,15 +113,20 @@ Json VersionsView::data(const UiServices &services) const {
|
|||
entry["category"] = edition.category;
|
||||
entry["installed"] = installed;
|
||||
entry["notInstalled"] = !installed;
|
||||
entry["isCustom"] = edition.id.rfind("custom_", 0) == 0;
|
||||
entry["selected"] = edition.id == active_profile;
|
||||
entry["rowClass"] = edition.id == active_profile ? "selected" : "";
|
||||
entry["gearClass"] = edition.id == open_menu_id_ ? "gear-btn-open" : "";
|
||||
entry["managed"] = instance_manager.is_managed(edition.id);
|
||||
entry["comingSoon"] = edition.coming_soon;
|
||||
entry["canDownload"] = !edition.url.empty() && !edition.hidden;
|
||||
entry["showInstall"] = !installed;
|
||||
entry["showGear"] = true;
|
||||
entry["updateAvailable"] = false;
|
||||
entry["hasArgsSchema"] =
|
||||
installed && has_args_schema(instance_manager, edition.id);
|
||||
entry["path"] = instance_manager.working_dir(edition.id).string();
|
||||
|
||||
if (services.downloads != nullptr) {
|
||||
const instances::DownloadSnapshot download =
|
||||
services.downloads->snapshot(edition.id);
|
||||
|
|
@ -128,6 +161,10 @@ Json VersionsView::data(const UiServices &services) const {
|
|||
entry["playtimeDay"] = 0;
|
||||
}
|
||||
|
||||
if (edition.id == active_profile) {
|
||||
data["selectedDesc"] = edition.desc;
|
||||
}
|
||||
|
||||
data["editions"].push_back(std::move(entry));
|
||||
}
|
||||
|
||||
|
|
@ -138,9 +175,157 @@ Json VersionsView::data(const UiServices &services) const {
|
|||
}
|
||||
}
|
||||
|
||||
if (!open_menu_id_.empty()) {
|
||||
const instances::Edition *menu_edition = nullptr;
|
||||
for (const instances::Edition &ed : editions) {
|
||||
if (ed.id == open_menu_id_) {
|
||||
menu_edition = &ed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (menu_edition != nullptr) { // neo: sadly i couldnt do it in xml (because
|
||||
// im lazy) so i did it in c++ instead
|
||||
const bool installed = instance_manager.is_installed(open_menu_id_);
|
||||
const bool is_custom = open_menu_id_.rfind("custom_", 0) == 0;
|
||||
data["menuTitle"] = menu_edition->name;
|
||||
if (menu_row_index >= 0) {
|
||||
const int top = kMenuFirstRowTop + menu_row_index * kMenuRowStep;
|
||||
data["menuTop"] = std::clamp(top, kMenuMinTop, kMenuMaxTop);
|
||||
}
|
||||
const auto translate = [&services](const char *key,
|
||||
const char *fallback) {
|
||||
return services.i18n != nullptr ? services.i18n->translate(key)
|
||||
: std::string(fallback);
|
||||
};
|
||||
data["menuDeleteLabel"] =
|
||||
is_custom ? translate("versions.removeCustom", "Remove Custom")
|
||||
: translate("versions.uninstall", "Uninstall");
|
||||
const auto add_item = [&items,
|
||||
&danger_items](const char *icon, std::string label,
|
||||
std::string action, bool danger) {
|
||||
Json item = Json::object();
|
||||
item["icon"] = icon;
|
||||
item["label"] = std::move(label);
|
||||
item["action"] = std::move(action);
|
||||
item["itemClass"] = danger ? "gear-menu-item-danger" : "";
|
||||
if (danger) {
|
||||
danger_items.push_back(std::move(item));
|
||||
} else {
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
};
|
||||
const std::string &id = open_menu_id_;
|
||||
if (installed) {
|
||||
add_item("/images/Update_Icon.png",
|
||||
translate("versions.repair", "Repair"),
|
||||
"versions.repair:" + id, false);
|
||||
}
|
||||
if (installed) {
|
||||
add_item("/images/Folder_Icon.png",
|
||||
translate("versions.openFolder", "Open Folder"),
|
||||
"versions.folder:" + id, false);
|
||||
}
|
||||
add_item("/images/steam.png",
|
||||
translate("versions.addToSteam", "Add to Steam"),
|
||||
"versions.steam:" + id, false);
|
||||
if (installed) {
|
||||
add_item("/images/Download_Icon.png",
|
||||
translate("versions.importWorld", "Import World"),
|
||||
"versions.importWorld:" + id, false);
|
||||
}
|
||||
if (installed) {
|
||||
add_item("/images/Layer_Icon.png",
|
||||
translate("versions.backup", "Backup"),
|
||||
"versions.backup:" + id, false);
|
||||
}
|
||||
add_item("/images/lock_icon.png", translate("versions.setUid", "Set UID"),
|
||||
"versions.setUid:" + id, false);
|
||||
if (is_custom) {
|
||||
add_item("/images/Change_Skin_Icon.png",
|
||||
translate("versions.editCustom", "Edit Custom"),
|
||||
"versions.editCustom:" + id, false);
|
||||
}
|
||||
add_item("/images/Trash_Bin_Icon.png",
|
||||
data["menuDeleteLabel"].get<std::string>(),
|
||||
"versions.delete:" + id, true);
|
||||
}
|
||||
}
|
||||
|
||||
data["menuItems"] = std::move(items);
|
||||
data["menuDangerItems"] = std::move(danger_items);
|
||||
if (!delete_pending_id_.empty()) {
|
||||
std::string delete_name = delete_pending_id_;
|
||||
for (const Json &item : data["editions"]) {
|
||||
if (item.value("id", "") == delete_pending_id_) {
|
||||
delete_name = item.value("name", delete_pending_id_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::string delete_title_template =
|
||||
services.i18n != nullptr
|
||||
? services.i18n->translate("versions.deleteTitle")
|
||||
: std::string("versions.deleteTitle");
|
||||
const std::string placeholder = "{{name}}";
|
||||
const std::size_t pos = delete_title_template.find(placeholder);
|
||||
if (pos != std::string::npos) {
|
||||
delete_title_template.replace(pos, placeholder.size(), delete_name);
|
||||
}
|
||||
data["deleteTitle"] = std::move(delete_title_template);
|
||||
}
|
||||
|
||||
if (!open_menu_id_.empty() || !delete_pending_id_.empty()) {
|
||||
Json tooltips = Json::array(); // neo: thats how you do it manually :3
|
||||
Json select = Json::object();
|
||||
select["icon"] = "A";
|
||||
select["label"] = "Select";
|
||||
Json close = Json::object();
|
||||
close["icon"] = "B";
|
||||
close["label"] = services.i18n != nullptr
|
||||
? services.i18n->translate("common.close")
|
||||
: std::string("Close");
|
||||
tooltips.push_back(std::move(select));
|
||||
tooltips.push_back(std::move(close));
|
||||
data["tooltips"] = std::move(tooltips);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
void VersionsView::open_menu(std::string id, UiServices &services) {
|
||||
open_menu_id_ = std::move(id);
|
||||
if (services.focus_scope) {
|
||||
services.focus_scope(kGearMenuScope);
|
||||
}
|
||||
}
|
||||
|
||||
void VersionsView::close_menu(UiServices &services) {
|
||||
if (open_menu_id_.empty()) {
|
||||
return;
|
||||
}
|
||||
const std::string id = open_menu_id_;
|
||||
open_menu_id_.clear();
|
||||
if (services.focus_scope) {
|
||||
services.focus_scope("");
|
||||
}
|
||||
if (!id.empty() && services.focus_element) {
|
||||
services.focus_element(std::string(kEditionRowPrefix) + id);
|
||||
}
|
||||
}
|
||||
|
||||
void VersionsView::close_delete_modal(UiServices &services) {
|
||||
if (delete_pending_id_.empty()) {
|
||||
return;
|
||||
}
|
||||
const std::string id = delete_pending_id_;
|
||||
delete_pending_id_.clear();
|
||||
if (services.focus_scope) {
|
||||
services.focus_scope("");
|
||||
}
|
||||
if (!id.empty() && services.focus_element) {
|
||||
services.focus_element(std::string(kEditionRowPrefix) + id);
|
||||
}
|
||||
}
|
||||
|
||||
bool VersionsView::action(std::string_view name, UiServices &services) {
|
||||
if (services.config == nullptr) {
|
||||
return false;
|
||||
|
|
@ -152,9 +337,11 @@ bool VersionsView::action(std::string_view name, UiServices &services) {
|
|||
colon == std::string::npos ? action : action.substr(0, colon);
|
||||
const std::string argument =
|
||||
colon == std::string::npos ? std::string() : action.substr(colon + 1);
|
||||
|
||||
if (verb == "versions.select") {
|
||||
services.config->data().profile = argument;
|
||||
services.config->save();
|
||||
close_menu(services);
|
||||
return true;
|
||||
}
|
||||
if (verb == "versions.refresh") {
|
||||
|
|
@ -162,8 +349,29 @@ bool VersionsView::action(std::string_view name, UiServices &services) {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (action == "versions.menu.close") {
|
||||
close_menu(services);
|
||||
if (services.audio != nullptr) {
|
||||
services.audio->play_back();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (verb == "versions.settings") {
|
||||
if (open_menu_id_ == argument) {
|
||||
close_menu(services);
|
||||
} else {
|
||||
open_menu(argument, services);
|
||||
if (services.audio != nullptr) {
|
||||
services.audio->play_press();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const instances::InstanceManager instance_manager(services.config->data());
|
||||
if (verb == "versions.install" || verb == "versions.repair") {
|
||||
close_menu(services);
|
||||
if (services.downloads == nullptr) {
|
||||
status_ = "Downloads are not available";
|
||||
return true;
|
||||
|
|
@ -201,12 +409,14 @@ bool VersionsView::action(std::string_view name, UiServices &services) {
|
|||
status_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (verb == "versions.cancel") {
|
||||
if (services.downloads != nullptr) {
|
||||
services.downloads->cancel(argument);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (verb == "versions.dismiss") {
|
||||
if (services.downloads != nullptr) {
|
||||
services.downloads->clear(argument);
|
||||
|
|
@ -217,18 +427,72 @@ bool VersionsView::action(std::string_view name, UiServices &services) {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (verb == "versions.folder" || verb == "versions.steam" ||
|
||||
verb == "versions.importWorld" || verb == "versions.backup" ||
|
||||
verb == "versions.setUid" || verb == "versions.editCustom") {
|
||||
close_menu(services);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (verb == "versions.delete") {
|
||||
if (!instance_manager.delete_instance(argument)) {
|
||||
status_ = "Cannot delete " + argument;
|
||||
close_menu(services);
|
||||
delete_pending_id_ = argument;
|
||||
if (services.focus_scope) {
|
||||
services.focus_scope(kDeleteModalScope);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == "versions.delete.cancel") {
|
||||
close_delete_modal(services);
|
||||
if (services.audio != nullptr) {
|
||||
services.audio->play_back();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == "versions.delete.confirm") {
|
||||
const std::string pending = delete_pending_id_;
|
||||
close_delete_modal(services);
|
||||
if (pending.empty()) {
|
||||
return true;
|
||||
}
|
||||
if (services.config->data().profile.value_or("") == argument) {
|
||||
if (!instance_manager.delete_instance(pending)) {
|
||||
status_ = "Cannot delete " + pending;
|
||||
return true;
|
||||
}
|
||||
if (services.config->data().profile.value_or("") == pending) {
|
||||
services.config->data().profile = "legacy_evolved";
|
||||
services.config->save();
|
||||
}
|
||||
status_ = "Deleted " + argument;
|
||||
status_ = "Deleted " + pending;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool VersionsView::on_escape(UiServices &services) {
|
||||
if (!open_menu_id_.empty()) {
|
||||
close_menu(services);
|
||||
if (services.audio != nullptr) {
|
||||
services.audio->play_back();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!delete_pending_id_.empty()) {
|
||||
close_delete_modal(services);
|
||||
if (services.audio != nullptr) {
|
||||
services.audio->play_back();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (services.audio != nullptr) {
|
||||
services.audio->play_back();
|
||||
}
|
||||
if (services.navigate) {
|
||||
services.navigate("home");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace emerald::ui
|
||||
|
|
@ -8,10 +8,14 @@ public:
|
|||
std::string_view id() const override { return "versions"; }
|
||||
Json data(const UiServices &services) const override;
|
||||
bool action(std::string_view name, UiServices &services) override;
|
||||
void set_status(std::string status) { status_ = std::move(status); }
|
||||
const std::string &status() const { return status_; }
|
||||
bool on_escape(UiServices &services) override;
|
||||
|
||||
private:
|
||||
void open_menu(std::string id, UiServices &services);
|
||||
void close_menu(UiServices &services);
|
||||
void close_delete_modal(UiServices &services);
|
||||
std::string status_;
|
||||
std::string delete_pending_id_;
|
||||
std::string open_menu_id_;
|
||||
};
|
||||
} // namespace emerald::ui
|
||||
} // namespace emerald::ui
|
||||
|
|
|
|||
|
|
@ -277,6 +277,8 @@ ElementPtr ViewBuilder::build_element(const pugi::xml_node &node,
|
|||
|
||||
StringMap attributes;
|
||||
std::vector<std::string> classes;
|
||||
std::string inline_style;
|
||||
std::map<std::string, std::string> style_attributes;
|
||||
for (const pugi::xml_attribute attribute : node.attributes()) {
|
||||
const std::string name = attribute.name();
|
||||
if (name == "foreach" || name == "if" || name == "as") {
|
||||
|
|
@ -292,13 +294,18 @@ ElementPtr ViewBuilder::build_element(const pugi::xml_node &node,
|
|||
classes = split_classes(value);
|
||||
} else if (name == "onclick") {
|
||||
element->set_click_handler(value);
|
||||
} else if (!is_style_attribute(name)) {
|
||||
attributes[name] = value;
|
||||
} else if (name == "style") {
|
||||
inline_style = std::move(value);
|
||||
} else if (is_style_attribute(name)) {
|
||||
style_attributes[name] = std::move(value);
|
||||
} else {
|
||||
attributes[name] = std::move(value);
|
||||
}
|
||||
}
|
||||
element->set_attributes(std::move(attributes));
|
||||
element->set_classes(std::move(classes));
|
||||
apply_styles(*element, node);
|
||||
apply_styles(*element, node, std::move(inline_style),
|
||||
std::move(style_attributes));
|
||||
for (const pugi::xml_node child : node.children()) {
|
||||
ElementPtr child_element = build_node(child, context, depth + 1);
|
||||
if (child_element != nullptr) {
|
||||
|
|
@ -343,20 +350,19 @@ ElementPtr ViewBuilder::instantiate_template(const std::string &name,
|
|||
return result;
|
||||
}
|
||||
|
||||
void ViewBuilder::apply_styles(Element &element,
|
||||
const pugi::xml_node &node) const {
|
||||
void ViewBuilder::apply_styles(
|
||||
Element &element, const pugi::xml_node &node,
|
||||
const std::string &inline_style,
|
||||
const std::map<std::string, std::string> &style_attributes) const {
|
||||
const auto build = [&](bool hovered) {
|
||||
ComputedStyle style;
|
||||
style = document_.styles().applyTo(style, node.name(), element.id(),
|
||||
element.classes(), hovered);
|
||||
for (const pugi::xml_attribute attribute : node.attributes()) {
|
||||
const std::string name = attribute.name();
|
||||
const std::string value = attribute.as_string();
|
||||
if (name == "style") {
|
||||
style = ComputedStyle::merge(style, parse_inline_style(value));
|
||||
} else if (is_style_attribute(name)) {
|
||||
apply_style_property(style, name, value);
|
||||
}
|
||||
if (!inline_style.empty()) {
|
||||
style = ComputedStyle::merge(style, parse_inline_style(inline_style));
|
||||
}
|
||||
for (const auto &[name, value] : style_attributes) {
|
||||
apply_style_property(style, name, value);
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ private:
|
|||
const pugi::xml_node &use_node,
|
||||
DataContext &context,
|
||||
std::size_t depth) const;
|
||||
void apply_styles(Element &element, const pugi::xml_node &node) const;
|
||||
void apply_styles(Element &element, const pugi::xml_node &node,
|
||||
const std::string &inline_style,
|
||||
const std::map<std::string, std::string> &style_attributes) const;
|
||||
const UiDocument &document_;
|
||||
ElementRegistry ®istry_;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue