Refine dark editor and launcher interface
Native and manual checks / native (ubuntu-24.04) (push) Failing after 52s
Native and manual checks / manual (push) Successful in 34s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
Native and manual checks / native (ubuntu-24.04) (push) Failing after 52s
Native and manual checks / manual (push) Successful in 34s
Windows editor and software Vulkan / windows-graphics (push) Canceled after 0s
Native and manual checks / native (windows-2025) (push) Canceled after 0s
This commit is contained in:
+48
-22
@@ -10,6 +10,7 @@
|
|||||||
namespace faset::editor {
|
namespace faset::editor {
|
||||||
namespace {
|
namespace {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
using ui::Appearance;
|
||||||
using ui::Kind;
|
using ui::Kind;
|
||||||
using ui::Widget;
|
using ui::Widget;
|
||||||
fs::path user_home() {
|
fs::path user_home() {
|
||||||
@@ -129,10 +130,18 @@ struct ProjectLauncher::Impl {
|
|||||||
: renderer(r), ui(engine / "assets/fonts/NotoSans.ttf"),
|
: renderer(r), ui(engine / "assets/fonts/NotoSans.ttf"),
|
||||||
recents_file(recents.empty() ? recent_path() : recents) {
|
recents_file(recents.empty() ? recent_path() : recents) {
|
||||||
auto theme = ui::Theme::load(engine / "assets/ui/dark.json");
|
auto theme = ui::Theme::load(engine / "assets/ui/dark.json");
|
||||||
theme.font_size = 15;
|
theme.font_size = 14;
|
||||||
theme.row_height = 30;
|
theme.row_height = 28;
|
||||||
ui.set_theme(theme);
|
ui.set_theme(theme);
|
||||||
ui.apply_layout(read_json(engine / "assets/ui/project-launcher-layout.json"));
|
ui.apply_layout(read_json(engine / "assets/ui/project-launcher-layout.json"));
|
||||||
|
for (const auto* id : {"launcher-heading-rule", "launcher-recent-rule",
|
||||||
|
"launcher-actions-rule"})
|
||||||
|
ui.find(id)->appearance = Appearance::Section;
|
||||||
|
for (const auto* id : {"launcher-browse", "launcher-cancel"})
|
||||||
|
ui.find(id)->appearance = Appearance::Quiet;
|
||||||
|
ui.find("launcher-submit")->appearance = Appearance::Primary;
|
||||||
|
for (const auto* id : {"launcher-recent-title", "launcher-hint", "launcher-shortcuts"})
|
||||||
|
ui.find(id)->enabled = false;
|
||||||
ui.set_clipboard([this] { return renderer.clipboard(); },
|
ui.set_clipboard([this] { return renderer.clipboard(); },
|
||||||
[this](const std::string& value) { renderer.set_clipboard(value); });
|
[this](const std::string& value) { renderer.set_clipboard(value); });
|
||||||
ui.set_ime(
|
ui.set_ime(
|
||||||
@@ -145,7 +154,6 @@ struct ProjectLauncher::Impl {
|
|||||||
ui.find("launcher-create")->on_click = [this](Widget&) { set_mode(true); };
|
ui.find("launcher-create")->on_click = [this](Widget&) { set_mode(true); };
|
||||||
ui.find("launcher-2d")->on_click = [this](Widget&) { dimension = 2; };
|
ui.find("launcher-2d")->on_click = [this](Widget&) { dimension = 2; };
|
||||||
ui.find("launcher-3d")->on_click = [this](Widget&) { dimension = 3; };
|
ui.find("launcher-3d")->on_click = [this](Widget&) { dimension = 3; };
|
||||||
ui.find("launcher-submit")->selected = true;
|
|
||||||
ui.find("launcher-submit")->on_click = [this](Widget&) { submit(); };
|
ui.find("launcher-submit")->on_click = [this](Widget&) { submit(); };
|
||||||
ui.find("launcher-cancel")->on_click = [this](Widget&) { cancelled = true; };
|
ui.find("launcher-cancel")->on_click = [this](Widget&) { cancelled = true; };
|
||||||
ui.find("launcher-name")->on_preview = [this](Widget&) { error.clear(); };
|
ui.find("launcher-name")->on_preview = [this](Widget&) { error.clear(); };
|
||||||
@@ -177,7 +185,7 @@ struct ProjectLauncher::Impl {
|
|||||||
continue;
|
continue;
|
||||||
const auto id = "launcher-recent-" + std::to_string(i++);
|
const auto id = "launcher-recent-" + std::to_string(i++);
|
||||||
auto& item = list.add(Kind::TreeRow, id, selection.name);
|
auto& item = list.add(Kind::TreeRow, id, selection.name);
|
||||||
item.layout.height = 40;
|
item.layout.height = 38;
|
||||||
item.tooltip = path_to_utf8(selection.path);
|
item.tooltip = path_to_utf8(selection.path);
|
||||||
item.on_click = [this, path = selection.path](Widget&) {
|
item.on_click = [this, path = selection.path](Widget&) {
|
||||||
ui.update_text("launcher-path", path_to_utf8(path), true);
|
ui.update_text("launcher-path", path_to_utf8(path), true);
|
||||||
@@ -187,7 +195,7 @@ struct ProjectLauncher::Impl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (i == 0)
|
if (i == 0)
|
||||||
list.add(Kind::Label, "launcher-recents-empty", "No recent projects");
|
list.add(Kind::Label, "launcher-recents-empty", "No recent projects").enabled = false;
|
||||||
}
|
}
|
||||||
void submit() {
|
void submit() {
|
||||||
ui.clear_focus();
|
ui.clear_focus();
|
||||||
@@ -223,18 +231,22 @@ struct ProjectLauncher::Impl {
|
|||||||
void build_browser() {
|
void build_browser() {
|
||||||
auto& panel = ui.root().add(Kind::Panel, "launcher-browser");
|
auto& panel = ui.root().add(Kind::Panel, "launcher-browser");
|
||||||
panel.layout.absolute = true;
|
panel.layout.absolute = true;
|
||||||
panel.layout.padding = 16;
|
panel.layout.padding = 20;
|
||||||
panel.layout.gap = 10;
|
panel.layout.gap = 8;
|
||||||
panel.visible = false;
|
panel.visible = false;
|
||||||
auto& title = panel.add(Kind::Label, "browser-heading", "Choose project directory");
|
auto& title = panel.add(Kind::Label, "browser-heading", "Choose project directory");
|
||||||
title.font_size = 20;
|
title.font_size = 20;
|
||||||
title.layout.height = 35;
|
title.layout.height = 44;
|
||||||
auto& nav = panel.add(Kind::Row, "browser-navigation");
|
auto& nav = panel.add(Kind::Row, "browser-navigation");
|
||||||
nav.layout.height = 36;
|
nav.layout.height = 38;
|
||||||
button(nav, "browser-up", "Up", [this] {
|
auto& up = button(nav, "browser-up", "Up", [this] {
|
||||||
browse_to(browse_path.parent_path());
|
browse_to(browse_path.parent_path());
|
||||||
}).layout.width = 60;
|
});
|
||||||
button(nav, "browser-home", "Home", [this] { browse_to(user_home()); }).layout.width = 76;
|
up.appearance = Appearance::Quiet;
|
||||||
|
up.layout.width = 60;
|
||||||
|
auto& home = button(nav, "browser-home", "Home", [this] { browse_to(user_home()); });
|
||||||
|
home.appearance = Appearance::Quiet;
|
||||||
|
home.layout.width = 76;
|
||||||
auto& path = nav.add(Kind::TextField, "browser-path");
|
auto& path = nav.add(Kind::TextField, "browser-path");
|
||||||
path.layout.flex = 1;
|
path.layout.flex = 1;
|
||||||
path.on_preview = [this](Widget&) { browser_valid = false; };
|
path.on_preview = [this](Widget&) { browser_valid = false; };
|
||||||
@@ -250,14 +262,18 @@ struct ProjectLauncher::Impl {
|
|||||||
list.layout.scroll = true;
|
list.layout.scroll = true;
|
||||||
list.layout.gap = 2;
|
list.layout.gap = 2;
|
||||||
auto& actions = panel.add(Kind::Row, "browser-actions");
|
auto& actions = panel.add(Kind::Row, "browser-actions");
|
||||||
actions.layout.height = 36;
|
actions.layout.height = 42;
|
||||||
button(actions, "browser-choose", "Choose directory", [this] {
|
auto& choose = button(actions, "browser-choose", "Choose directory", [this] {
|
||||||
if (browser_valid) {
|
if (browser_valid) {
|
||||||
ui.update_text("launcher-path", path_to_utf8(browse_path), true);
|
ui.update_text("launcher-path", path_to_utf8(browse_path), true);
|
||||||
close_browser();
|
close_browser();
|
||||||
}
|
}
|
||||||
}).layout.width = 180;
|
});
|
||||||
button(actions, "browser-cancel", "Cancel", [this] { close_browser(); }).layout.width = 90;
|
choose.appearance = Appearance::Primary;
|
||||||
|
choose.layout.width = 180;
|
||||||
|
auto& cancel = button(actions, "browser-cancel", "Cancel", [this] { close_browser(); });
|
||||||
|
cancel.appearance = Appearance::Quiet;
|
||||||
|
cancel.layout.width = 90;
|
||||||
panel.add(Kind::Label, "browser-error");
|
panel.add(Kind::Label, "browser-error");
|
||||||
}
|
}
|
||||||
void browse_to(const fs::path& path) {
|
void browse_to(const fs::path& path) {
|
||||||
@@ -285,14 +301,14 @@ struct ProjectLauncher::Impl {
|
|||||||
for (const auto& child : children) {
|
for (const auto& child : children) {
|
||||||
auto& row = list.add(Kind::TreeRow, "browser-entry-" + std::to_string(index++),
|
auto& row = list.add(Kind::TreeRow, "browser-entry-" + std::to_string(index++),
|
||||||
"[Folder] " + path_to_utf8(child.filename()));
|
"[Folder] " + path_to_utf8(child.filename()));
|
||||||
row.layout.height = 34;
|
row.layout.height = 36;
|
||||||
row.on_click = [this, child](Widget&) {
|
row.on_click = [this, child](Widget&) {
|
||||||
ui.clear_focus();
|
ui.clear_focus();
|
||||||
browse_to(child);
|
browse_to(child);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (children.empty())
|
if (children.empty())
|
||||||
list.add(Kind::Label, "browser-empty", "No subdirectories");
|
list.add(Kind::Label, "browser-empty", "No subdirectories").enabled = false;
|
||||||
} catch (const fs::filesystem_error&) {
|
} catch (const fs::filesystem_error&) {
|
||||||
error = "Cannot access this directory.";
|
error = "Cannot access this directory.";
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -335,8 +351,14 @@ struct ProjectLauncher::Impl {
|
|||||||
ui.find("launcher-create")->selected = create;
|
ui.find("launcher-create")->selected = create;
|
||||||
ui.find("launcher-2d")->selected = dimension == 2;
|
ui.find("launcher-2d")->selected = dimension == 2;
|
||||||
ui.find("launcher-3d")->selected = dimension == 3;
|
ui.find("launcher-3d")->selected = dimension == 3;
|
||||||
|
ui.find("launcher-2d")->appearance =
|
||||||
|
dimension == 2 ? Appearance::Quiet : Appearance::Default;
|
||||||
|
ui.find("launcher-3d")->appearance =
|
||||||
|
dimension == 3 ? Appearance::Quiet : Appearance::Default;
|
||||||
for (const auto* id : {"launcher-name-label", "launcher-name", "launcher-dimension-label",
|
for (const auto* id : {"launcher-name-label", "launcher-name", "launcher-dimension-label",
|
||||||
"launcher-dimensions"})
|
"launcher-dimensions", "launcher-name-group",
|
||||||
|
"launcher-name-space", "launcher-path-space",
|
||||||
|
"launcher-dimension-group"})
|
||||||
ui.find(id)->visible = create;
|
ui.find(id)->visible = create;
|
||||||
ui.find("launcher-heading")->text = create ? "Create project" : "Open project";
|
ui.find("launcher-heading")->text = create ? "Create project" : "Open project";
|
||||||
ui.find("launcher-submit")->text = create ? "Create project" : "Open project";
|
ui.find("launcher-submit")->text = create ? "Create project" : "Open project";
|
||||||
@@ -352,9 +374,13 @@ struct ProjectLauncher::Impl {
|
|||||||
dialog->layout.height = std::max(300.f, std::min(540.f, logical_height() - 40));
|
dialog->layout.height = std::max(300.f, std::min(540.f, logical_height() - 40));
|
||||||
dialog->layout.x = (logical_width() - dialog->layout.width) * .5f;
|
dialog->layout.x = (logical_width() - dialog->layout.width) * .5f;
|
||||||
dialog->layout.y = (logical_height() - dialog->layout.height) * .5f;
|
dialog->layout.y = (logical_height() - dialog->layout.height) * .5f;
|
||||||
ui.find("launcher-sidebar")->layout.width =
|
const auto sidebar_width = std::clamp(logical_width() * .24f, 180.f, 235.f);
|
||||||
std::clamp(logical_width() * .24f, 180.f, 235.f);
|
const auto main_padding = logical_width() < 850 ? 20.f : 36.f;
|
||||||
ui.find("launcher-main")->layout.padding = logical_width() < 850 ? 16 : 32;
|
ui.find("launcher-sidebar")->layout.width = sidebar_width;
|
||||||
|
ui.find("launcher-sidebar")->layout.padding = logical_width() < 850 ? 14.f : 18.f;
|
||||||
|
ui.find("launcher-main")->layout.padding = main_padding;
|
||||||
|
ui.find("launcher-form")->layout.width =
|
||||||
|
std::min(720.f, std::max(0.f, logical_width() - sidebar_width - main_padding * 2));
|
||||||
}
|
}
|
||||||
void frame(const std::vector<render::Event>& events) {
|
void frame(const std::vector<render::Event>& events) {
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
+10
-10
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"background": [0.073, 0.078, 0.090, 1],
|
"background": [0.078, 0.078, 0.078, 1],
|
||||||
"surface": [0.102, 0.108, 0.122, 1],
|
"surface": [0.110, 0.110, 0.114, 1],
|
||||||
"raised": [0.145, 0.151, 0.169, 1],
|
"raised": [0.153, 0.153, 0.161, 1],
|
||||||
"hover": [0.19, 0.197, 0.219, 1],
|
"hover": [0.196, 0.196, 0.204, 1],
|
||||||
"border": [0.24, 0.25, 0.278, 1],
|
"border": [0.227, 0.227, 0.239, 1],
|
||||||
"text": [0.88, 0.889, 0.91, 1],
|
"text": [0.878, 0.878, 0.886, 1],
|
||||||
"muted": [0.59, 0.61, 0.65, 1],
|
"muted": [0.604, 0.604, 0.624, 1],
|
||||||
"accent": [0.65, 0.60, 0.88, 1],
|
"accent": [0.659, 0.627, 0.761, 1],
|
||||||
"selection": [0.26, 0.245, 0.35, 1],
|
"selection": [0.220, 0.212, 0.251, 1],
|
||||||
"danger": [0.92, 0.39, 0.38, 1],
|
"danger": [0.835, 0.431, 0.420, 1],
|
||||||
"font_size": 14,
|
"font_size": 14,
|
||||||
"row_height": 28,
|
"row_height": 28,
|
||||||
"padding": 8,
|
"padding": 8,
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
"root": {
|
"root": {
|
||||||
"id": "root", "kind": "column", "layout": {"gap": 1},
|
"id": "root", "kind": "column", "layout": {"gap": 1},
|
||||||
"children": [
|
"children": [
|
||||||
{"id": "menubar", "kind": "panel", "layout": {"height": 34, "padding": 3}},
|
{"id": "menubar", "kind": "panel", "layout": {"height": 36, "padding": 3}},
|
||||||
{"id": "toolbar", "kind": "panel", "layout": {"height": 40, "padding": 5}},
|
{"id": "toolbar", "kind": "panel", "layout": {"height": 44, "padding": 7}},
|
||||||
{"id": "workspace", "kind": "row", "layout": {"flex": 1, "gap": 1}, "children": [
|
{"id": "workspace", "kind": "row", "layout": {"flex": 1, "gap": 1}, "children": [
|
||||||
{"id": "scene_panel", "kind": "panel", "layout": {"width": 224, "min_width": 150, "gap": 0}},
|
{"id": "scene_panel", "kind": "panel", "layout": {"width": 238, "min_width": 150, "gap": 0}},
|
||||||
{"id": "left_divider", "kind": "divider", "layout": {"width": 5}},
|
{"id": "left_divider", "kind": "divider", "layout": {"width": 5}},
|
||||||
{"id": "viewport", "kind": "viewport", "layout": {"flex": 1, "min_width": 180}},
|
{"id": "viewport", "kind": "viewport", "layout": {"flex": 1, "min_width": 180}},
|
||||||
{"id": "right_divider", "kind": "divider", "layout": {"width": 5}},
|
{"id": "right_divider", "kind": "divider", "layout": {"width": 5}},
|
||||||
{"id": "inspector_panel", "kind": "panel", "layout": {"width": 300, "min_width": 210, "gap": 0}}
|
{"id": "inspector_panel", "kind": "panel", "layout": {"width": 312, "min_width": 210, "gap": 0}}
|
||||||
]},
|
]},
|
||||||
{"id": "bottom_divider", "kind": "divider", "layout": {"height": 5}},
|
{"id": "bottom_divider", "kind": "divider", "layout": {"height": 5}},
|
||||||
{"id": "bottom_panel", "kind": "panel", "layout": {"height": 184, "min_height": 90, "gap": 0}},
|
{"id": "bottom_panel", "kind": "panel", "layout": {"height": 190, "min_height": 90, "gap": 0}},
|
||||||
{"id": "statusbar", "kind": "panel", "layout": {"height": 26, "padding": 0}}
|
{"id": "statusbar", "kind": "panel", "layout": {"height": 25, "padding": 0}}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,34 +5,54 @@
|
|||||||
"children": [{
|
"children": [{
|
||||||
"id": "launcher-body", "kind": "row", "layout": {"flex": 1, "gap": 0},
|
"id": "launcher-body", "kind": "row", "layout": {"flex": 1, "gap": 0},
|
||||||
"children": [
|
"children": [
|
||||||
{"id": "launcher-sidebar", "kind": "panel", "layout": {"width": 235, "padding": 16, "gap": 8}, "children": [
|
{"id": "launcher-sidebar", "kind": "panel", "layout": {"width": 235, "padding": 18, "gap": 0}, "children": [
|
||||||
{"id": "launcher-wordmark", "kind": "label", "text": "Faset", "font_size": 26, "layout": {"height": 66}},
|
{"id": "launcher-wordmark", "kind": "label", "text": "Faset", "font_size": 24, "layout": {"height": 70}},
|
||||||
{"id": "launcher-open", "kind": "tree_row", "text": "Open project", "layout": {"height": 40}},
|
{"id": "launcher-nav", "kind": "column", "layout": {"gap": 4}, "children": [
|
||||||
{"id": "launcher-create", "kind": "tree_row", "text": "Create project", "layout": {"height": 40}},
|
{"id": "launcher-open", "kind": "tree_row", "text": "Open project", "layout": {"height": 38}},
|
||||||
{"id": "launcher-recent-title", "kind": "label", "text": "Recent projects", "layout": {"height": 54}},
|
{"id": "launcher-create", "kind": "tree_row", "text": "Create project", "layout": {"height": 38}}
|
||||||
{"id": "launcher-recents", "kind": "column", "layout": {"flex": 1, "scroll": true, "gap": 6}}
|
]},
|
||||||
|
{"id": "launcher-sidebar-space", "kind": "row", "layout": {"height": 22}},
|
||||||
|
{"id": "launcher-recent-rule", "kind": "row", "layout": {"height": 1}},
|
||||||
|
{"id": "launcher-recent-title", "kind": "label", "text": "Recent projects", "font_size": 13, "layout": {"height": 48}},
|
||||||
|
{"id": "launcher-recents", "kind": "column", "layout": {"flex": 1, "scroll": true, "gap": 4}}
|
||||||
]},
|
]},
|
||||||
{"id": "launcher-main", "kind": "column", "layout": {"flex": 1, "padding": 32, "gap": 12, "scroll": true}, "children": [
|
{"id": "launcher-main", "kind": "column", "layout": {"flex": 1, "padding": 36, "gap": 0, "scroll": true}, "children": [
|
||||||
{"id": "launcher-heading", "kind": "label", "text": "Open project", "font_size": 26, "layout": {"height": 64}},
|
{"id": "launcher-heading", "kind": "label", "text": "Open project", "font_size": 26, "layout": {"height": 64}},
|
||||||
{"id": "launcher-name-label", "kind": "label", "text": "Project name"},
|
{"id": "launcher-heading-rule", "kind": "row", "layout": {"height": 1}},
|
||||||
{"id": "launcher-name", "kind": "text_field", "text": "My Game", "layout": {"height": 40}},
|
{"id": "launcher-form", "kind": "column", "layout": {"width": 720, "gap": 0}, "children": [
|
||||||
{"id": "launcher-path-label", "kind": "label", "text": "Project directory"},
|
{"id": "launcher-form-top", "kind": "row", "layout": {"height": 24}},
|
||||||
{"id": "launcher-path-row", "kind": "row", "layout": {"height": 40, "gap": 8}, "children": [
|
{"id": "launcher-name-group", "kind": "column", "layout": {"gap": 6}, "children": [
|
||||||
{"id": "launcher-path", "kind": "text_field", "layout": {"flex": 1}},
|
{"id": "launcher-name-label", "kind": "label", "text": "Project name", "layout": {"height": 24}},
|
||||||
{"id": "launcher-browse", "kind": "button", "text": "Browse", "layout": {"width": 84}}
|
{"id": "launcher-name", "kind": "text_field", "text": "My Game", "layout": {"height": 42}}
|
||||||
|
]},
|
||||||
|
{"id": "launcher-name-space", "kind": "row", "layout": {"height": 20}},
|
||||||
|
{"id": "launcher-path-group", "kind": "column", "layout": {"gap": 6}, "children": [
|
||||||
|
{"id": "launcher-path-label", "kind": "label", "text": "Project directory", "layout": {"height": 24}},
|
||||||
|
{"id": "launcher-path-row", "kind": "row", "layout": {"height": 42, "gap": 8}, "children": [
|
||||||
|
{"id": "launcher-path", "kind": "text_field", "layout": {"flex": 1}},
|
||||||
|
{"id": "launcher-browse", "kind": "button", "text": "Browse", "layout": {"width": 88}}
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
{"id": "launcher-path-space", "kind": "row", "layout": {"height": 20}},
|
||||||
|
{"id": "launcher-dimension-group", "kind": "column", "layout": {"gap": 6}, "children": [
|
||||||
|
{"id": "launcher-dimension-label", "kind": "label", "text": "Initial scene type", "layout": {"height": 24}},
|
||||||
|
{"id": "launcher-dimensions", "kind": "row", "layout": {"height": 42, "gap": 4}, "children": [
|
||||||
|
{"id": "launcher-2d", "kind": "button", "text": "2D", "layout": {"width": 116}},
|
||||||
|
{"id": "launcher-3d", "kind": "button", "text": "3D", "layout": {"width": 116}}
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
{"id": "launcher-hint", "kind": "label", "text": "Open a directory containing project.faset.json.", "font_size": 13, "layout": {"height": 36}},
|
||||||
|
{"id": "launcher-actions-space", "kind": "row", "layout": {"height": 22}},
|
||||||
|
{"id": "launcher-actions-rule", "kind": "row", "layout": {"height": 1}},
|
||||||
|
{"id": "launcher-actions-top", "kind": "row", "layout": {"height": 18}},
|
||||||
|
{"id": "launcher-actions", "kind": "row", "layout": {"height": 42, "gap": 10}, "children": [
|
||||||
|
{"id": "launcher-submit", "kind": "button", "text": "Open project", "layout": {"width": 176}},
|
||||||
|
{"id": "launcher-cancel", "kind": "button", "text": "Cancel", "layout": {"width": 90}}
|
||||||
|
]},
|
||||||
|
{"id": "launcher-error", "kind": "label", "layout": {"height": 36}}
|
||||||
]},
|
]},
|
||||||
{"id": "launcher-dimension-label", "kind": "label", "text": "Initial scene type"},
|
{"id": "launcher-main-spacer", "kind": "row", "layout": {"flex": 1}},
|
||||||
{"id": "launcher-dimensions", "kind": "row", "layout": {"height": 40, "gap": 0}, "children": [
|
{"id": "launcher-shortcuts", "kind": "label", "text": "Tab: next field Ctrl+Enter: continue Escape: cancel", "font_size": 12, "layout": {"height": 24}}
|
||||||
{"id": "launcher-2d", "kind": "tab", "text": "2D", "layout": {"width": 120}},
|
|
||||||
{"id": "launcher-3d", "kind": "tab", "text": "3D", "layout": {"width": 120}}
|
|
||||||
]},
|
|
||||||
{"id": "launcher-hint", "kind": "label", "text": "Open a directory containing project.faset.json.", "layout": {"height": 36}},
|
|
||||||
{"id": "launcher-actions", "kind": "row", "layout": {"height": 40, "gap": 8}, "children": [
|
|
||||||
{"id": "launcher-submit", "kind": "button", "text": "Open project", "layout": {"width": 180}},
|
|
||||||
{"id": "launcher-cancel", "kind": "button", "text": "Cancel", "layout": {"width": 90}}
|
|
||||||
]},
|
|
||||||
{"id": "launcher-error", "kind": "label", "layout": {"height": 30}},
|
|
||||||
{"id": "launcher-shortcuts", "kind": "label", "text": "Tab: next field Ctrl+Enter: continue Escape: cancel"}
|
|
||||||
]}
|
]}
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Editor visual direction — 2026-09-23
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Generated with the built-in image generation tool before changing the native editor UI. This is a visual reference, not a working Faset screenshot or a feature specification. The generated scene, icons, asset columns, FPS counter, tag/layer controls and viewport shading are illustrative. The implemented controls must reflect real Faset behavior.
|
||||||
|
|
||||||
|
## Implemented native UI
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
These are Vulkan captures from the Linux UI integration tests after the redesign. Their test project data and viewport contents are examples; the interaction model is defined by the native UI and editor code.
|
||||||
|
|
||||||
|
## Design brief
|
||||||
|
|
||||||
|
- A quiet graphics workbench: the rendered scene is the focal point; tool chrome recedes.
|
||||||
|
- Near-black neutral surfaces: background `#141414`, panels `#1c1c1d`, raised controls `#272729`, hairline borders `#3a3a3d`.
|
||||||
|
- Light text `#e0e0e2`, secondary text `#9a9a9f`, restrained lavender `#a8a0c2` for focus, selection and the primary Play action.
|
||||||
|
- Keep Noto Sans for the native editor's single-font Unicode pipeline; use size and spacing for hierarchy. Compact 14 px controls and 28 px rows.
|
||||||
|
- Retain the real Scene / viewport / Inspector / bottom dock layout, stable widget IDs, keyboard focus, editor commands and MCP parity. Use no raster screenshot as an interactive background.
|
||||||
|
- Make secondary actions quiet; keep explicit input fields and modal actions legible. Show file names first and preserve full asset paths in tooltips. Keep import freshness visible at the start of imported asset rows.
|
||||||
|
|
||||||
|
The generated prototype was requested with the current editor's real regions and actions, a simple greybox scene and flat retained-mode controls. The visual hierarchy, palette and density are the reference; the repository's UI code and authoring contract determine functionality.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@@ -97,15 +97,16 @@ Read the fresh value and retry; the stale edit does not silently replace the new
|
|||||||
|
|
||||||
## Refresh C++ metadata
|
## Refresh C++ metadata
|
||||||
|
|
||||||
**Build C++ !** and the status message indicate stale gameplay metadata, for example
|
The status message indicates stale gameplay metadata, for example after changing
|
||||||
after changing gameplay sources or after a failed build. Choose **Build C++**, then
|
gameplay sources or after a failed build. Hover over **Build** for details, then
|
||||||
check **Jobs** and **Console**. A successful build and schema export refresh the
|
choose **Build** and check **Jobs** and **Console**. A successful build and schema
|
||||||
Inspector. A failed build retains the previous metadata and reports the failure.
|
export refresh the Inspector. A failed build retains the previous metadata and
|
||||||
|
reports the failure.
|
||||||
|
|
||||||
A missing schema or unsupported component version appears as read-only raw fields
|
A missing schema or unsupported component version appears as read-only raw fields
|
||||||
with **Copy raw fields**. Restore a missing module to make its schema available.
|
with **Copy raw fields**. Restore a missing module to make its schema available.
|
||||||
For an older component, declare [data migration rules](../scripting/api.md#editor-data-migrations),
|
For an older component, declare [data migration rules](../scripting/api.md#editor-data-migrations),
|
||||||
choose **Build C++**, then **Migrate to v…** in the Inspector. This is one undoable
|
choose **Build**, then **Migrate to v…** in the Inspector. This is one undoable
|
||||||
authoring edit; save explicitly afterward. Inherited components show **Open source
|
authoring edit; save explicitly afterward. Inherited components show **Open source
|
||||||
to migrate** instead. Top-level local additions migrate in their owning instance.
|
to migrate** instead. Top-level local additions migrate in their owning instance.
|
||||||
Missing rules or conversion errors preserve the data and appear in Console.
|
Missing rules or conversion errors preserve the data and appear in Console.
|
||||||
@@ -151,6 +152,11 @@ messages and diagnostics. **Jobs** shows build/import/export progress and cancel
|
|||||||
drag panel dividers to resize the Scene, Inspector, and bottom areas. These choices
|
drag panel dividers to resize the Scene, Inspector, and bottom areas. These choices
|
||||||
are stored per project in `.faset/editor-layout.json`.
|
are stored per project in `.faset/editor-layout.json`.
|
||||||
|
|
||||||
|
File rows put the filename before its directory; hover to see the full project-relative
|
||||||
|
path. Long generated hexadecimal filenames are shortened in the list, while the
|
||||||
|
path and underlying asset identity remain intact. The search field filters by the
|
||||||
|
full project-relative path.
|
||||||
|
|
||||||
The current layout supports these panel sizes and bottom-tab ordering. It does not
|
The current layout supports these panel sizes and bottom-tab ordering. It does not
|
||||||
provide floating panels or multiple Editor windows. Theme and base layout JSON live
|
provide floating panels or multiple Editor windows. Theme and base layout JSON live
|
||||||
in the engine's `assets/ui/dark.json` and `assets/ui/editor-layout.json`. The Editor
|
in the engine's `assets/ui/dark.json` and `assets/ui/editor-layout.json`. The Editor
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class Session {
|
|||||||
bool playing() const {
|
bool playing() const {
|
||||||
return bool(player_);
|
return bool(player_);
|
||||||
}
|
}
|
||||||
|
bool play_pending() const {
|
||||||
|
return !pending_play_job_.empty();
|
||||||
|
}
|
||||||
void log(std::string message);
|
void log(std::string message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
+10
-6
@@ -18,10 +18,11 @@ struct Rect {
|
|||||||
Rect intersection(const Rect&) const noexcept;
|
Rect intersection(const Rect&) const noexcept;
|
||||||
};
|
};
|
||||||
struct Theme {
|
struct Theme {
|
||||||
Color background{.075f, .078f, .086f, 1}, surface{.10f, .105f, .115f, 1};
|
Color background{.078f, .078f, .078f, 1}, surface{.110f, .110f, .114f, 1};
|
||||||
Color raised{.135f, .14f, .15f, 1}, hover{.175f, .18f, .20f, 1}, border{.23f, .235f, .255f, 1};
|
Color raised{.153f, .153f, .161f, 1}, hover{.196f, .196f, .204f, 1};
|
||||||
Color text{.86f, .875f, .90f, 1}, muted{.55f, .575f, .62f, 1}, accent{.65f, .60f, .88f, 1};
|
Color border{.227f, .227f, .239f, 1}, text{.878f, .878f, .886f, 1};
|
||||||
Color selection{.26f, .245f, .35f, 1}, danger{.92f, .39f, .38f, 1};
|
Color muted{.604f, .604f, .624f, 1}, accent{.659f, .627f, .761f, 1};
|
||||||
|
Color selection{.220f, .212f, .251f, 1}, danger{.835f, .431f, .420f, 1};
|
||||||
float font_size = 14, row_height = 28, padding = 8, gap = 4;
|
float font_size = 14, row_height = 28, padding = 8, gap = 4;
|
||||||
static Theme from_json(const Json&);
|
static Theme from_json(const Json&);
|
||||||
static Theme load(const std::filesystem::path&);
|
static Theme load(const std::filesystem::path&);
|
||||||
@@ -103,15 +104,18 @@ enum class Kind {
|
|||||||
Divider,
|
Divider,
|
||||||
Viewport
|
Viewport
|
||||||
};
|
};
|
||||||
|
// Visual emphasis is independent of widget behavior and stable identity.
|
||||||
|
enum class Appearance { Default, Quiet, Primary, Section };
|
||||||
struct Layout {
|
struct Layout {
|
||||||
float width = -1, height = -1, flex = 0;
|
float width = -1, height = -1, flex = 0;
|
||||||
float min_width = 0, min_height = 0, max_width = 100000, max_height = 100000;
|
float min_width = 0, min_height = 0, max_width = 100000, max_height = 100000;
|
||||||
float padding = 0, gap = 4;
|
float padding = 0, gap = 4;
|
||||||
bool absolute = false, scroll = false, clip = true;
|
bool absolute = false, scroll = false, clip = true, stack_vertical = false;
|
||||||
float x = 0, y = 0;
|
float x = 0, y = 0;
|
||||||
};
|
};
|
||||||
struct Widget {
|
struct Widget {
|
||||||
Kind kind = Kind::Panel;
|
Kind kind = Kind::Panel;
|
||||||
|
Appearance appearance = Appearance::Default;
|
||||||
std::string id, text;
|
std::string id, text;
|
||||||
Layout layout;
|
Layout layout;
|
||||||
Rect rect, clip;
|
Rect rect, clip;
|
||||||
@@ -120,7 +124,7 @@ struct Widget {
|
|||||||
int precision = 3, indent = 0;
|
int precision = 3, indent = 0;
|
||||||
float font_size = 0; // Zero inherits the theme typography.
|
float font_size = 0; // Zero inherits the theme typography.
|
||||||
float scroll_y = 0, content_height = 0;
|
float scroll_y = 0, content_height = 0;
|
||||||
std::string error, tooltip;
|
std::string error, tooltip, placeholder;
|
||||||
Json drag_payload;
|
Json drag_payload;
|
||||||
std::string dock_area, dock_panel;
|
std::string dock_area, dock_panel;
|
||||||
std::function<void(Widget&)> on_click, on_preview, on_commit, on_cancel;
|
std::function<void(Widget&)> on_click, on_preview, on_commit, on_cancel;
|
||||||
|
|||||||
+217
-88
@@ -239,9 +239,9 @@ struct EditorUI::Impl {
|
|||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
session.log(std::string("Layout reset: ") + e.what());
|
session.log(std::string("Layout reset: ") + e.what());
|
||||||
}
|
}
|
||||||
ui.find("scene_panel")->layout.width = dock.size("scene", 224);
|
ui.find("scene_panel")->layout.width = dock.size("scene", 238);
|
||||||
ui.find("inspector_panel")->layout.width = dock.size("inspector", 300);
|
ui.find("inspector_panel")->layout.width = dock.size("inspector", 312);
|
||||||
ui.find("bottom_panel")->layout.height = dock.size("bottom", 184);
|
ui.find("bottom_panel")->layout.height = dock.size("bottom", 190);
|
||||||
ui.set_clipboard([this] { return renderer.clipboard(); },
|
ui.set_clipboard([this] { return renderer.clipboard(); },
|
||||||
[this](const std::string& text) { renderer.set_clipboard(text); });
|
[this](const std::string& text) { renderer.set_clipboard(text); });
|
||||||
ui.set_ime([this](bool enabled) { renderer.set_text_input(enabled); },
|
ui.set_ime([this](bool enabled) { renderer.set_text_input(enabled); },
|
||||||
@@ -404,58 +404,88 @@ struct EditorUI::Impl {
|
|||||||
}
|
}
|
||||||
void build_static() {
|
void build_static() {
|
||||||
auto& menurow = ui.find("menubar")->add(Kind::Row, "menuitems");
|
auto& menurow = ui.find("menubar")->add(Kind::Row, "menuitems");
|
||||||
menurow.layout.gap = 2;
|
menurow.layout.gap = 0;
|
||||||
for (const std::string name : {"Faset", "File", "Edit", "Scene", "View", "Help"})
|
for (const std::string name : {"Faset", "File", "Edit", "Scene", "View", "Help"}) {
|
||||||
button(
|
auto& item = button(
|
||||||
menurow, "menu-" + name, name, [this, name] { menu = menu == name ? "" : name; },
|
menurow, "menu-" + name, name, [this, name] { menu = menu == name ? "" : name; },
|
||||||
name == "Faset" ? 72 : 54);
|
name == "Faset" ? 66 : name == "Scene" ? 58 : 50);
|
||||||
|
item.appearance = ui::Appearance::Quiet;
|
||||||
|
if (name == "Faset")
|
||||||
|
item.font_size = 15;
|
||||||
|
}
|
||||||
|
menurow.add(Kind::Label, "menu-space").layout.flex = 1;
|
||||||
auto& project = menurow.add(Kind::Label, "project-title",
|
auto& project = menurow.add(Kind::Label, "project-title",
|
||||||
session.project().value("name", std::string("Project")));
|
session.project().value("name", std::string("Project")));
|
||||||
project.layout.flex = 1;
|
project.layout.width = 350;
|
||||||
|
project.font_size = 12;
|
||||||
|
project.enabled = false;
|
||||||
auto& toolbar = ui.find("toolbar")->add(Kind::Row, "tools");
|
auto& toolbar = ui.find("toolbar")->add(Kind::Row, "tools");
|
||||||
toolbar.layout.gap = 5;
|
toolbar.layout.gap = 3;
|
||||||
button(toolbar, "save", "Save", [this] { save(); }, 60);
|
auto& file_tools = toolbar.add(Kind::Row, "file-tools");
|
||||||
button(toolbar, "undo", "Undo", [this] { history(false); }, 56);
|
file_tools.layout.width = 180;
|
||||||
button(toolbar, "redo", "Redo", [this] { history(true); }, 56);
|
file_tools.layout.gap = 3;
|
||||||
|
button(file_tools, "save", "Save", [this] { save(); }, 58).appearance =
|
||||||
|
ui::Appearance::Quiet;
|
||||||
|
button(file_tools, "undo", "Undo", [this] { history(false); }, 56).appearance =
|
||||||
|
ui::Appearance::Quiet;
|
||||||
|
button(file_tools, "redo", "Redo", [this] { history(true); }, 56).appearance =
|
||||||
|
ui::Appearance::Quiet;
|
||||||
|
toolbar.add(Kind::Label, "left-tools-space").layout.flex = 1;
|
||||||
|
auto& play_tools = toolbar.add(Kind::Row, "play-tools");
|
||||||
|
play_tools.layout.width = 265;
|
||||||
|
play_tools.layout.gap = 4;
|
||||||
|
button(play_tools, "play", "Play", [this] { call("faset_play", {{"document", document}}); },
|
||||||
|
66)
|
||||||
|
.appearance = ui::Appearance::Primary;
|
||||||
button(
|
button(
|
||||||
toolbar, "play", "Play", [this] { call("faset_play", {{"document", document}}); }, 58);
|
play_tools, "pause", "Pause",
|
||||||
button(
|
|
||||||
toolbar, "stop", "Stop",
|
|
||||||
[this] {
|
|
||||||
call("faset_stop");
|
|
||||||
paused = false;
|
|
||||||
},
|
|
||||||
56);
|
|
||||||
button(
|
|
||||||
toolbar, "pause", "Pause",
|
|
||||||
[this] {
|
[this] {
|
||||||
if (!call("faset_play_control", {{"command", paused ? "resume" : "pause"}})
|
if (!call("faset_play_control", {{"command", paused ? "resume" : "pause"}})
|
||||||
.is_null())
|
.is_null())
|
||||||
paused = !paused;
|
paused = !paused;
|
||||||
},
|
},
|
||||||
65);
|
68)
|
||||||
|
.appearance = ui::Appearance::Quiet;
|
||||||
button(
|
button(
|
||||||
toolbar, "step", "Step", [this] { call("faset_play_control", {{"command", "step"}}); },
|
play_tools, "stop", "Stop",
|
||||||
55);
|
[this] {
|
||||||
button(toolbar, "build", "Build", [this] { call("faset_build"); }, 94);
|
call("faset_stop");
|
||||||
|
paused = false;
|
||||||
|
},
|
||||||
|
60)
|
||||||
|
.appearance = ui::Appearance::Quiet;
|
||||||
button(
|
button(
|
||||||
toolbar, "export", "Export",
|
play_tools, "step", "Step",
|
||||||
[this] { call("faset_export", {{"document", document}, {"output", "Exports"}}); }, 64);
|
[this] { call("faset_play_control", {{"command", "step"}}); }, 56)
|
||||||
|
.appearance = ui::Appearance::Quiet;
|
||||||
|
toolbar.add(Kind::Label, "right-tools-space").layout.flex = 1;
|
||||||
|
auto& build_tools = toolbar.add(Kind::Row, "build-tools");
|
||||||
|
build_tools.layout.width = 354;
|
||||||
|
build_tools.layout.gap = 3;
|
||||||
|
button(build_tools, "build", "Build", [this] { call("faset_build"); }, 76).appearance =
|
||||||
|
ui::Appearance::Quiet;
|
||||||
button(
|
button(
|
||||||
toolbar, "simulation-settings", "Simulation",
|
build_tools, "export", "Export",
|
||||||
[this] { simulation_open = !simulation_open; }, 96);
|
[this] { call("faset_export", {{"document", document}, {"output", "Exports"}}); }, 72)
|
||||||
auto& space = toolbar.add(Kind::Label, "toolbar-space", "");
|
.appearance = ui::Appearance::Quiet;
|
||||||
space.layout.flex = 1;
|
button(
|
||||||
button(toolbar, "command-palette", "Commands", [this] { palette = !palette; }, 104);
|
build_tools, "simulation-settings", "Simulation",
|
||||||
|
[this] { simulation_open = !simulation_open; }, 98)
|
||||||
|
.appearance = ui::Appearance::Quiet;
|
||||||
|
button(build_tools, "command-palette", "Commands", [this] { palette = !palette; }, 99)
|
||||||
|
.appearance = ui::Appearance::Quiet;
|
||||||
auto& scene = *ui.find("scene_panel");
|
auto& scene = *ui.find("scene_panel");
|
||||||
scene.add(Kind::Tab, "scene-tab", "Scene").selected = true;
|
scene.add(Kind::Tab, "scene-tab", "Scene").selected = true;
|
||||||
auto& tools = scene.add(Kind::Row, "scene-tools");
|
auto& tools = scene.add(Kind::Row, "scene-tools");
|
||||||
tools.layout.height = 28;
|
tools.layout.height = 32;
|
||||||
tools.layout.padding = 3;
|
tools.layout.padding = 3;
|
||||||
tools.layout.gap = 3;
|
tools.layout.gap = 3;
|
||||||
button(tools, "add-object", "+ Object", [this] { create_object("Object"); }, 80);
|
button(tools, "add-object", "+ Object", [this] { create_object("Object"); }, 80)
|
||||||
button(tools, "add-cube", "Cube", [this] { create_object("Cube"); }, 55);
|
.appearance = ui::Appearance::Quiet;
|
||||||
button(tools, "add-sprite", "Sprite", [this] { create_object("Sprite"); }, 58);
|
button(tools, "add-cube", "Cube", [this] { create_object("Cube"); }, 55).appearance =
|
||||||
|
ui::Appearance::Quiet;
|
||||||
|
button(tools, "add-sprite", "Sprite", [this] { create_object("Sprite"); }, 58).appearance =
|
||||||
|
ui::Appearance::Quiet;
|
||||||
auto& tree = scene.add(Kind::Column, "scene-tree");
|
auto& tree = scene.add(Kind::Column, "scene-tree");
|
||||||
tree.layout.flex = 1;
|
tree.layout.flex = 1;
|
||||||
tree.layout.scroll = true;
|
tree.layout.scroll = true;
|
||||||
@@ -469,26 +499,32 @@ struct EditorUI::Impl {
|
|||||||
auto& body = inspector.add(Kind::Column, "properties");
|
auto& body = inspector.add(Kind::Column, "properties");
|
||||||
body.layout.flex = 1;
|
body.layout.flex = 1;
|
||||||
body.layout.padding = 10;
|
body.layout.padding = 10;
|
||||||
body.layout.gap = 7;
|
body.layout.gap = 6;
|
||||||
body.layout.scroll = true;
|
body.layout.scroll = true;
|
||||||
auto& vp = *ui.find("viewport");
|
auto& vp = *ui.find("viewport");
|
||||||
auto& vptools = vp.add(Kind::Row, "viewport-tools");
|
auto& tool_surface = vp.add(Kind::Panel, "viewport-tool-surface");
|
||||||
vptools.layout.absolute = true;
|
tool_surface.layout.absolute = true;
|
||||||
vptools.layout.x = 8;
|
tool_surface.layout.x = 8;
|
||||||
vptools.layout.y = 8;
|
tool_surface.layout.y = 8;
|
||||||
|
tool_surface.layout.width = 352;
|
||||||
|
tool_surface.layout.height = 36;
|
||||||
|
tool_surface.layout.padding = 4;
|
||||||
|
auto& vptools = tool_surface.add(Kind::Row, "viewport-tools");
|
||||||
vptools.layout.height = 28;
|
vptools.layout.height = 28;
|
||||||
vptools.layout.width = 300;
|
vptools.layout.gap = 4;
|
||||||
for (const std::string mode : {"Move", "Rotate", "Scale"})
|
for (const std::string mode : {"Move", "Rotate", "Scale"})
|
||||||
button(vptools, "gizmo-" + mode, mode, [this, mode] { gizmo_mode = mode; }, 64);
|
button(vptools, "gizmo-" + mode, mode, [this, mode] { gizmo_mode = mode; }, 64)
|
||||||
|
.appearance = ui::Appearance::Quiet;
|
||||||
button(
|
button(
|
||||||
vptools, "back-document", "Back",
|
vptools, "back-document", "Back",
|
||||||
[this] {
|
[this] {
|
||||||
if (!last_document.empty())
|
if (!last_document.empty())
|
||||||
choose_document(last_document);
|
choose_document(last_document);
|
||||||
},
|
},
|
||||||
54);
|
54)
|
||||||
vptools.layout.width = 360;
|
.appearance = ui::Appearance::Quiet;
|
||||||
button(vptools, "frame-selection", "Frame", [this] { frame_selection(); }, 64);
|
button(vptools, "frame-selection", "Frame", [this] { frame_selection(); }, 64)
|
||||||
|
.appearance = ui::Appearance::Quiet;
|
||||||
vp.on_drop = [this](Widget&, const Json& data) {
|
vp.on_drop = [this](Widget&, const Json& data) {
|
||||||
if (data.value("kind", std::string()) == "asset")
|
if (data.value("kind", std::string()) == "asset")
|
||||||
instantiate_asset(data.at("id").get<std::string>());
|
instantiate_asset(data.at("id").get<std::string>());
|
||||||
@@ -509,12 +545,15 @@ struct EditorUI::Impl {
|
|||||||
tab.on_click = [this, id](Widget&) { active_bottom = id; };
|
tab.on_click = [this, id](Widget&) { active_bottom = id; };
|
||||||
}
|
}
|
||||||
auto& assetbar = bottom.add(Kind::Row, "asset-toolbar");
|
auto& assetbar = bottom.add(Kind::Row, "asset-toolbar");
|
||||||
assetbar.layout.height = 30;
|
assetbar.layout.height = 32;
|
||||||
assetbar.layout.padding = 2;
|
assetbar.layout.padding = 3;
|
||||||
assetbar.layout.gap = 5;
|
assetbar.layout.gap = 5;
|
||||||
label(assetbar, "asset-path", "Project files", 135);
|
label(assetbar, "asset-path", "Project files", 102);
|
||||||
|
assetbar.find("asset-path")->enabled = false;
|
||||||
auto& search = assetbar.add(Kind::TextField, "asset-search", "");
|
auto& search = assetbar.add(Kind::TextField, "asset-search", "");
|
||||||
search.layout.width = 220;
|
search.layout.width = 220;
|
||||||
|
search.placeholder = "Search project files";
|
||||||
|
search.tooltip = "Filter files by project-relative path";
|
||||||
search.on_preview = [this](Widget& w) {
|
search.on_preview = [this](Widget& w) {
|
||||||
asset_filter = w.text;
|
asset_filter = w.text;
|
||||||
assets_dirty = true;
|
assets_dirty = true;
|
||||||
@@ -525,8 +564,10 @@ struct EditorUI::Impl {
|
|||||||
if (!source_file.empty())
|
if (!source_file.empty())
|
||||||
call("faset_import", {{"path", source_file}});
|
call("faset_import", {{"path", source_file}});
|
||||||
},
|
},
|
||||||
146);
|
146)
|
||||||
button(assetbar, "asset-open", "Open Scene", [this] { open_source(); }, 104);
|
.appearance = ui::Appearance::Quiet;
|
||||||
|
button(assetbar, "asset-open", "Open Scene", [this] { open_source(); }, 104)
|
||||||
|
.appearance = ui::Appearance::Quiet;
|
||||||
button(
|
button(
|
||||||
assetbar, "asset-refresh", "Refresh",
|
assetbar, "asset-refresh", "Refresh",
|
||||||
[this] {
|
[this] {
|
||||||
@@ -534,7 +575,8 @@ struct EditorUI::Impl {
|
|||||||
view.clearCache();
|
view.clearCache();
|
||||||
picks_key.clear();
|
picks_key.clear();
|
||||||
},
|
},
|
||||||
75);
|
75)
|
||||||
|
.appearance = ui::Appearance::Quiet;
|
||||||
auto& items = bottom.add(Kind::Column, "asset-items");
|
auto& items = bottom.add(Kind::Column, "asset-items");
|
||||||
items.layout.flex = 1;
|
items.layout.flex = 1;
|
||||||
items.layout.scroll = true;
|
items.layout.scroll = true;
|
||||||
@@ -554,7 +596,13 @@ struct EditorUI::Impl {
|
|||||||
auto& statusrow = ui.find("statusbar")->add(Kind::Row, "status-row");
|
auto& statusrow = ui.find("statusbar")->add(Kind::Row, "status-row");
|
||||||
auto& statuslabel = statusrow.add(Kind::Label, "status", "Ready");
|
auto& statuslabel = statusrow.add(Kind::Label, "status", "Ready");
|
||||||
statuslabel.layout.flex = 1;
|
statuslabel.layout.flex = 1;
|
||||||
|
auto& schema_status = statusrow.add(Kind::Label, "schema-status");
|
||||||
|
schema_status.layout.width = 220;
|
||||||
|
schema_status.visible = false;
|
||||||
|
schema_status.enabled = false;
|
||||||
|
schema_status.tooltip = "Choose Build to refresh gameplay fields in Inspector";
|
||||||
label(statusrow, "renderer-status", "Vulkan", 225);
|
label(statusrow, "renderer-status", "Vulkan", 225);
|
||||||
|
statusrow.find("renderer-status")->enabled = false;
|
||||||
build_overlays();
|
build_overlays();
|
||||||
}
|
}
|
||||||
void build_overlays() {
|
void build_overlays() {
|
||||||
@@ -1173,8 +1221,13 @@ struct EditorUI::Impl {
|
|||||||
ui.find("undo")->enabled = current.value("can_undo", false);
|
ui.find("undo")->enabled = current.value("can_undo", false);
|
||||||
ui.find("redo")->enabled = current.value("can_redo", false);
|
ui.find("redo")->enabled = current.value("can_redo", false);
|
||||||
ui.find("pause")->enabled = session.playing();
|
ui.find("pause")->enabled = session.playing();
|
||||||
|
ui.find("stop")->enabled = session.playing() || session.play_pending();
|
||||||
ui.find("step")->enabled = session.playing();
|
ui.find("step")->enabled = session.playing();
|
||||||
ui.find("pause")->text = paused ? "Resume" : "Pause";
|
ui.find("pause")->text = paused ? "Resume" : "Pause";
|
||||||
|
for (const std::string mode : {"Move", "Rotate", "Scale"})
|
||||||
|
ui.find("gizmo-" + mode)->selected = gizmo_mode == mode;
|
||||||
|
for (const std::string name : {"Faset", "File", "Edit", "Scene", "View", "Help"})
|
||||||
|
ui.find("menu-" + name)->selected = menu == name;
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
// Source fingerprints read complete script bytes; do not hash them every render frame.
|
// Source fingerprints read complete script bytes; do not hash them every render frame.
|
||||||
if (schema_state.is_null() || now - last_schema_poll >= std::chrono::seconds(1)) {
|
if (schema_state.is_null() || now - last_schema_poll >= std::chrono::seconds(1)) {
|
||||||
@@ -1183,17 +1236,27 @@ struct EditorUI::Impl {
|
|||||||
}
|
}
|
||||||
if (!schema_state.is_null()) {
|
if (!schema_state.is_null()) {
|
||||||
const bool stale = schema_state.value("stale", false);
|
const bool stale = schema_state.value("stale", false);
|
||||||
ui.find("build")->text = stale ? "Build !" : "Build";
|
auto& build = *ui.find("build");
|
||||||
ui.find("build")->tooltip =
|
build.text = "Build";
|
||||||
stale ? "Gameplay schema is stale: " + schema_state.value("error", std::string())
|
const auto schema_error = schema_state.value("error", std::string());
|
||||||
: "Incremental gameplay build and C++/Lua Inspector metadata refresh";
|
build.tooltip = stale ? "Gameplay metadata is out of date. Build to refresh Inspector "
|
||||||
if (stale && status == "Ready")
|
"fields."
|
||||||
status = "Gameplay schema is stale; Build to refresh Inspector metadata";
|
: "Build gameplay code and refresh Inspector metadata";
|
||||||
|
if (stale && !schema_error.empty())
|
||||||
|
build.tooltip += " Last error: " + schema_error;
|
||||||
|
ui.find("schema-status")->visible = stale;
|
||||||
|
ui.find("schema-status")->text = "Gameplay metadata out of date";
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.find("project-title")->text = session.project().value("name", std::string("Project")) +
|
const auto project_name = session.project().value("name", std::string("Project"));
|
||||||
" / " + current.at("name").get<std::string>() +
|
const auto scene_name = current.at("name").get<std::string>();
|
||||||
|
const auto scene_path = current.value("path", std::string());
|
||||||
|
const auto scene_label = scene_path.empty()
|
||||||
|
? scene_name
|
||||||
|
: path_to_utf8(path_from_utf8(scene_path).filename());
|
||||||
|
ui.find("project-title")->text = project_name + " / " + scene_label +
|
||||||
(current.value("dirty", false) ? " *" : "");
|
(current.value("dirty", false) ? " *" : "");
|
||||||
|
ui.find("project-title")->tooltip = project_name + " / " + scene_name;
|
||||||
ui.find("status")->text = status;
|
ui.find("status")->text = status;
|
||||||
ui.find("renderer-status")->text =
|
ui.find("renderer-status")->text =
|
||||||
"Vulkan 1.3 | " + std::to_string(resolved.at("entities").size()) + " objects";
|
"Vulkan 1.3 | " + std::to_string(resolved.at("entities").size()) + " objects";
|
||||||
@@ -1207,7 +1270,16 @@ struct EditorUI::Impl {
|
|||||||
keep.insert(row.id);
|
keep.insert(row.id);
|
||||||
order[row.id] = sequence++;
|
order[row.id] = sequence++;
|
||||||
};
|
};
|
||||||
auto& root = tree.add(Kind::TreeRow, "scene-root", current.at("scene").at("name"));
|
const auto full_scene_name = current.at("scene").at("name").get<std::string>();
|
||||||
|
auto scene_name = full_scene_name;
|
||||||
|
if (ui.font().measure(scene_name, ui.theme().font_size) >
|
||||||
|
ui.find("scene_panel")->layout.width - 18) {
|
||||||
|
const auto path = current.value("path", std::string());
|
||||||
|
if (!path.empty())
|
||||||
|
scene_name = path_to_utf8(path_from_utf8(path).filename());
|
||||||
|
}
|
||||||
|
auto& root = tree.add(Kind::TreeRow, "scene-root", scene_name);
|
||||||
|
root.tooltip = full_scene_name;
|
||||||
root.selected = selected.empty() && instance_selection.empty();
|
root.selected = selected.empty() && instance_selection.empty();
|
||||||
root.on_click = [this](Widget&) { select(""); };
|
root.on_click = [this](Widget&) { select(""); };
|
||||||
touch(root);
|
touch(root);
|
||||||
@@ -1400,6 +1472,11 @@ struct EditorUI::Impl {
|
|||||||
void refresh_inspector() {
|
void refresh_inspector() {
|
||||||
auto& body = *ui.find("properties");
|
auto& body = *ui.find("properties");
|
||||||
std::set<std::string> keep;
|
std::set<std::string> keep;
|
||||||
|
std::vector<std::string> ordered;
|
||||||
|
const auto keep_in_order = [&](const std::string& id) {
|
||||||
|
keep.insert(id);
|
||||||
|
ordered.push_back(id);
|
||||||
|
};
|
||||||
const auto* e = entity(resolved, selected);
|
const auto* e = entity(resolved, selected);
|
||||||
if (!instance_selection.empty()) {
|
if (!instance_selection.empty()) {
|
||||||
refresh_instance_inspector(body, keep);
|
refresh_instance_inspector(body, keep);
|
||||||
@@ -1407,29 +1484,36 @@ struct EditorUI::Impl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!e) {
|
if (!e) {
|
||||||
label(body, "inspector-empty", "Select an object to edit its components");
|
label(body, "inspector-empty", "Select an object in Scene to edit it");
|
||||||
|
body.find("inspector-empty")->enabled = false;
|
||||||
keep.insert("inspector-empty");
|
keep.insert("inspector-empty");
|
||||||
trim_children(body, keep);
|
trim_children(body, keep);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto& name = body.add(Kind::TextField, "object-name");
|
auto& identity = body.add(Kind::Row, "object-identity");
|
||||||
|
keep_in_order(identity.id);
|
||||||
|
identity.layout.height = 30;
|
||||||
|
identity.layout.gap = 4;
|
||||||
|
label(identity, "object-name-label", "Name", 90);
|
||||||
|
identity.find("object-name-label")->enabled = false;
|
||||||
|
auto& name = identity.add(Kind::TextField, "object-name");
|
||||||
|
name.layout.flex = 1;
|
||||||
ui.update_text(name.id, e->at("name"));
|
ui.update_text(name.id, e->at("name"));
|
||||||
const bool source_object = inherited(*e), local_addition = owned_addition(*e);
|
const bool source_object = inherited(*e), local_addition = owned_addition(*e);
|
||||||
name.enabled = !source_object || local_addition;
|
name.enabled = !source_object || local_addition;
|
||||||
if (local_addition) {
|
if (local_addition) {
|
||||||
label(body, "object-origin", "Local addition to this instance");
|
label(body, "object-origin", "Local addition to this instance");
|
||||||
keep.insert("object-origin");
|
keep_in_order("object-origin");
|
||||||
} else if (source_object) {
|
} else if (source_object) {
|
||||||
const auto path = e->at("origin").at("path");
|
const auto path = e->at("origin").at("path");
|
||||||
const auto object = e->at("origin").at("object").get<std::string>();
|
const auto object = e->at("origin").at("object").get<std::string>();
|
||||||
label(body, "object-origin",
|
label(body, "object-origin",
|
||||||
"Source: " + path_to_utf8(path_from_utf8(instance_source(path)).filename()));
|
"Source: " + path_to_utf8(path_from_utf8(instance_source(path)).filename()));
|
||||||
keep.insert("object-origin");
|
keep_in_order("object-origin");
|
||||||
button(body, "object-open-source", "Open source",
|
button(body, "object-open-source", "Open source",
|
||||||
[this, path, object] { open_template_source(path, object); });
|
[this, path, object] { open_template_source(path, object); });
|
||||||
keep.insert("object-open-source");
|
keep_in_order("object-open-source");
|
||||||
}
|
}
|
||||||
keep.insert(name.id);
|
|
||||||
name.on_preview = [this](Widget&) {
|
name.on_preview = [this](Widget&) {
|
||||||
edit_revisions.try_emplace("object-name", current.at("revision").get<std::uint64_t>());
|
edit_revisions.try_emplace("object-name", current.at("revision").get<std::uint64_t>());
|
||||||
};
|
};
|
||||||
@@ -1462,17 +1546,20 @@ struct EditorUI::Impl {
|
|||||||
: " (schema missing)")},
|
: " (schema missing)")},
|
||||||
{"fields", Json::object()}};
|
{"fields", Json::object()}};
|
||||||
auto& header = body.add(Kind::Row, "component-header-" + cid);
|
auto& header = body.add(Kind::Row, "component-header-" + cid);
|
||||||
keep.insert(header.id);
|
keep_in_order(header.id);
|
||||||
header.layout.height = 28;
|
header.layout.height = 32;
|
||||||
|
header.appearance = ui::Appearance::Section;
|
||||||
auto& title =
|
auto& title =
|
||||||
header.add(Kind::Label, "component-title-" + cid, metadata.value("name", type));
|
header.add(Kind::Label, "component-title-" + cid, metadata.value("name", type));
|
||||||
title.layout.flex = 1;
|
title.layout.flex = 1;
|
||||||
|
title.font_size = 14;
|
||||||
button(
|
button(
|
||||||
header, "component-remove-" + cid, "x", [this, cid] { remove_component(cid); }, 25)
|
header, "component-remove-" + cid, "x", [this, cid] { remove_component(cid); }, 25)
|
||||||
.enabled = !source_object || local_addition;
|
.appearance = ui::Appearance::Quiet;
|
||||||
|
header.find("component-remove-" + cid)->enabled = !source_object || local_addition;
|
||||||
if (!known) {
|
if (!known) {
|
||||||
label(body, "opaque-note-" + cid, "Schema unavailable. Data is preserved.");
|
label(body, "opaque-note-" + cid, "Schema unavailable. Data is preserved.");
|
||||||
keep.insert("opaque-note-" + cid);
|
keep_in_order("opaque-note-" + cid);
|
||||||
if (has_schema &&
|
if (has_schema &&
|
||||||
c.value("version", 1) <
|
c.value("version", 1) <
|
||||||
session.authoring().schemas().schema(type).value("version", 1)) {
|
session.authoring().schemas().schema(type).value("version", 1)) {
|
||||||
@@ -1489,31 +1576,48 @@ struct EditorUI::Impl {
|
|||||||
[this, cid] { migrate_component(cid); });
|
[this, cid] { migrate_component(cid); });
|
||||||
action.tooltip =
|
action.tooltip =
|
||||||
"Apply the schema's declared migration rules. Errors keep all stored data.";
|
"Apply the schema's declared migration rules. Errors keep all stored data.";
|
||||||
keep.insert(action_id);
|
keep_in_order(action_id);
|
||||||
}
|
}
|
||||||
auto& raw =
|
auto& raw =
|
||||||
body.add(Kind::TextField, "opaque-fields-" + cid, c.at("fields").dump());
|
body.add(Kind::TextField, "opaque-fields-" + cid, c.at("fields").dump());
|
||||||
raw.enabled = false;
|
raw.enabled = false;
|
||||||
keep.insert(raw.id);
|
keep_in_order(raw.id);
|
||||||
button(body, "opaque-copy-" + cid, "Copy raw fields",
|
button(body, "opaque-copy-" + cid, "Copy raw fields",
|
||||||
[this, fields = c.at("fields")] { renderer.set_clipboard(fields.dump(2)); });
|
[this, fields = c.at("fields")] { renderer.set_clipboard(fields.dump(2)); });
|
||||||
keep.insert("opaque-copy-" + cid);
|
keep_in_order("opaque-copy-" + cid);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const auto& [fid, value] : c.at("fields").items()) {
|
for (const auto& [fid, value] : c.at("fields").items()) {
|
||||||
const auto descriptor = metadata.at("fields").value(fid, Json::object());
|
const auto descriptor = metadata.at("fields").value(fid, Json::object());
|
||||||
const auto key = "field-" + cid + "-" + fid;
|
const auto key = "field-" + cid + "-" + fid;
|
||||||
auto& row = body.add(Kind::Column, key);
|
auto& row = body.add(Kind::Row, key);
|
||||||
keep.insert(key);
|
keep_in_order(key);
|
||||||
row.layout.gap = 2;
|
const bool radians = descriptor.value("unit", std::string()) == "radians";
|
||||||
label(row, key + "-label",
|
|
||||||
descriptor.value("name", fid) +
|
|
||||||
(descriptor.value("unit", std::string()) == "radians" ? " (rad)" : ""));
|
|
||||||
const auto& options = descriptor.value("enum", Json::array());
|
const auto& options = descriptor.value("enum", Json::array());
|
||||||
const bool vector_value = value.is_array() && value.size() >= 2 &&
|
const bool vector_value = value.is_array() && value.size() >= 2 &&
|
||||||
value.size() <= 4 &&
|
value.size() <= 4 &&
|
||||||
std::all_of(value.begin(), value.end(),
|
std::all_of(value.begin(), value.end(),
|
||||||
[](const Json& v) { return v.is_number(); });
|
[](const Json& v) { return v.is_number(); });
|
||||||
|
const float label_width = radians ? 108.f : 90.f;
|
||||||
|
const float field_width = vector_value
|
||||||
|
? float(value.size()) * 44.f +
|
||||||
|
float(value.size() - 1) * 3.f
|
||||||
|
: 72.f;
|
||||||
|
const float available_width =
|
||||||
|
ui.find("inspector_panel")->layout.width - 2.f * body.layout.padding;
|
||||||
|
const bool stacked =
|
||||||
|
available_width < label_width + 4.f + field_width +
|
||||||
|
(source_object ? 61.f : 0.f);
|
||||||
|
row.layout.stack_vertical = stacked;
|
||||||
|
row.layout.height = stacked ? (source_object ? 72.f : 48.f) : 28.f;
|
||||||
|
row.layout.gap = stacked ? 2.f : 4.f;
|
||||||
|
label(row, key + "-label",
|
||||||
|
descriptor.value("name", fid) + (radians ? " (rad)" : ""),
|
||||||
|
stacked ? -1.f : label_width);
|
||||||
|
auto& field_label = *row.find(key + "-label");
|
||||||
|
field_label.layout.height = stacked ? 18.f : -1.f;
|
||||||
|
field_label.enabled = false;
|
||||||
|
field_label.tooltip = descriptor.value("name", fid);
|
||||||
const auto input_id = key + (vector_value ? "-vector" : "-value");
|
const auto input_id = key + (vector_value ? "-vector" : "-value");
|
||||||
const auto input_kind = !options.empty() ? Kind::Button
|
const auto input_kind = !options.empty() ? Kind::Button
|
||||||
: vector_value ? Kind::Row
|
: vector_value ? Kind::Row
|
||||||
@@ -1537,7 +1641,9 @@ struct EditorUI::Impl {
|
|||||||
{"address", address}}}));
|
{"address", address}}}));
|
||||||
});
|
});
|
||||||
revert.enabled = local_override;
|
revert.enabled = local_override;
|
||||||
revert.layout.height = 23;
|
revert.appearance = ui::Appearance::Quiet;
|
||||||
|
revert.layout.width = stacked ? -1.f : 57.f;
|
||||||
|
revert.layout.height = stacked ? 22.f : -1.f;
|
||||||
}
|
}
|
||||||
trim_children(row, source_object ? std::set<std::string>{key + "-label", input_id,
|
trim_children(row, source_object ? std::set<std::string>{key + "-label", input_id,
|
||||||
key + "-revert"}
|
key + "-revert"}
|
||||||
@@ -1554,14 +1660,16 @@ struct EditorUI::Impl {
|
|||||||
options.size();
|
options.size();
|
||||||
set_field(cid, fid, options[index], key + "-value");
|
set_field(cid, fid, options[index], key + "-value");
|
||||||
});
|
});
|
||||||
input.layout.height = 27;
|
input.layout.flex = 1;
|
||||||
} else if (value.is_boolean()) {
|
} else if (value.is_boolean()) {
|
||||||
auto& input = row.add(Kind::Checkbox, key + "-value", "Enabled");
|
auto& input = row.add(Kind::Checkbox, key + "-value", "Enabled");
|
||||||
|
input.layout.flex = 1;
|
||||||
input.checked = value;
|
input.checked = value;
|
||||||
bind_field(input, cid, fid, value);
|
bind_field(input, cid, fid, value);
|
||||||
} else if (vector_value) {
|
} else if (vector_value) {
|
||||||
auto& vectorrow = row.add(Kind::Row, key + "-vector");
|
auto& vectorrow = row.add(Kind::Row, key + "-vector");
|
||||||
vectorrow.layout.gap = 3;
|
vectorrow.layout.gap = 3;
|
||||||
|
vectorrow.layout.flex = 1;
|
||||||
std::set<std::string> axis_ids;
|
std::set<std::string> axis_ids;
|
||||||
for (std::size_t axis = 0; axis < value.size(); ++axis) {
|
for (std::size_t axis = 0; axis < value.size(); ++axis) {
|
||||||
auto& input =
|
auto& input =
|
||||||
@@ -1576,12 +1684,14 @@ struct EditorUI::Impl {
|
|||||||
trim_children(vectorrow, axis_ids);
|
trim_children(vectorrow, axis_ids);
|
||||||
} else if (value.is_number()) {
|
} else if (value.is_number()) {
|
||||||
auto& input = row.add(Kind::NumberField, key + "-value");
|
auto& input = row.add(Kind::NumberField, key + "-value");
|
||||||
|
input.layout.flex = 1;
|
||||||
input.step = value.is_number_integer() ? 1 : .05;
|
input.step = value.is_number_integer() ? 1 : .05;
|
||||||
input.precision = value.is_number_integer() ? 0 : 3;
|
input.precision = value.is_number_integer() ? 0 : 3;
|
||||||
ui.update_number(input.id, value.get<double>());
|
ui.update_number(input.id, value.get<double>());
|
||||||
bind_field(input, cid, fid, value);
|
bind_field(input, cid, fid, value);
|
||||||
} else {
|
} else {
|
||||||
auto& input = row.add(Kind::TextField, key + "-value");
|
auto& input = row.add(Kind::TextField, key + "-value");
|
||||||
|
input.layout.flex = 1;
|
||||||
ui.update_text(input.id,
|
ui.update_text(input.id,
|
||||||
value.is_string() ? value.get<std::string>() : value.dump());
|
value.is_string() ? value.get<std::string>() : value.dump());
|
||||||
bind_field(input, cid, fid, value);
|
bind_field(input, cid, fid, value);
|
||||||
@@ -1591,23 +1701,29 @@ struct EditorUI::Impl {
|
|||||||
auto& add = button(body, "add-component", "+ Add Component",
|
auto& add = button(body, "add-component", "+ Add Component",
|
||||||
[this] { component_menu = !component_menu; });
|
[this] { component_menu = !component_menu; });
|
||||||
add.enabled = !source_object || local_addition;
|
add.enabled = !source_object || local_addition;
|
||||||
keep.insert(add.id);
|
keep_in_order(add.id);
|
||||||
if (source_object) {
|
if (source_object) {
|
||||||
const auto path = e->at("origin").at("path");
|
const auto path = e->at("origin").at("path");
|
||||||
const auto object = e->at("origin").at("object").get<std::string>();
|
const auto object = e->at("origin").at("object").get<std::string>();
|
||||||
auto& child = button(body, "instance-add-child", "Add local child",
|
auto& child = button(body, "instance-add-child", "Add local child",
|
||||||
[this, path, object] { add_instance_child(path, object); });
|
[this, path, object] { add_instance_child(path, object); });
|
||||||
child.enabled = path.size() == 1;
|
child.enabled = path.size() == 1;
|
||||||
keep.insert(child.id);
|
keep_in_order(child.id);
|
||||||
}
|
}
|
||||||
if (component_menu && (!source_object || local_addition))
|
if (component_menu && (!source_object || local_addition))
|
||||||
for (const auto& schema : schemas.at("types")) {
|
for (const auto& schema : schemas.at("types")) {
|
||||||
const auto type = schema.at("id").get<std::string>();
|
const auto type = schema.at("id").get<std::string>();
|
||||||
auto& choice = button(body, "component-choice-" + type, schema.value("name", type),
|
auto& choice = button(body, "component-choice-" + type, schema.value("name", type),
|
||||||
[this, type] { add_component(type); });
|
[this, type] { add_component(type); });
|
||||||
keep.insert(choice.id);
|
keep_in_order(choice.id);
|
||||||
}
|
}
|
||||||
trim_children(body, keep);
|
trim_children(body, keep);
|
||||||
|
std::map<std::string, std::size_t> position;
|
||||||
|
for (std::size_t i = 0; i < ordered.size(); ++i)
|
||||||
|
position[ordered[i]] = i;
|
||||||
|
std::stable_sort(body.children.begin(), body.children.end(), [&](const auto& a, const auto& b) {
|
||||||
|
return position.at(a->id) < position.at(b->id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
void refresh_assets() {
|
void refresh_assets() {
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
@@ -1659,9 +1775,21 @@ struct EditorUI::Impl {
|
|||||||
std::set<std::string> keep;
|
std::set<std::string> keep;
|
||||||
for (const auto& file : files) {
|
for (const auto& file : files) {
|
||||||
const auto path = file.get<std::string>();
|
const auto path = file.get<std::string>();
|
||||||
auto& row = list.add(Kind::TreeRow, "file-" + path, path);
|
const auto file_path = path_from_utf8(path);
|
||||||
|
auto name = path_to_utf8(file_path.filename());
|
||||||
|
const auto stem = path_to_utf8(file_path.stem());
|
||||||
|
if (stem.size() > 24 &&
|
||||||
|
std::all_of(stem.begin(), stem.end(), [](unsigned char c) {
|
||||||
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
|
||||||
|
(c >= 'A' && c <= 'F');
|
||||||
|
}))
|
||||||
|
name = stem.substr(0, 10) + "…" + stem.substr(stem.size() - 6) +
|
||||||
|
path_to_utf8(file_path.extension());
|
||||||
|
const auto folder = generic_path_to_utf8(file_path.parent_path());
|
||||||
|
auto& row = list.add(Kind::TreeRow, "file-" + path, name + " / " + folder);
|
||||||
row.layout.height = 25;
|
row.layout.height = 25;
|
||||||
row.indent = 1;
|
row.indent = 1;
|
||||||
|
row.tooltip = path;
|
||||||
row.selected = source_file == path;
|
row.selected = source_file == path;
|
||||||
row.on_click = [this, path](Widget&) {
|
row.on_click = [this, path](Widget&) {
|
||||||
source_file = path;
|
source_file = path;
|
||||||
@@ -1681,9 +1809,10 @@ struct EditorUI::Impl {
|
|||||||
: state == "stale" ? "Stale"
|
: state == "stale" ? "Stale"
|
||||||
: "Unavailable";
|
: "Unavailable";
|
||||||
auto& row =
|
auto& row =
|
||||||
list.add(Kind::TreeRow, "asset-" + id, std::string(prefix) + " / " + name);
|
list.add(Kind::TreeRow, "asset-" + id, std::string(prefix) + " / " + name);
|
||||||
row.layout.height = 25;
|
row.layout.height = 25;
|
||||||
row.indent = 1;
|
row.indent = 1;
|
||||||
|
row.tooltip = "Asset " + id + " — " + state;
|
||||||
row.drag_payload = {{"kind", "asset"}, {"id", id}, {"label", name}};
|
row.drag_payload = {{"kind", "asset"}, {"id", id}, {"label", name}};
|
||||||
row.on_click = [this, id, asset, freshness, state](Widget&) {
|
row.on_click = [this, id, asset, freshness, state](Widget&) {
|
||||||
renderer.set_clipboard(id);
|
renderer.set_clipboard(id);
|
||||||
|
|||||||
+101
-13
@@ -1,4 +1,5 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <faset/core/io.hpp>
|
#include <faset/core/io.hpp>
|
||||||
#include <faset/ui/ui.hpp>
|
#include <faset/ui/ui.hpp>
|
||||||
@@ -108,6 +109,7 @@ Layout parse_layout(const Json& j, Layout l = {}) {
|
|||||||
l.absolute = j.value("absolute", l.absolute);
|
l.absolute = j.value("absolute", l.absolute);
|
||||||
l.scroll = j.value("scroll", l.scroll);
|
l.scroll = j.value("scroll", l.scroll);
|
||||||
l.clip = j.value("clip", l.clip);
|
l.clip = j.value("clip", l.clip);
|
||||||
|
l.stack_vertical = j.value("stack_vertical", l.stack_vertical);
|
||||||
if (l.flex < 0 || l.padding < 0 || l.gap < 0 || l.min_width < 0 || l.min_height < 0 ||
|
if (l.flex < 0 || l.padding < 0 || l.gap < 0 || l.min_width < 0 || l.min_height < 0 ||
|
||||||
l.max_width < l.min_width || l.max_height < l.min_height)
|
l.max_width < l.min_width || l.max_height < l.min_height)
|
||||||
throw std::runtime_error("Invalid layout constraints");
|
throw std::runtime_error("Invalid layout constraints");
|
||||||
@@ -243,6 +245,7 @@ struct Context::Impl {
|
|||||||
FontAtlas font;
|
FontAtlas font;
|
||||||
float width = 0, height = 0, scale = 1, mouse_x = 0, mouse_y = 0;
|
float width = 0, height = 0, scale = 1, mouse_x = 0, mouse_y = 0;
|
||||||
std::string focused, hovered, captured;
|
std::string focused, hovered, captured;
|
||||||
|
std::chrono::steady_clock::time_point hover_started = std::chrono::steady_clock::now();
|
||||||
struct Edit {
|
struct Edit {
|
||||||
TextBuffer buffer;
|
TextBuffer buffer;
|
||||||
std::string original, composition;
|
std::string original, composition;
|
||||||
@@ -285,18 +288,19 @@ struct Context::Impl {
|
|||||||
return 80 * scale;
|
return 80 * scale;
|
||||||
}
|
}
|
||||||
if (w.kind == Kind::Panel || w.kind == Kind::Column || w.kind == Kind::Row) {
|
if (w.kind == Kind::Panel || w.kind == Kind::Column || w.kind == Kind::Row) {
|
||||||
|
const bool horizontal = w.kind == Kind::Row && !w.layout.stack_vertical;
|
||||||
float total = 0;
|
float total = 0;
|
||||||
std::size_t count = 0;
|
std::size_t count = 0;
|
||||||
for (auto& child : w.children)
|
for (auto& child : w.children)
|
||||||
if (child->visible && !child->layout.absolute) {
|
if (child->visible && !child->layout.absolute) {
|
||||||
const auto value = intrinsic(*child, false);
|
const auto value = intrinsic(*child, false);
|
||||||
if (w.kind == Kind::Row)
|
if (horizontal)
|
||||||
total = std::max(total, value);
|
total = std::max(total, value);
|
||||||
else
|
else
|
||||||
total += value;
|
total += value;
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
if (w.kind != Kind::Row && count)
|
if (!horizontal && count)
|
||||||
total += float(count - 1) * w.layout.gap * scale;
|
total += float(count - 1) * w.layout.gap * scale;
|
||||||
return total + w.layout.padding * 2 * scale;
|
return total + w.layout.padding * 2 * scale;
|
||||||
}
|
}
|
||||||
@@ -310,7 +314,7 @@ struct Context::Impl {
|
|||||||
const auto pad = w.layout.padding * scale;
|
const auto pad = w.layout.padding * scale;
|
||||||
Rect inner{rect.x + pad, rect.y + pad, std::max(0.f, rect.width - 2 * pad),
|
Rect inner{rect.x + pad, rect.y + pad, std::max(0.f, rect.width - 2 * pad),
|
||||||
std::max(0.f, rect.height - 2 * pad)};
|
std::max(0.f, rect.height - 2 * pad)};
|
||||||
const bool horizontal = w.kind == Kind::Row;
|
const bool horizontal = w.kind == Kind::Row && !w.layout.stack_vertical;
|
||||||
const float available = horizontal ? inner.width : inner.height;
|
const float available = horizontal ? inner.width : inner.height;
|
||||||
std::vector<Widget*> flow;
|
std::vector<Widget*> flow;
|
||||||
float fixed = 0, flex = 0;
|
float fixed = 0, flex = 0;
|
||||||
@@ -557,17 +561,30 @@ struct Context::Impl {
|
|||||||
const auto& rect = w.rect;
|
const auto& rect = w.rect;
|
||||||
if (w.kind == Kind::Panel) {
|
if (w.kind == Kind::Panel) {
|
||||||
fill(frame, rect, theme.surface, w.clip);
|
fill(frame, rect, theme.surface, w.clip);
|
||||||
outline(frame, rect, theme.border, w.clip);
|
if (w.layout.absolute)
|
||||||
|
outline(frame, rect, theme.border, w.clip);
|
||||||
} else if (w.kind == Kind::Button) {
|
} else if (w.kind == Kind::Button) {
|
||||||
fill(frame, rect,
|
if (w.appearance == Appearance::Primary && w.enabled) {
|
||||||
hover && w.enabled ? theme.hover
|
fill(frame, rect, theme.accent, w.clip);
|
||||||
: w.selected ? theme.selection
|
ink = theme.background;
|
||||||
: theme.raised,
|
} else if (w.appearance == Appearance::Quiet) {
|
||||||
w.clip);
|
if (w.selected || (hover && w.enabled))
|
||||||
outline(frame, rect, focus ? theme.accent : theme.border, w.clip);
|
fill(frame, rect, w.selected ? theme.selection : theme.hover, w.clip);
|
||||||
|
} else {
|
||||||
|
fill(frame, rect,
|
||||||
|
hover && w.enabled ? theme.hover
|
||||||
|
: w.selected ? theme.selection
|
||||||
|
: theme.raised,
|
||||||
|
w.clip);
|
||||||
|
outline(frame, rect, focus ? theme.accent : theme.border, w.clip);
|
||||||
|
}
|
||||||
|
if (focus && w.appearance != Appearance::Default)
|
||||||
|
outline(frame, rect, theme.accent, w.clip);
|
||||||
} else if (w.kind == Kind::Tab || w.kind == Kind::TreeRow) {
|
} else if (w.kind == Kind::Tab || w.kind == Kind::TreeRow) {
|
||||||
if (w.selected || hover)
|
if (w.kind == Kind::TreeRow && (w.selected || hover))
|
||||||
fill(frame, rect, w.selected ? theme.selection : theme.hover, w.clip);
|
fill(frame, rect, w.selected ? theme.selection : theme.hover, w.clip);
|
||||||
|
else if (w.kind == Kind::Tab && hover && !w.selected)
|
||||||
|
fill(frame, rect, theme.hover, w.clip);
|
||||||
if (w.selected) {
|
if (w.selected) {
|
||||||
if (w.kind == Kind::Tab)
|
if (w.kind == Kind::Tab)
|
||||||
fill(frame, {rect.x, rect.y + rect.height - 2 * scale, rect.width, 2 * scale},
|
fill(frame, {rect.x, rect.y + rect.height - 2 * scale, rect.width, 2 * scale},
|
||||||
@@ -575,8 +592,12 @@ struct Context::Impl {
|
|||||||
else
|
else
|
||||||
fill(frame, {rect.x, rect.y, 2 * scale, rect.height}, theme.accent, w.clip);
|
fill(frame, {rect.x, rect.y, 2 * scale, rect.height}, theme.accent, w.clip);
|
||||||
}
|
}
|
||||||
|
if (w.kind == Kind::Tab && !w.selected)
|
||||||
|
ink = theme.muted;
|
||||||
if (focus)
|
if (focus)
|
||||||
outline(frame, rect, theme.accent, w.clip);
|
outline(frame, rect, theme.accent, w.clip);
|
||||||
|
} else if (w.kind == Kind::Row && w.appearance == Appearance::Section) {
|
||||||
|
fill(frame, {rect.x, rect.y, rect.width, scale}, theme.border, w.clip);
|
||||||
} else if (field(w)) {
|
} else if (field(w)) {
|
||||||
fill(frame, rect, theme.background, w.clip);
|
fill(frame, rect, theme.background, w.clip);
|
||||||
outline(frame, rect,
|
outline(frame, rect,
|
||||||
@@ -585,7 +606,7 @@ struct Context::Impl {
|
|||||||
: theme.border,
|
: theme.border,
|
||||||
w.clip);
|
w.clip);
|
||||||
} else if (w.kind == Kind::Divider) {
|
} else if (w.kind == Kind::Divider) {
|
||||||
fill(frame, rect, hover || w.id == captured ? theme.accent : theme.background, w.clip);
|
fill(frame, rect, hover || w.id == captured ? theme.border : theme.background, w.clip);
|
||||||
}
|
}
|
||||||
float tx = rect.x + theme.padding * scale + w.indent * 14 * scale;
|
float tx = rect.x + theme.padding * scale + w.indent * 14 * scale;
|
||||||
const auto font_size = (w.font_size > 0 ? w.font_size : theme.font_size) * scale;
|
const auto font_size = (w.font_size > 0 ? w.font_size : theme.font_size) * scale;
|
||||||
@@ -673,7 +694,11 @@ struct Context::Impl {
|
|||||||
}
|
}
|
||||||
fill(frame, {tx + caret, rect.y + 5 * scale, scale, rect.height - 10 * scale},
|
fill(frame, {tx + caret, rect.y + 5 * scale, scale, rect.height - 10 * scale},
|
||||||
theme.accent, text_clip);
|
theme.accent, text_clip);
|
||||||
} else if (w.kind != Kind::Divider && w.kind != Kind::Viewport && !w.text.empty())
|
} else if (field(w) && w.text.empty() && !w.placeholder.empty())
|
||||||
|
font.draw(frame, w.placeholder, tx, ty, font_size, theme.muted,
|
||||||
|
w.clip.intersection(
|
||||||
|
{rect.x + 2 * scale, rect.y, rect.width - 4 * scale, rect.height}));
|
||||||
|
else if (w.kind != Kind::Divider && w.kind != Kind::Viewport && !w.text.empty())
|
||||||
font.draw(frame, w.kind == Kind::NumberField ? number(w.value, w.precision) : w.text,
|
font.draw(frame, w.kind == Kind::NumberField ? number(w.value, w.precision) : w.text,
|
||||||
tx, ty, font_size, ink,
|
tx, ty, font_size, ink,
|
||||||
w.clip.intersection(
|
w.clip.intersection(
|
||||||
@@ -805,6 +830,14 @@ void Context::layout(float width, float height, float scale) {
|
|||||||
check(root());
|
check(root());
|
||||||
impl_->arrange(root(), {0, 0, impl_->width, impl_->height},
|
impl_->arrange(root(), {0, 0, impl_->width, impl_->height},
|
||||||
{0, 0, impl_->width, impl_->height});
|
{0, 0, impl_->width, impl_->height});
|
||||||
|
if (!impl_->hovered.empty()) {
|
||||||
|
const auto* hit = impl_->hit(root(), impl_->mouse_x, impl_->mouse_y);
|
||||||
|
const auto current_hover = hit ? hit->id : "";
|
||||||
|
if (current_hover != impl_->hovered) {
|
||||||
|
impl_->hovered = current_hover;
|
||||||
|
impl_->hover_started = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
std::erase_if(impl_->edits, [&](const auto& entry) { return !ids.contains(entry.first); });
|
std::erase_if(impl_->edits, [&](const auto& entry) { return !ids.contains(entry.first); });
|
||||||
auto* focused = impl_->find(impl_->focused);
|
auto* focused = impl_->find(impl_->focused);
|
||||||
if (!focused || !focusable(*focused))
|
if (!focused || !focusable(*focused))
|
||||||
@@ -814,6 +847,58 @@ void Context::layout(float width, float height, float scale) {
|
|||||||
}
|
}
|
||||||
void Context::draw(render::Snapshot& snapshot) {
|
void Context::draw(render::Snapshot& snapshot) {
|
||||||
impl_->draw_widget(root(), snapshot);
|
impl_->draw_widget(root(), snapshot);
|
||||||
|
if (impl_->payload.is_null() && impl_->captured.empty() &&
|
||||||
|
std::chrono::steady_clock::now() - impl_->hover_started >=
|
||||||
|
std::chrono::milliseconds(450)) {
|
||||||
|
const auto* widget = impl_->find(impl_->hovered);
|
||||||
|
if (widget && widget->visible && !widget->tooltip.empty() && impl_->width > 100) {
|
||||||
|
const auto font_size = impl_->theme.font_size * impl_->scale;
|
||||||
|
const auto padding = 8.f * impl_->scale;
|
||||||
|
const auto max_line_width =
|
||||||
|
std::max(40.f, std::min(480.f * impl_->scale, impl_->width - 24.f * impl_->scale) -
|
||||||
|
2.f * padding);
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::string line;
|
||||||
|
for (std::size_t i = 0; i < widget->tooltip.size();) {
|
||||||
|
const auto first = static_cast<unsigned char>(widget->tooltip[i]);
|
||||||
|
std::size_t bytes = first < 0x80 ? 1 : first < 0xe0 ? 2 : first < 0xf0 ? 3 : 4;
|
||||||
|
bytes = std::min(bytes, widget->tooltip.size() - i);
|
||||||
|
const auto character = widget->tooltip.substr(i, bytes);
|
||||||
|
i += bytes;
|
||||||
|
if (character == "\n") {
|
||||||
|
lines.push_back(line);
|
||||||
|
line.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!line.empty() &&
|
||||||
|
impl_->font.measure(line + character, font_size) > max_line_width) {
|
||||||
|
lines.push_back(line);
|
||||||
|
line.clear();
|
||||||
|
}
|
||||||
|
line += character;
|
||||||
|
}
|
||||||
|
lines.push_back(line);
|
||||||
|
float line_width = 0;
|
||||||
|
for (const auto& value : lines)
|
||||||
|
line_width = std::max(line_width, impl_->font.measure(value, font_size));
|
||||||
|
const auto line_height = font_size + 5.f * impl_->scale;
|
||||||
|
const float width = line_width + 2.f * padding;
|
||||||
|
const float height = float(lines.size()) * line_height + 2.f * padding;
|
||||||
|
const Rect viewport{0, 0, impl_->width, impl_->height};
|
||||||
|
const Rect tooltip{
|
||||||
|
std::clamp(impl_->mouse_x + 12.f * impl_->scale, 4.f,
|
||||||
|
std::max(4.f, impl_->width - width - 4.f)),
|
||||||
|
std::clamp(impl_->mouse_y + 18.f * impl_->scale, 4.f,
|
||||||
|
std::max(4.f, impl_->height - height - 4.f)),
|
||||||
|
width, height};
|
||||||
|
fill(snapshot, tooltip, impl_->theme.raised, viewport);
|
||||||
|
outline(snapshot, tooltip, impl_->theme.border, viewport);
|
||||||
|
for (std::size_t i = 0; i < lines.size(); ++i)
|
||||||
|
impl_->font.draw(snapshot, lines[i], tooltip.x + padding,
|
||||||
|
tooltip.y + padding + float(i) * line_height, font_size,
|
||||||
|
impl_->theme.text, viewport);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!impl_->payload.is_null()) {
|
if (!impl_->payload.is_null()) {
|
||||||
const Rect viewport{0, 0, impl_->width, impl_->height};
|
const Rect viewport{0, 0, impl_->width, impl_->height};
|
||||||
const auto text =
|
const auto text =
|
||||||
@@ -890,6 +975,7 @@ bool Context::handle(const render::Event& event) {
|
|||||||
using Type = render::Event::Type;
|
using Type = render::Event::Type;
|
||||||
if (event.type == Type::FocusLost) {
|
if (event.type == Type::FocusLost) {
|
||||||
p.cancel_capture();
|
p.cancel_capture();
|
||||||
|
p.hovered.clear();
|
||||||
if (auto found = p.edits.find(p.focused); found != p.edits.end())
|
if (auto found = p.edits.find(p.focused); found != p.edits.end())
|
||||||
found->second.composition.clear();
|
found->second.composition.clear();
|
||||||
p.unfocus(true);
|
p.unfocus(true);
|
||||||
@@ -901,6 +987,7 @@ bool Context::handle(const render::Event& event) {
|
|||||||
p.mouse_y = event.y;
|
p.mouse_y = event.y;
|
||||||
auto* hit = p.hit(p.root, event.x, event.y);
|
auto* hit = p.hit(p.root, event.x, event.y);
|
||||||
p.hovered = hit ? hit->id : "";
|
p.hovered = hit ? hit->id : "";
|
||||||
|
p.hover_started = std::chrono::steady_clock::now();
|
||||||
}
|
}
|
||||||
if (event.type == Type::Wheel) {
|
if (event.type == Type::Wheel) {
|
||||||
auto* w = p.hit(p.root, p.mouse_x, p.mouse_y);
|
auto* w = p.hit(p.root, p.mouse_x, p.mouse_y);
|
||||||
@@ -911,6 +998,7 @@ bool Context::handle(const render::Event& event) {
|
|||||||
std::max(0.f, w->content_height - w->rect.height +
|
std::max(0.f, w->content_height - w->rect.height +
|
||||||
2 * w->layout.padding * p.scale));
|
2 * w->layout.padding * p.scale));
|
||||||
layout(p.width, p.height, p.scale);
|
layout(p.width, p.height, p.scale);
|
||||||
|
p.hover_started = std::chrono::steady_clock::now();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ int main() {
|
|||||||
"test", "Unicode scene was saved under the wrong filename");
|
"test", "Unicode scene was saved under the wrong filename");
|
||||||
session.commands().call("faset_document_open", {{"path", "Scenes/Начало 世界.scene.json"}});
|
session.commands().call("faset_document_open", {{"path", "Scenes/Начало 世界.scene.json"}});
|
||||||
auto& commands = session.commands();
|
auto& commands = session.commands();
|
||||||
|
const auto play = commands.call("faset_play", {{"document", document.at("id")}});
|
||||||
|
require(play.value("play_pending", false) && session.play_pending() && !session.playing(),
|
||||||
|
"test", "Play must expose its cancellable build phase before Player starts");
|
||||||
|
commands.call("faset_stop", Json::object());
|
||||||
|
require(!session.play_pending() && !session.playing(), "test",
|
||||||
|
"Stop must cancel a pending Play build");
|
||||||
const auto initial = commands.call("faset_project_settings_get", Json::object());
|
const auto initial = commands.call("faset_project_settings_get", Json::object());
|
||||||
auto changed = commands.call("faset_project_settings_set",
|
auto changed = commands.call("faset_project_settings_set",
|
||||||
{{"revision", initial.at("revision")},
|
{{"revision", initial.at("revision")},
|
||||||
|
|||||||
@@ -244,6 +244,19 @@ int main() {
|
|||||||
state = session.authoring().query(ui.current_document());
|
state = session.authoring().query(ui.current_document());
|
||||||
check(state["scene"]["entities"][0]["name"] == "Дверь", "Inspector Unicode rename");
|
check(state["scene"]["entities"][0]["name"] == "Дверь", "Inspector Unicode rename");
|
||||||
const auto cid = state["scene"]["entities"][0]["components"][0]["id"].get<std::string>();
|
const auto cid = state["scene"]["entities"][0]["components"][0]["id"].get<std::string>();
|
||||||
|
ui.widgets().find("inspector_panel")->layout.width = 210;
|
||||||
|
ui.frame({});
|
||||||
|
const auto* position_row = ui.widgets().find("field-" + cid + "-position");
|
||||||
|
const auto* last_axis = ui.widgets().find("field-" + cid + "-position-2");
|
||||||
|
check(position_row && position_row->layout.stack_vertical,
|
||||||
|
"Narrow Inspector stacks vector fields");
|
||||||
|
check(last_axis && last_axis->rect.width >= 35 &&
|
||||||
|
last_axis->rect.x + last_axis->rect.width <=
|
||||||
|
ui.widgets().find("properties")->rect.x +
|
||||||
|
ui.widgets().find("properties")->rect.width,
|
||||||
|
"Narrow Inspector keeps vector inputs within the panel");
|
||||||
|
ui.widgets().find("inspector_panel")->layout.width = 312;
|
||||||
|
ui.frame({});
|
||||||
const auto position = "field-" + cid + "-position-0";
|
const auto position = "field-" + cid + "-position-0";
|
||||||
text(ui, position, "3.5");
|
text(ui, position, "3.5");
|
||||||
state = session.authoring().query(ui.current_document());
|
state = session.authoring().query(ui.current_document());
|
||||||
|
|||||||
Reference in New Issue
Block a user