From 7fd36de77a5fc78581b2d981a6492e18d3d63aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Tue, 19 Nov 2024 22:23:40 +0100 Subject: [PATCH] Use String as ID, working visible toggle --- GlitchyEditor/EditorUI/package-lock.json | 96 ++++++++++++++++++- GlitchyEditor/EditorUI/package.json | 2 + .../src/Components/IconCheckButton.tsx | 10 ++ GlitchyEditor/EditorUI/src/EngineGlue.ts | 8 +- GlitchyEditor/EditorUI/src/Entity.ts | 6 +- .../src/Windows/EntityHierarchyWindow.tsx | 29 +++--- .../EditorUI/src/Windows/MainWindow.tsx | 4 +- GlitchyEditor/src/Ultralight/JSObject.bf | 53 ++++++++++ .../src/Ultralight/UltralightHelper.bf | 18 ++++ .../src/Ultralight/UltralightMainWindow.bf | 70 +++++++++++--- .../src/Ultralight/UltralightWindow.bf | 17 +++- 11 files changed, 272 insertions(+), 41 deletions(-) create mode 100644 GlitchyEditor/EditorUI/src/Components/IconCheckButton.tsx create mode 100644 GlitchyEditor/src/Ultralight/JSObject.bf diff --git a/GlitchyEditor/EditorUI/package-lock.json b/GlitchyEditor/EditorUI/package-lock.json index 7228738..c7a1797 100644 --- a/GlitchyEditor/EditorUI/package-lock.json +++ b/GlitchyEditor/EditorUI/package-lock.json @@ -8,6 +8,7 @@ "name": "glitchy-editor-ui", "version": "0.0.0", "dependencies": { + "@rollup/plugin-typescript": "^12.1.1", "dockview-react": "^1.17.2", "eslint-plugin-react": "^7.37.2", "react": "^18.3.1", @@ -16,6 +17,7 @@ }, "devDependencies": { "@eslint/js": "^9.11.1", + "@types/node": "^22.9.0", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.2", @@ -942,6 +944,79 @@ "node": ">= 8" } }, + "node_modules/@rollup/plugin-typescript": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", + "integrity": "sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript/node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-typescript/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -1224,6 +1299,15 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -2455,8 +2539,7 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/esutils": { "version": "2.0.3", @@ -3769,7 +3852,7 @@ "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", - "dev": true, + "devOptional": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -4183,7 +4266,6 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4229,6 +4311,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", diff --git a/GlitchyEditor/EditorUI/package.json b/GlitchyEditor/EditorUI/package.json index 0755078..a1a90d2 100644 --- a/GlitchyEditor/EditorUI/package.json +++ b/GlitchyEditor/EditorUI/package.json @@ -11,6 +11,7 @@ "preview": "vite preview" }, "dependencies": { + "@rollup/plugin-typescript": "^12.1.1", "dockview-react": "^1.17.2", "eslint-plugin-react": "^7.37.2", "react": "^18.3.1", @@ -19,6 +20,7 @@ }, "devDependencies": { "@eslint/js": "^9.11.1", + "@types/node": "^22.9.0", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.2", diff --git a/GlitchyEditor/EditorUI/src/Components/IconCheckButton.tsx b/GlitchyEditor/EditorUI/src/Components/IconCheckButton.tsx new file mode 100644 index 0000000..a76e960 --- /dev/null +++ b/GlitchyEditor/EditorUI/src/Components/IconCheckButton.tsx @@ -0,0 +1,10 @@ +import {MouseEventHandler, ReactElement} from "react"; + +export function IconCheckButton({isChecked, iconChecked, iconUnchecked, onClick} : {isChecked: boolean, iconChecked: string, iconUnchecked: string, onClick: MouseEventHandler }) : ReactElement +{ + return ( +
+ +
+ ); +} diff --git a/GlitchyEditor/EditorUI/src/EngineGlue.ts b/GlitchyEditor/EditorUI/src/EngineGlue.ts index 5dd97e3..b496b3a 100644 --- a/GlitchyEditor/EditorUI/src/EngineGlue.ts +++ b/GlitchyEditor/EditorUI/src/EngineGlue.ts @@ -1,4 +1,4 @@ -import { Entity } from "./Entity.ts"; +import {Entity, EntityId} from "./Entity.ts"; export interface IEngineGlue { @@ -12,6 +12,7 @@ export interface IEngineGlue handleHoverNonClientArea(hover: boolean): void; onClickCreateEmptyEntity(): void; + setEntityVisibility(entityId: EntityId, isVisible: boolean): void; requestEntityHierarchyUpdate(): void; onUpdateEntities?: (entities: Entity[]) => void; @@ -60,6 +61,11 @@ class DevEngineGlue implements IEngineGlue { { logNotImplemented("onClickCreateEmptyEntity"); } + setEntityVisibility(entityId: EntityId, isVisible: boolean): void + { + logNotImplemented("onClickCreateEmptyEntity"); + console.log(`entityId ${entityId} isVisible ${isVisible}`) + } requestEntityHierarchyUpdate(): void { diff --git a/GlitchyEditor/EditorUI/src/Entity.ts b/GlitchyEditor/EditorUI/src/Entity.ts index 395b36b..07af822 100644 --- a/GlitchyEditor/EditorUI/src/Entity.ts +++ b/GlitchyEditor/EditorUI/src/Entity.ts @@ -1,4 +1,4 @@ -export type EntityId = number; +export type EntityId = string; export class Entity { @@ -7,10 +7,10 @@ export class Entity visible: boolean = true; children: EntityId[]; - constructor(name: string, key: number, children: EntityId[] = []) + constructor(name: string, id: EntityId, children: EntityId[] = []) { this.name = name; - this.id = key; + this.id = id; this.children = children; } } diff --git a/GlitchyEditor/EditorUI/src/Windows/EntityHierarchyWindow.tsx b/GlitchyEditor/EditorUI/src/Windows/EntityHierarchyWindow.tsx index ddf839b..cf6d311 100644 --- a/GlitchyEditor/EditorUI/src/Windows/EntityHierarchyWindow.tsx +++ b/GlitchyEditor/EditorUI/src/Windows/EntityHierarchyWindow.tsx @@ -1,8 +1,8 @@ -import {IDockviewPanelProps} from "dockview"; +import {IDockviewPanelProps} from "dockview"; import {MenuBar, MenuItem} from "../Components/Menu.tsx"; import "./EntityHierarchyWindow.css"; -import {MouseEvent, MouseEventHandler, ReactElement, useEffect, useState} from "react"; +import React, {MouseEvent, MouseEventHandler, ReactElement, useEffect, useState} from "react"; import IconVisible from "../assets/Icons/AntDesign/eye-visible.svg"; import IconInvisible from "../assets/Icons/AntDesign/eye-invisible.svg"; @@ -12,7 +12,7 @@ import {EngineGlue} from "../EngineGlue"; import {enableMapSet} from "immer" import {Entity, EntityId} from "../Entity.ts"; -import * as sea from "node:sea"; +import {IconCheckButton} from "../Components/IconCheckButton.tsx"; enableMapSet() @@ -25,10 +25,10 @@ type EntityMap = { const entityHierarchy: EntityMap = { selectedIds: new Set(), entities: new Map([ - [0, new Entity("Root", 0, [1, 2])], - [1, new Entity("Entity 1", 1, [])], - [2, new Entity("Entity 2", 2, [3])], - [3, new Entity("Entity 3 in 2", 3, [])] + ["0", new Entity("Root", "0", ["1", "2"])], + ["1", new Entity("Entity 1", "1", [])], + ["2", new Entity("Entity 2", "2", ["3"])], + ["3", new Entity("Entity 3 in 2", "3", [])] ]) // // 0 is hardcoded to be the root. The editor must provide this "pseudo"-entity. // 0: new Entity("Root", 0, [1, 2]), @@ -160,27 +160,24 @@ function VisibilityToggle({item, onClick, updateEntities} : {item: Entity, onCli if (entity !== undefined) { - entity.visible = !item.visible; + entity.visible = !entity.visible; + EngineGlue.setEntityVisibility(entity.id, entity.visible); } }); - console.log("Clickidy" + item.visible); - onClick(event); event.stopPropagation(); } - return ( -
- -
- ); + return } + + function EntityTree({items, updateItems, entityFilter}: { items: EntityMap, updateItems: Updater, entityFilter: string }) { - const rootEntity = items.entities.get(0); + const rootEntity = items.entities.get("0"); if (entityFilter.length == 0) { diff --git a/GlitchyEditor/EditorUI/src/Windows/MainWindow.tsx b/GlitchyEditor/EditorUI/src/Windows/MainWindow.tsx index 0b5e90b..7e919ce 100644 --- a/GlitchyEditor/EditorUI/src/Windows/MainWindow.tsx +++ b/GlitchyEditor/EditorUI/src/Windows/MainWindow.tsx @@ -4,6 +4,7 @@ import {MenuBar, MenuDivider, MenuItem} from "../Components/Menu.tsx"; import {ReactElement, useEffect, useRef} from "react"; import '../EngineGlue.ts' import {EngineGlue} from "../EngineGlue.ts"; +import {StartupCard} from "../Components/StartupCard.tsx"; //something = 'testing'; @@ -21,6 +22,7 @@ export default function MainWindow() { function TitleBar({ projectName, fileName } : {projectName: string, fileName: string}): ReactElement { + // {false ? "🗗" : "🗖"} return (
@@ -56,7 +58,7 @@ function MainMenuBar(): ReactElement { console.log("Test") }}> - + window.open("file:///index.html")}/> diff --git a/GlitchyEditor/src/Ultralight/JSObject.bf b/GlitchyEditor/src/Ultralight/JSObject.bf new file mode 100644 index 0000000..7f24915 --- /dev/null +++ b/GlitchyEditor/src/Ultralight/JSObject.bf @@ -0,0 +1,53 @@ +using Ultralight.CAPI; +using System; +namespace GlitchyEditor.Ultralight; + +class JSObject +{ + private JSContextRef _jsContext; + private JSObjectRef _jsObject; + + public JSObjectRef ObjectRef => _jsObject; + + private this(JSContextRef context) + { + _jsContext = context; + } + + public static JSObject CreateArray(JSContextRef context) + { + JSObject object = new JSObject(context); + + object._jsObject = JSObjectMakeArray(context, 0, null, null); + + return object; + } + + public JSObjectRef this[int index] + { + get => JSObjectGetPropertyAtIndex(_jsContext, _jsObject, (uint32)index, null); + set => JSObjectSetPropertyAtIndex(_jsContext, _jsObject, (uint32)index, value, null); + } +} + +class JSString +{ + private JSContextRef _context; + private JSStringRef _string; + + public this(JSContextRef context, JSValueRef jsValue) + { + _context = context; + _string = JSValueToStringCopy(context, jsValue, null); + } + + public ~this() + { + //JSStringRelease(_string); + } + + public void GetUTF8String(String buffer) + { + buffer.Append(Span((char16*)JSStringGetCharactersPtr(_string), JSStringGetLength(_string))); + } +} diff --git a/GlitchyEditor/src/Ultralight/UltralightHelper.bf b/GlitchyEditor/src/Ultralight/UltralightHelper.bf index 5b35f3d..5b43c31 100644 --- a/GlitchyEditor/src/Ultralight/UltralightHelper.bf +++ b/GlitchyEditor/src/Ultralight/UltralightHelper.bf @@ -49,4 +49,22 @@ static class UltralightHelper { return JSObjectMake(context, NativeFunctionClass(), Internal.UnsafeCastToPtr(ownCallback)); } + + public static JSObjectRef CreateObjectFromString(JSContextRef context, StringView string) + { + JSStringRef entityName = JSStringCreateWithUTF8CString(string.ToScopeCStr!()); + JSValueRef nameObject = JSValueMakeString(context, entityName); + JSStringRelease(entityName); + + return nameObject; + } + + public static JSObjectRef CreateObjectFromString(JSContextRef context, String string) + { + JSStringRef entityName = JSStringCreateWithUTF8CString(string.CStr()); + JSValueRef nameObject = JSValueMakeString(context, entityName); + JSStringRelease(entityName); + + return nameObject; + } } diff --git a/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf b/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf index 11dfc1a..78c96c2 100644 --- a/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf +++ b/GlitchyEditor/src/Ultralight/UltralightMainWindow.bf @@ -5,6 +5,8 @@ using GlitchyEngine.Collections; using GlitchyEngine.World; using System.Collections; using GlitchyEngine.Core; +using System.Reflection; +using GlitchyEngine.World.Components; using static GlitchyEngine.UI.Window; namespace GlitchyEditor.Ultralight; @@ -198,8 +200,10 @@ class UltralightMainWindow : UltralightWindow defer JSStringRelease(childrenProperty); JSObjectRef jsArray = JSObjectMakeArray(context, 0, null, null); - uint32 index = 0; + + String id = scope .(18); + for (let (entityId, operation) in updates) { if (operation == .None) @@ -208,25 +212,17 @@ class UltralightMainWindow : UltralightWindow } JSObjectRef jsEntity = JSObjectMake(context, null, null); - - /* - name: string; - id: EntityId; - visible: boolean = true; - children: EntityId[]; - */ - JSValueRef idObject = JSValueMakeNumber(context, (double)(uint64)entityId); + entityId.ToString(id..Clear()); + + JSValueRef idObject = UltralightHelper.CreateObjectFromString(context, id); JSObjectSetProperty(context, jsEntity, idProperty, idObject, 0, null); if (operation != .Delete) { EntityEntry entry = _entities[entityId]; - JSStringRef entityName = JSStringCreateWithUTF8CString(entry.Name); - JSValueRef nameObject = JSValueMakeString(context, entityName); - JSStringRelease(entityName); - + JSValueRef nameObject = UltralightHelper.CreateObjectFromString(context, entry.Name); JSObjectSetProperty(context, jsEntity, nameProperty, nameObject, 0, null); JSValueRef visibleObject = JSValueMakeBoolean(context, true); @@ -236,7 +232,8 @@ class UltralightMainWindow : UltralightWindow for (let childId in entry.Children) { - JSValueRef childIdObject = JSValueMakeNumber(context, (double)(uint64)childId); + childId.ToString(id..Clear()); + JSValueRef childIdObject = UltralightHelper.CreateObjectFromString(context, id); JSObjectSetPropertyAtIndex(context, childrenArray, (uint32)@childId.Index, childIdObject, null); } @@ -351,7 +348,52 @@ class UltralightMainWindow : UltralightWindow { _window.Close(); } + + [BindToJsFunction("setEntityVisibility")] + void HandleSetEntityVisibility(JSContextRef context, JSObjectRef thisObject, Span arguments, JSValueRef* exception = null) + { + if (arguments.Length != 2) + { + Log.EngineLogger.Error("EngineGlue.setEntityVisibility: called with wrong number of arguments."); + return; + } + + /*if (!JSValueIsNumber(context, arguments[0])) + { + Log.EngineLogger.Error($"EngineGlue.setEntityVisibility: expected number as argument 0, but received {JSValueGetType(context, arguments[0])} instead."); + return; + }*/ + + if (!JSValueIsBoolean(context, arguments[1])) + { + Log.EngineLogger.Error($"EngineGlue.setEntityVisibility: expected boolean as argument 1, but received {JSValueGetType(context, arguments[1])} instead."); + return; + } + + JSString jsId = scope .(context, arguments[0]); + String idString = scope .(); + jsId.GetUTF8String(idString); + + if (uint64.Parse(idString) case .Ok(let id)) + { + bool visible = JSValueToBoolean(context, arguments[1]); + UUID entityId = .((uint64)id); + + if (Editor.Instance.CurrentScene.GetEntityByID(entityId) case .Ok(let entity)) + { + if (entity.TryGetComponent(let flags)) + { + Enum.SetFlagConditionally(ref flags.Flags, .HideInScene, !visible); + } + } + else + { + Log.ClientLogger.Error($"Entity with ID {entityId} doesn't exist."); + } + } + } + [BindToJsFunction("callFromEngine_updateEntities")] private JsFunctionCall uiCallUpdateEntities ~ delete _; diff --git a/GlitchyEditor/src/Ultralight/UltralightWindow.bf b/GlitchyEditor/src/Ultralight/UltralightWindow.bf index 1225ddd..354cb5f 100644 --- a/GlitchyEditor/src/Ultralight/UltralightWindow.bf +++ b/GlitchyEditor/src/Ultralight/UltralightWindow.bf @@ -11,7 +11,7 @@ using GlitchyEngine.UI; namespace GlitchyEditor.Ultralight; -abstract class UltralightWindow +class UltralightWindow { protected internal Window _window ~ delete _; @@ -21,11 +21,13 @@ abstract class UltralightWindow private Texture2D _texture ~ _.ReleaseRef(); - protected this(StringView title = "GlitchyEngine ♥ Ultralight", StringView startUrl = "file:///app.html") + protected this(StringView title = "GlitchyEngine ♥ Ultralight", StringView startUrl = "file:///app.html", uint2 size = .Zero) { WindowDescription windowDesc = .Default; windowDesc.Title = title; windowDesc.WindowStyle = .CustomTitle; + windowDesc.Width = size.X; + windowDesc.Height = size.Y; _window = new Window(windowDesc); _window.EventCallback = new => EventHandler; @@ -68,6 +70,8 @@ abstract class UltralightWindow ulViewSetAddConsoleMessageCallback(_view, => OnAddConsoleMessage, userData); ulViewSetChangeCursorCallback(_view, => OnChangeCursor, userData); + + ulViewSetCreateChildViewCallback(_view, => OnCreateChildView, userData); } #endregion Init @@ -493,5 +497,14 @@ abstract class UltralightWindow Log.ClientLogger.Log(logLevel, prettyMessage); } + private static ULView OnCreateChildView(void* user_data, ULView caller, ULString opener_url, ULString target_url, bool is_popup, ULIntRect popup_rect) + { + StringView target = StringView(ulStringGetData(target_url), ulStringGetLength(target_url)); + + UltralightWindow newWindow = new UltralightWindow(startUrl: target, size: .((uint32)(popup_rect.right - popup_rect.left), (uint32)(popup_rect.bottom - popup_rect.top))); + UltralightLayer.Instance.[Friend]_windows.Add(newWindow); + return newWindow._view; + } + #endregion Ultralight Callbacks }