Refine dark editor and launcher interface
Native and manual checks / native (windows-2025) (push) Waiting to run
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) Waiting to run
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
This commit is contained in:
+48
-22
@@ -10,6 +10,7 @@
|
||||
namespace faset::editor {
|
||||
namespace {
|
||||
namespace fs = std::filesystem;
|
||||
using ui::Appearance;
|
||||
using ui::Kind;
|
||||
using ui::Widget;
|
||||
fs::path user_home() {
|
||||
@@ -129,10 +130,18 @@ struct ProjectLauncher::Impl {
|
||||
: renderer(r), ui(engine / "assets/fonts/NotoSans.ttf"),
|
||||
recents_file(recents.empty() ? recent_path() : recents) {
|
||||
auto theme = ui::Theme::load(engine / "assets/ui/dark.json");
|
||||
theme.font_size = 15;
|
||||
theme.row_height = 30;
|
||||
theme.font_size = 14;
|
||||
theme.row_height = 28;
|
||||
ui.set_theme(theme);
|
||||
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(); },
|
||||
[this](const std::string& value) { renderer.set_clipboard(value); });
|
||||
ui.set_ime(
|
||||
@@ -145,7 +154,6 @@ struct ProjectLauncher::Impl {
|
||||
ui.find("launcher-create")->on_click = [this](Widget&) { set_mode(true); };
|
||||
ui.find("launcher-2d")->on_click = [this](Widget&) { dimension = 2; };
|
||||
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-cancel")->on_click = [this](Widget&) { cancelled = true; };
|
||||
ui.find("launcher-name")->on_preview = [this](Widget&) { error.clear(); };
|
||||
@@ -177,7 +185,7 @@ struct ProjectLauncher::Impl {
|
||||
continue;
|
||||
const auto id = "launcher-recent-" + std::to_string(i++);
|
||||
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.on_click = [this, path = selection.path](Widget&) {
|
||||
ui.update_text("launcher-path", path_to_utf8(path), true);
|
||||
@@ -187,7 +195,7 @@ struct ProjectLauncher::Impl {
|
||||
}
|
||||
}
|
||||
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() {
|
||||
ui.clear_focus();
|
||||
@@ -223,18 +231,22 @@ struct ProjectLauncher::Impl {
|
||||
void build_browser() {
|
||||
auto& panel = ui.root().add(Kind::Panel, "launcher-browser");
|
||||
panel.layout.absolute = true;
|
||||
panel.layout.padding = 16;
|
||||
panel.layout.gap = 10;
|
||||
panel.layout.padding = 20;
|
||||
panel.layout.gap = 8;
|
||||
panel.visible = false;
|
||||
auto& title = panel.add(Kind::Label, "browser-heading", "Choose project directory");
|
||||
title.font_size = 20;
|
||||
title.layout.height = 35;
|
||||
title.layout.height = 44;
|
||||
auto& nav = panel.add(Kind::Row, "browser-navigation");
|
||||
nav.layout.height = 36;
|
||||
button(nav, "browser-up", "Up", [this] {
|
||||
nav.layout.height = 38;
|
||||
auto& up = button(nav, "browser-up", "Up", [this] {
|
||||
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");
|
||||
path.layout.flex = 1;
|
||||
path.on_preview = [this](Widget&) { browser_valid = false; };
|
||||
@@ -250,14 +262,18 @@ struct ProjectLauncher::Impl {
|
||||
list.layout.scroll = true;
|
||||
list.layout.gap = 2;
|
||||
auto& actions = panel.add(Kind::Row, "browser-actions");
|
||||
actions.layout.height = 36;
|
||||
button(actions, "browser-choose", "Choose directory", [this] {
|
||||
actions.layout.height = 42;
|
||||
auto& choose = button(actions, "browser-choose", "Choose directory", [this] {
|
||||
if (browser_valid) {
|
||||
ui.update_text("launcher-path", path_to_utf8(browse_path), true);
|
||||
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");
|
||||
}
|
||||
void browse_to(const fs::path& path) {
|
||||
@@ -285,14 +301,14 @@ struct ProjectLauncher::Impl {
|
||||
for (const auto& child : children) {
|
||||
auto& row = list.add(Kind::TreeRow, "browser-entry-" + std::to_string(index++),
|
||||
"[Folder] " + path_to_utf8(child.filename()));
|
||||
row.layout.height = 34;
|
||||
row.layout.height = 36;
|
||||
row.on_click = [this, child](Widget&) {
|
||||
ui.clear_focus();
|
||||
browse_to(child);
|
||||
};
|
||||
}
|
||||
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&) {
|
||||
error = "Cannot access this directory.";
|
||||
} catch (const std::exception& e) {
|
||||
@@ -335,8 +351,14 @@ struct ProjectLauncher::Impl {
|
||||
ui.find("launcher-create")->selected = create;
|
||||
ui.find("launcher-2d")->selected = dimension == 2;
|
||||
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",
|
||||
"launcher-dimensions"})
|
||||
"launcher-dimensions", "launcher-name-group",
|
||||
"launcher-name-space", "launcher-path-space",
|
||||
"launcher-dimension-group"})
|
||||
ui.find(id)->visible = create;
|
||||
ui.find("launcher-heading")->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.x = (logical_width() - dialog->layout.width) * .5f;
|
||||
dialog->layout.y = (logical_height() - dialog->layout.height) * .5f;
|
||||
ui.find("launcher-sidebar")->layout.width =
|
||||
std::clamp(logical_width() * .24f, 180.f, 235.f);
|
||||
ui.find("launcher-main")->layout.padding = logical_width() < 850 ? 16 : 32;
|
||||
const auto sidebar_width = std::clamp(logical_width() * .24f, 180.f, 235.f);
|
||||
const auto main_padding = logical_width() < 850 ? 20.f : 36.f;
|
||||
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) {
|
||||
refresh();
|
||||
|
||||
+10
-10
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"background": [0.073, 0.078, 0.090, 1],
|
||||
"surface": [0.102, 0.108, 0.122, 1],
|
||||
"raised": [0.145, 0.151, 0.169, 1],
|
||||
"hover": [0.19, 0.197, 0.219, 1],
|
||||
"border": [0.24, 0.25, 0.278, 1],
|
||||
"text": [0.88, 0.889, 0.91, 1],
|
||||
"muted": [0.59, 0.61, 0.65, 1],
|
||||
"accent": [0.65, 0.60, 0.88, 1],
|
||||
"selection": [0.26, 0.245, 0.35, 1],
|
||||
"danger": [0.92, 0.39, 0.38, 1],
|
||||
"background": [0.078, 0.078, 0.078, 1],
|
||||
"surface": [0.110, 0.110, 0.114, 1],
|
||||
"raised": [0.153, 0.153, 0.161, 1],
|
||||
"hover": [0.196, 0.196, 0.204, 1],
|
||||
"border": [0.227, 0.227, 0.239, 1],
|
||||
"text": [0.878, 0.878, 0.886, 1],
|
||||
"muted": [0.604, 0.604, 0.624, 1],
|
||||
"accent": [0.659, 0.627, 0.761, 1],
|
||||
"selection": [0.220, 0.212, 0.251, 1],
|
||||
"danger": [0.835, 0.431, 0.420, 1],
|
||||
"font_size": 14,
|
||||
"row_height": 28,
|
||||
"padding": 8,
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
"root": {
|
||||
"id": "root", "kind": "column", "layout": {"gap": 1},
|
||||
"children": [
|
||||
{"id": "menubar", "kind": "panel", "layout": {"height": 34, "padding": 3}},
|
||||
{"id": "toolbar", "kind": "panel", "layout": {"height": 40, "padding": 5}},
|
||||
{"id": "menubar", "kind": "panel", "layout": {"height": 36, "padding": 3}},
|
||||
{"id": "toolbar", "kind": "panel", "layout": {"height": 44, "padding": 7}},
|
||||
{"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": "viewport", "kind": "viewport", "layout": {"flex": 1, "min_width": 180}},
|
||||
{"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_panel", "kind": "panel", "layout": {"height": 184, "min_height": 90, "gap": 0}},
|
||||
{"id": "statusbar", "kind": "panel", "layout": {"height": 26, "padding": 0}}
|
||||
{"id": "bottom_panel", "kind": "panel", "layout": {"height": 190, "min_height": 90, "gap": 0}},
|
||||
{"id": "statusbar", "kind": "panel", "layout": {"height": 25, "padding": 0}}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,34 +5,54 @@
|
||||
"children": [{
|
||||
"id": "launcher-body", "kind": "row", "layout": {"flex": 1, "gap": 0},
|
||||
"children": [
|
||||
{"id": "launcher-sidebar", "kind": "panel", "layout": {"width": 235, "padding": 16, "gap": 8}, "children": [
|
||||
{"id": "launcher-wordmark", "kind": "label", "text": "Faset", "font_size": 26, "layout": {"height": 66}},
|
||||
{"id": "launcher-open", "kind": "tree_row", "text": "Open project", "layout": {"height": 40}},
|
||||
{"id": "launcher-create", "kind": "tree_row", "text": "Create project", "layout": {"height": 40}},
|
||||
{"id": "launcher-recent-title", "kind": "label", "text": "Recent projects", "layout": {"height": 54}},
|
||||
{"id": "launcher-recents", "kind": "column", "layout": {"flex": 1, "scroll": true, "gap": 6}}
|
||||
{"id": "launcher-sidebar", "kind": "panel", "layout": {"width": 235, "padding": 18, "gap": 0}, "children": [
|
||||
{"id": "launcher-wordmark", "kind": "label", "text": "Faset", "font_size": 24, "layout": {"height": 70}},
|
||||
{"id": "launcher-nav", "kind": "column", "layout": {"gap": 4}, "children": [
|
||||
{"id": "launcher-open", "kind": "tree_row", "text": "Open project", "layout": {"height": 38}},
|
||||
{"id": "launcher-create", "kind": "tree_row", "text": "Create project", "layout": {"height": 38}}
|
||||
]},
|
||||
{"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-name-label", "kind": "label", "text": "Project name"},
|
||||
{"id": "launcher-name", "kind": "text_field", "text": "My Game", "layout": {"height": 40}},
|
||||
{"id": "launcher-path-label", "kind": "label", "text": "Project directory"},
|
||||
{"id": "launcher-path-row", "kind": "row", "layout": {"height": 40, "gap": 8}, "children": [
|
||||
{"id": "launcher-path", "kind": "text_field", "layout": {"flex": 1}},
|
||||
{"id": "launcher-browse", "kind": "button", "text": "Browse", "layout": {"width": 84}}
|
||||
{"id": "launcher-heading-rule", "kind": "row", "layout": {"height": 1}},
|
||||
{"id": "launcher-form", "kind": "column", "layout": {"width": 720, "gap": 0}, "children": [
|
||||
{"id": "launcher-form-top", "kind": "row", "layout": {"height": 24}},
|
||||
{"id": "launcher-name-group", "kind": "column", "layout": {"gap": 6}, "children": [
|
||||
{"id": "launcher-name-label", "kind": "label", "text": "Project name", "layout": {"height": 24}},
|
||||
{"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-dimensions", "kind": "row", "layout": {"height": 40, "gap": 0}, "children": [
|
||||
{"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"}
|
||||
{"id": "launcher-main-spacer", "kind": "row", "layout": {"flex": 1}},
|
||||
{"id": "launcher-shortcuts", "kind": "label", "text": "Tab: next field Ctrl+Enter: continue Escape: cancel", "font_size": 12, "layout": {"height": 24}}
|
||||
]}
|
||||
]
|
||||
}]
|
||||
|
||||
@@ -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
|
||||
|
||||
**Build C++ !** and the status message indicate stale gameplay metadata, for example
|
||||
after changing gameplay sources or after a failed build. Choose **Build C++**, then
|
||||
check **Jobs** and **Console**. A successful build and schema export refresh the
|
||||
Inspector. A failed build retains the previous metadata and reports the failure.
|
||||
The status message indicates stale gameplay metadata, for example after changing
|
||||
gameplay sources or after a failed build. Hover over **Build** for details, then
|
||||
choose **Build** and check **Jobs** and **Console**. A successful build and schema
|
||||
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
|
||||
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),
|
||||
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
|
||||
to migrate** instead. Top-level local additions migrate in their owning instance.
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -39,6 +39,9 @@ class Session {
|
||||
bool playing() const {
|
||||
return bool(player_);
|
||||
}
|
||||
bool play_pending() const {
|
||||
return !pending_play_job_.empty();
|
||||
}
|
||||
void log(std::string message);
|
||||
|
||||
private:
|
||||
|
||||
+10
-6
@@ -18,10 +18,11 @@ struct Rect {
|
||||
Rect intersection(const Rect&) const noexcept;
|
||||
};
|
||||
struct Theme {
|
||||
Color background{.075f, .078f, .086f, 1}, surface{.10f, .105f, .115f, 1};
|
||||
Color raised{.135f, .14f, .15f, 1}, hover{.175f, .18f, .20f, 1}, border{.23f, .235f, .255f, 1};
|
||||
Color text{.86f, .875f, .90f, 1}, muted{.55f, .575f, .62f, 1}, accent{.65f, .60f, .88f, 1};
|
||||
Color selection{.26f, .245f, .35f, 1}, danger{.92f, .39f, .38f, 1};
|
||||
Color background{.078f, .078f, .078f, 1}, surface{.110f, .110f, .114f, 1};
|
||||
Color raised{.153f, .153f, .161f, 1}, hover{.196f, .196f, .204f, 1};
|
||||
Color border{.227f, .227f, .239f, 1}, text{.878f, .878f, .886f, 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;
|
||||
static Theme from_json(const Json&);
|
||||
static Theme load(const std::filesystem::path&);
|
||||
@@ -103,15 +104,18 @@ enum class Kind {
|
||||
Divider,
|
||||
Viewport
|
||||
};
|
||||
// Visual emphasis is independent of widget behavior and stable identity.
|
||||
enum class Appearance { Default, Quiet, Primary, Section };
|
||||
struct Layout {
|
||||
float width = -1, height = -1, flex = 0;
|
||||
float min_width = 0, min_height = 0, max_width = 100000, max_height = 100000;
|
||||
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;
|
||||
};
|
||||
struct Widget {
|
||||
Kind kind = Kind::Panel;
|
||||
Appearance appearance = Appearance::Default;
|
||||
std::string id, text;
|
||||
Layout layout;
|
||||
Rect rect, clip;
|
||||
@@ -120,7 +124,7 @@ struct Widget {
|
||||
int precision = 3, indent = 0;
|
||||
float font_size = 0; // Zero inherits the theme typography.
|
||||
float scroll_y = 0, content_height = 0;
|
||||
std::string error, tooltip;
|
||||
std::string error, tooltip, placeholder;
|
||||
Json drag_payload;
|
||||
std::string dock_area, dock_panel;
|
||||
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) {
|
||||
session.log(std::string("Layout reset: ") + e.what());
|
||||
}
|
||||
ui.find("scene_panel")->layout.width = dock.size("scene", 224);
|
||||
ui.find("inspector_panel")->layout.width = dock.size("inspector", 300);
|
||||
ui.find("bottom_panel")->layout.height = dock.size("bottom", 184);
|
||||
ui.find("scene_panel")->layout.width = dock.size("scene", 238);
|
||||
ui.find("inspector_panel")->layout.width = dock.size("inspector", 312);
|
||||
ui.find("bottom_panel")->layout.height = dock.size("bottom", 190);
|
||||
ui.set_clipboard([this] { return renderer.clipboard(); },
|
||||
[this](const std::string& text) { renderer.set_clipboard(text); });
|
||||
ui.set_ime([this](bool enabled) { renderer.set_text_input(enabled); },
|
||||
@@ -404,58 +404,88 @@ struct EditorUI::Impl {
|
||||
}
|
||||
void build_static() {
|
||||
auto& menurow = ui.find("menubar")->add(Kind::Row, "menuitems");
|
||||
menurow.layout.gap = 2;
|
||||
for (const std::string name : {"Faset", "File", "Edit", "Scene", "View", "Help"})
|
||||
button(
|
||||
menurow.layout.gap = 0;
|
||||
for (const std::string name : {"Faset", "File", "Edit", "Scene", "View", "Help"}) {
|
||||
auto& item = button(
|
||||
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",
|
||||
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");
|
||||
toolbar.layout.gap = 5;
|
||||
button(toolbar, "save", "Save", [this] { save(); }, 60);
|
||||
button(toolbar, "undo", "Undo", [this] { history(false); }, 56);
|
||||
button(toolbar, "redo", "Redo", [this] { history(true); }, 56);
|
||||
toolbar.layout.gap = 3;
|
||||
auto& file_tools = toolbar.add(Kind::Row, "file-tools");
|
||||
file_tools.layout.width = 180;
|
||||
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(
|
||||
toolbar, "play", "Play", [this] { call("faset_play", {{"document", document}}); }, 58);
|
||||
button(
|
||||
toolbar, "stop", "Stop",
|
||||
[this] {
|
||||
call("faset_stop");
|
||||
paused = false;
|
||||
},
|
||||
56);
|
||||
button(
|
||||
toolbar, "pause", "Pause",
|
||||
play_tools, "pause", "Pause",
|
||||
[this] {
|
||||
if (!call("faset_play_control", {{"command", paused ? "resume" : "pause"}})
|
||||
.is_null())
|
||||
paused = !paused;
|
||||
},
|
||||
65);
|
||||
68)
|
||||
.appearance = ui::Appearance::Quiet;
|
||||
button(
|
||||
toolbar, "step", "Step", [this] { call("faset_play_control", {{"command", "step"}}); },
|
||||
55);
|
||||
button(toolbar, "build", "Build", [this] { call("faset_build"); }, 94);
|
||||
play_tools, "stop", "Stop",
|
||||
[this] {
|
||||
call("faset_stop");
|
||||
paused = false;
|
||||
},
|
||||
60)
|
||||
.appearance = ui::Appearance::Quiet;
|
||||
button(
|
||||
toolbar, "export", "Export",
|
||||
[this] { call("faset_export", {{"document", document}, {"output", "Exports"}}); }, 64);
|
||||
play_tools, "step", "Step",
|
||||
[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(
|
||||
toolbar, "simulation-settings", "Simulation",
|
||||
[this] { simulation_open = !simulation_open; }, 96);
|
||||
auto& space = toolbar.add(Kind::Label, "toolbar-space", "");
|
||||
space.layout.flex = 1;
|
||||
button(toolbar, "command-palette", "Commands", [this] { palette = !palette; }, 104);
|
||||
build_tools, "export", "Export",
|
||||
[this] { call("faset_export", {{"document", document}, {"output", "Exports"}}); }, 72)
|
||||
.appearance = ui::Appearance::Quiet;
|
||||
button(
|
||||
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");
|
||||
scene.add(Kind::Tab, "scene-tab", "Scene").selected = true;
|
||||
auto& tools = scene.add(Kind::Row, "scene-tools");
|
||||
tools.layout.height = 28;
|
||||
tools.layout.height = 32;
|
||||
tools.layout.padding = 3;
|
||||
tools.layout.gap = 3;
|
||||
button(tools, "add-object", "+ Object", [this] { create_object("Object"); }, 80);
|
||||
button(tools, "add-cube", "Cube", [this] { create_object("Cube"); }, 55);
|
||||
button(tools, "add-sprite", "Sprite", [this] { create_object("Sprite"); }, 58);
|
||||
button(tools, "add-object", "+ Object", [this] { create_object("Object"); }, 80)
|
||||
.appearance = ui::Appearance::Quiet;
|
||||
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");
|
||||
tree.layout.flex = 1;
|
||||
tree.layout.scroll = true;
|
||||
@@ -469,26 +499,32 @@ struct EditorUI::Impl {
|
||||
auto& body = inspector.add(Kind::Column, "properties");
|
||||
body.layout.flex = 1;
|
||||
body.layout.padding = 10;
|
||||
body.layout.gap = 7;
|
||||
body.layout.gap = 6;
|
||||
body.layout.scroll = true;
|
||||
auto& vp = *ui.find("viewport");
|
||||
auto& vptools = vp.add(Kind::Row, "viewport-tools");
|
||||
vptools.layout.absolute = true;
|
||||
vptools.layout.x = 8;
|
||||
vptools.layout.y = 8;
|
||||
auto& tool_surface = vp.add(Kind::Panel, "viewport-tool-surface");
|
||||
tool_surface.layout.absolute = true;
|
||||
tool_surface.layout.x = 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.width = 300;
|
||||
vptools.layout.gap = 4;
|
||||
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(
|
||||
vptools, "back-document", "Back",
|
||||
[this] {
|
||||
if (!last_document.empty())
|
||||
choose_document(last_document);
|
||||
},
|
||||
54);
|
||||
vptools.layout.width = 360;
|
||||
button(vptools, "frame-selection", "Frame", [this] { frame_selection(); }, 64);
|
||||
54)
|
||||
.appearance = ui::Appearance::Quiet;
|
||||
button(vptools, "frame-selection", "Frame", [this] { frame_selection(); }, 64)
|
||||
.appearance = ui::Appearance::Quiet;
|
||||
vp.on_drop = [this](Widget&, const Json& data) {
|
||||
if (data.value("kind", std::string()) == "asset")
|
||||
instantiate_asset(data.at("id").get<std::string>());
|
||||
@@ -509,12 +545,15 @@ struct EditorUI::Impl {
|
||||
tab.on_click = [this, id](Widget&) { active_bottom = id; };
|
||||
}
|
||||
auto& assetbar = bottom.add(Kind::Row, "asset-toolbar");
|
||||
assetbar.layout.height = 30;
|
||||
assetbar.layout.padding = 2;
|
||||
assetbar.layout.height = 32;
|
||||
assetbar.layout.padding = 3;
|
||||
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", "");
|
||||
search.layout.width = 220;
|
||||
search.placeholder = "Search project files";
|
||||
search.tooltip = "Filter files by project-relative path";
|
||||
search.on_preview = [this](Widget& w) {
|
||||
asset_filter = w.text;
|
||||
assets_dirty = true;
|
||||
@@ -525,8 +564,10 @@ struct EditorUI::Impl {
|
||||
if (!source_file.empty())
|
||||
call("faset_import", {{"path", source_file}});
|
||||
},
|
||||
146);
|
||||
button(assetbar, "asset-open", "Open Scene", [this] { open_source(); }, 104);
|
||||
146)
|
||||
.appearance = ui::Appearance::Quiet;
|
||||
button(assetbar, "asset-open", "Open Scene", [this] { open_source(); }, 104)
|
||||
.appearance = ui::Appearance::Quiet;
|
||||
button(
|
||||
assetbar, "asset-refresh", "Refresh",
|
||||
[this] {
|
||||
@@ -534,7 +575,8 @@ struct EditorUI::Impl {
|
||||
view.clearCache();
|
||||
picks_key.clear();
|
||||
},
|
||||
75);
|
||||
75)
|
||||
.appearance = ui::Appearance::Quiet;
|
||||
auto& items = bottom.add(Kind::Column, "asset-items");
|
||||
items.layout.flex = 1;
|
||||
items.layout.scroll = true;
|
||||
@@ -554,7 +596,13 @@ struct EditorUI::Impl {
|
||||
auto& statusrow = ui.find("statusbar")->add(Kind::Row, "status-row");
|
||||
auto& statuslabel = statusrow.add(Kind::Label, "status", "Ready");
|
||||
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);
|
||||
statusrow.find("renderer-status")->enabled = false;
|
||||
build_overlays();
|
||||
}
|
||||
void build_overlays() {
|
||||
@@ -1173,8 +1221,13 @@ struct EditorUI::Impl {
|
||||
ui.find("undo")->enabled = current.value("can_undo", false);
|
||||
ui.find("redo")->enabled = current.value("can_redo", false);
|
||||
ui.find("pause")->enabled = session.playing();
|
||||
ui.find("stop")->enabled = session.playing() || session.play_pending();
|
||||
ui.find("step")->enabled = session.playing();
|
||||
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();
|
||||
// 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)) {
|
||||
@@ -1183,17 +1236,27 @@ struct EditorUI::Impl {
|
||||
}
|
||||
if (!schema_state.is_null()) {
|
||||
const bool stale = schema_state.value("stale", false);
|
||||
ui.find("build")->text = stale ? "Build !" : "Build";
|
||||
ui.find("build")->tooltip =
|
||||
stale ? "Gameplay schema is stale: " + schema_state.value("error", std::string())
|
||||
: "Incremental gameplay build and C++/Lua Inspector metadata refresh";
|
||||
if (stale && status == "Ready")
|
||||
status = "Gameplay schema is stale; Build to refresh Inspector metadata";
|
||||
auto& build = *ui.find("build");
|
||||
build.text = "Build";
|
||||
const auto schema_error = schema_state.value("error", std::string());
|
||||
build.tooltip = stale ? "Gameplay metadata is out of date. Build to refresh Inspector "
|
||||
"fields."
|
||||
: "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")) +
|
||||
" / " + current.at("name").get<std::string>() +
|
||||
const auto project_name = session.project().value("name", std::string("Project"));
|
||||
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) ? " *" : "");
|
||||
ui.find("project-title")->tooltip = project_name + " / " + scene_name;
|
||||
ui.find("status")->text = status;
|
||||
ui.find("renderer-status")->text =
|
||||
"Vulkan 1.3 | " + std::to_string(resolved.at("entities").size()) + " objects";
|
||||
@@ -1207,7 +1270,16 @@ struct EditorUI::Impl {
|
||||
keep.insert(row.id);
|
||||
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.on_click = [this](Widget&) { select(""); };
|
||||
touch(root);
|
||||
@@ -1400,6 +1472,11 @@ struct EditorUI::Impl {
|
||||
void refresh_inspector() {
|
||||
auto& body = *ui.find("properties");
|
||||
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);
|
||||
if (!instance_selection.empty()) {
|
||||
refresh_instance_inspector(body, keep);
|
||||
@@ -1407,29 +1484,36 @@ struct EditorUI::Impl {
|
||||
return;
|
||||
}
|
||||
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");
|
||||
trim_children(body, keep);
|
||||
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"));
|
||||
const bool source_object = inherited(*e), local_addition = owned_addition(*e);
|
||||
name.enabled = !source_object || local_addition;
|
||||
if (local_addition) {
|
||||
label(body, "object-origin", "Local addition to this instance");
|
||||
keep.insert("object-origin");
|
||||
keep_in_order("object-origin");
|
||||
} else if (source_object) {
|
||||
const auto path = e->at("origin").at("path");
|
||||
const auto object = e->at("origin").at("object").get<std::string>();
|
||||
label(body, "object-origin",
|
||||
"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",
|
||||
[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&) {
|
||||
edit_revisions.try_emplace("object-name", current.at("revision").get<std::uint64_t>());
|
||||
};
|
||||
@@ -1462,17 +1546,20 @@ struct EditorUI::Impl {
|
||||
: " (schema missing)")},
|
||||
{"fields", Json::object()}};
|
||||
auto& header = body.add(Kind::Row, "component-header-" + cid);
|
||||
keep.insert(header.id);
|
||||
header.layout.height = 28;
|
||||
keep_in_order(header.id);
|
||||
header.layout.height = 32;
|
||||
header.appearance = ui::Appearance::Section;
|
||||
auto& title =
|
||||
header.add(Kind::Label, "component-title-" + cid, metadata.value("name", type));
|
||||
title.layout.flex = 1;
|
||||
title.font_size = 14;
|
||||
button(
|
||||
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) {
|
||||
label(body, "opaque-note-" + cid, "Schema unavailable. Data is preserved.");
|
||||
keep.insert("opaque-note-" + cid);
|
||||
keep_in_order("opaque-note-" + cid);
|
||||
if (has_schema &&
|
||||
c.value("version", 1) <
|
||||
session.authoring().schemas().schema(type).value("version", 1)) {
|
||||
@@ -1489,31 +1576,48 @@ struct EditorUI::Impl {
|
||||
[this, cid] { migrate_component(cid); });
|
||||
action.tooltip =
|
||||
"Apply the schema's declared migration rules. Errors keep all stored data.";
|
||||
keep.insert(action_id);
|
||||
keep_in_order(action_id);
|
||||
}
|
||||
auto& raw =
|
||||
body.add(Kind::TextField, "opaque-fields-" + cid, c.at("fields").dump());
|
||||
raw.enabled = false;
|
||||
keep.insert(raw.id);
|
||||
keep_in_order(raw.id);
|
||||
button(body, "opaque-copy-" + cid, "Copy raw fields",
|
||||
[this, fields = c.at("fields")] { renderer.set_clipboard(fields.dump(2)); });
|
||||
keep.insert("opaque-copy-" + cid);
|
||||
keep_in_order("opaque-copy-" + cid);
|
||||
continue;
|
||||
}
|
||||
for (const auto& [fid, value] : c.at("fields").items()) {
|
||||
const auto descriptor = metadata.at("fields").value(fid, Json::object());
|
||||
const auto key = "field-" + cid + "-" + fid;
|
||||
auto& row = body.add(Kind::Column, key);
|
||||
keep.insert(key);
|
||||
row.layout.gap = 2;
|
||||
label(row, key + "-label",
|
||||
descriptor.value("name", fid) +
|
||||
(descriptor.value("unit", std::string()) == "radians" ? " (rad)" : ""));
|
||||
auto& row = body.add(Kind::Row, key);
|
||||
keep_in_order(key);
|
||||
const bool radians = descriptor.value("unit", std::string()) == "radians";
|
||||
const auto& options = descriptor.value("enum", Json::array());
|
||||
const bool vector_value = value.is_array() && value.size() >= 2 &&
|
||||
value.size() <= 4 &&
|
||||
std::all_of(value.begin(), value.end(),
|
||||
[](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_kind = !options.empty() ? Kind::Button
|
||||
: vector_value ? Kind::Row
|
||||
@@ -1537,7 +1641,9 @@ struct EditorUI::Impl {
|
||||
{"address", address}}}));
|
||||
});
|
||||
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,
|
||||
key + "-revert"}
|
||||
@@ -1554,14 +1660,16 @@ struct EditorUI::Impl {
|
||||
options.size();
|
||||
set_field(cid, fid, options[index], key + "-value");
|
||||
});
|
||||
input.layout.height = 27;
|
||||
input.layout.flex = 1;
|
||||
} else if (value.is_boolean()) {
|
||||
auto& input = row.add(Kind::Checkbox, key + "-value", "Enabled");
|
||||
input.layout.flex = 1;
|
||||
input.checked = value;
|
||||
bind_field(input, cid, fid, value);
|
||||
} else if (vector_value) {
|
||||
auto& vectorrow = row.add(Kind::Row, key + "-vector");
|
||||
vectorrow.layout.gap = 3;
|
||||
vectorrow.layout.flex = 1;
|
||||
std::set<std::string> axis_ids;
|
||||
for (std::size_t axis = 0; axis < value.size(); ++axis) {
|
||||
auto& input =
|
||||
@@ -1576,12 +1684,14 @@ struct EditorUI::Impl {
|
||||
trim_children(vectorrow, axis_ids);
|
||||
} else if (value.is_number()) {
|
||||
auto& input = row.add(Kind::NumberField, key + "-value");
|
||||
input.layout.flex = 1;
|
||||
input.step = value.is_number_integer() ? 1 : .05;
|
||||
input.precision = value.is_number_integer() ? 0 : 3;
|
||||
ui.update_number(input.id, value.get<double>());
|
||||
bind_field(input, cid, fid, value);
|
||||
} else {
|
||||
auto& input = row.add(Kind::TextField, key + "-value");
|
||||
input.layout.flex = 1;
|
||||
ui.update_text(input.id,
|
||||
value.is_string() ? value.get<std::string>() : value.dump());
|
||||
bind_field(input, cid, fid, value);
|
||||
@@ -1591,23 +1701,29 @@ struct EditorUI::Impl {
|
||||
auto& add = button(body, "add-component", "+ Add Component",
|
||||
[this] { component_menu = !component_menu; });
|
||||
add.enabled = !source_object || local_addition;
|
||||
keep.insert(add.id);
|
||||
keep_in_order(add.id);
|
||||
if (source_object) {
|
||||
const auto path = e->at("origin").at("path");
|
||||
const auto object = e->at("origin").at("object").get<std::string>();
|
||||
auto& child = button(body, "instance-add-child", "Add local child",
|
||||
[this, path, object] { add_instance_child(path, object); });
|
||||
child.enabled = path.size() == 1;
|
||||
keep.insert(child.id);
|
||||
keep_in_order(child.id);
|
||||
}
|
||||
if (component_menu && (!source_object || local_addition))
|
||||
for (const auto& schema : schemas.at("types")) {
|
||||
const auto type = schema.at("id").get<std::string>();
|
||||
auto& choice = button(body, "component-choice-" + type, schema.value("name", type),
|
||||
[this, type] { add_component(type); });
|
||||
keep.insert(choice.id);
|
||||
keep_in_order(choice.id);
|
||||
}
|
||||
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() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
@@ -1659,9 +1775,21 @@ struct EditorUI::Impl {
|
||||
std::set<std::string> keep;
|
||||
for (const auto& file : files) {
|
||||
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.indent = 1;
|
||||
row.tooltip = path;
|
||||
row.selected = source_file == path;
|
||||
row.on_click = [this, path](Widget&) {
|
||||
source_file = path;
|
||||
@@ -1681,9 +1809,10 @@ struct EditorUI::Impl {
|
||||
: state == "stale" ? "Stale"
|
||||
: "Unavailable";
|
||||
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.indent = 1;
|
||||
row.tooltip = "Asset " + id + " — " + state;
|
||||
row.drag_payload = {{"kind", "asset"}, {"id", id}, {"label", name}};
|
||||
row.on_click = [this, id, asset, freshness, state](Widget&) {
|
||||
renderer.set_clipboard(id);
|
||||
|
||||
+101
-13
@@ -1,4 +1,5 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <faset/core/io.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.scroll = j.value("scroll", l.scroll);
|
||||
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 ||
|
||||
l.max_width < l.min_width || l.max_height < l.min_height)
|
||||
throw std::runtime_error("Invalid layout constraints");
|
||||
@@ -243,6 +245,7 @@ struct Context::Impl {
|
||||
FontAtlas font;
|
||||
float width = 0, height = 0, scale = 1, mouse_x = 0, mouse_y = 0;
|
||||
std::string focused, hovered, captured;
|
||||
std::chrono::steady_clock::time_point hover_started = std::chrono::steady_clock::now();
|
||||
struct Edit {
|
||||
TextBuffer buffer;
|
||||
std::string original, composition;
|
||||
@@ -285,18 +288,19 @@ struct Context::Impl {
|
||||
return 80 * scale;
|
||||
}
|
||||
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;
|
||||
std::size_t count = 0;
|
||||
for (auto& child : w.children)
|
||||
if (child->visible && !child->layout.absolute) {
|
||||
const auto value = intrinsic(*child, false);
|
||||
if (w.kind == Kind::Row)
|
||||
if (horizontal)
|
||||
total = std::max(total, value);
|
||||
else
|
||||
total += value;
|
||||
++count;
|
||||
}
|
||||
if (w.kind != Kind::Row && count)
|
||||
if (!horizontal && count)
|
||||
total += float(count - 1) * w.layout.gap * scale;
|
||||
return total + w.layout.padding * 2 * scale;
|
||||
}
|
||||
@@ -310,7 +314,7 @@ struct Context::Impl {
|
||||
const auto pad = w.layout.padding * scale;
|
||||
Rect inner{rect.x + pad, rect.y + pad, std::max(0.f, rect.width - 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;
|
||||
std::vector<Widget*> flow;
|
||||
float fixed = 0, flex = 0;
|
||||
@@ -557,17 +561,30 @@ struct Context::Impl {
|
||||
const auto& rect = w.rect;
|
||||
if (w.kind == Kind::Panel) {
|
||||
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) {
|
||||
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 (w.appearance == Appearance::Primary && w.enabled) {
|
||||
fill(frame, rect, theme.accent, w.clip);
|
||||
ink = theme.background;
|
||||
} else if (w.appearance == Appearance::Quiet) {
|
||||
if (w.selected || (hover && w.enabled))
|
||||
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) {
|
||||
if (w.selected || hover)
|
||||
if (w.kind == Kind::TreeRow && (w.selected || hover))
|
||||
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.kind == Kind::Tab)
|
||||
fill(frame, {rect.x, rect.y + rect.height - 2 * scale, rect.width, 2 * scale},
|
||||
@@ -575,8 +592,12 @@ struct Context::Impl {
|
||||
else
|
||||
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)
|
||||
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)) {
|
||||
fill(frame, rect, theme.background, w.clip);
|
||||
outline(frame, rect,
|
||||
@@ -585,7 +606,7 @@ struct Context::Impl {
|
||||
: theme.border,
|
||||
w.clip);
|
||||
} 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;
|
||||
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},
|
||||
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,
|
||||
tx, ty, font_size, ink,
|
||||
w.clip.intersection(
|
||||
@@ -805,6 +830,14 @@ void Context::layout(float width, float height, float scale) {
|
||||
check(root());
|
||||
impl_->arrange(root(), {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); });
|
||||
auto* focused = impl_->find(impl_->focused);
|
||||
if (!focused || !focusable(*focused))
|
||||
@@ -814,6 +847,58 @@ void Context::layout(float width, float height, float scale) {
|
||||
}
|
||||
void Context::draw(render::Snapshot& 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()) {
|
||||
const Rect viewport{0, 0, impl_->width, impl_->height};
|
||||
const auto text =
|
||||
@@ -890,6 +975,7 @@ bool Context::handle(const render::Event& event) {
|
||||
using Type = render::Event::Type;
|
||||
if (event.type == Type::FocusLost) {
|
||||
p.cancel_capture();
|
||||
p.hovered.clear();
|
||||
if (auto found = p.edits.find(p.focused); found != p.edits.end())
|
||||
found->second.composition.clear();
|
||||
p.unfocus(true);
|
||||
@@ -901,6 +987,7 @@ bool Context::handle(const render::Event& event) {
|
||||
p.mouse_y = event.y;
|
||||
auto* hit = p.hit(p.root, event.x, event.y);
|
||||
p.hovered = hit ? hit->id : "";
|
||||
p.hover_started = std::chrono::steady_clock::now();
|
||||
}
|
||||
if (event.type == Type::Wheel) {
|
||||
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 +
|
||||
2 * w->layout.padding * p.scale));
|
||||
layout(p.width, p.height, p.scale);
|
||||
p.hover_started = std::chrono::steady_clock::now();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -20,6 +20,12 @@ int main() {
|
||||
"test", "Unicode scene was saved under the wrong filename");
|
||||
session.commands().call("faset_document_open", {{"path", "Scenes/Начало 世界.scene.json"}});
|
||||
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());
|
||||
auto changed = commands.call("faset_project_settings_set",
|
||||
{{"revision", initial.at("revision")},
|
||||
|
||||
@@ -244,6 +244,19 @@ int main() {
|
||||
state = session.authoring().query(ui.current_document());
|
||||
check(state["scene"]["entities"][0]["name"] == "Дверь", "Inspector Unicode rename");
|
||||
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";
|
||||
text(ui, position, "3.5");
|
||||
state = session.authoring().query(ui.current_document());
|
||||
|
||||
Reference in New Issue
Block a user