Compare commits

...

69 Commits

Author SHA1 Message Date
Lucien Greathouse
b84aab0960 Release v6.0.2 2021-02-09 11:45:58 -05:00
Lucien Greathouse
0e89b91c38 Implement CSRF challenge support in upload 2021-02-09 11:36:59 -05:00
Lucien Greathouse
7888a704e1 Release 6.0.1 2021-01-22 13:47:39 -07:00
Lucien Greathouse
804fd3de8e Remove Requester header to handle API change 2021-01-22 13:41:55 -07:00
Lucien Greathouse
4992c36f08 Delete ErrorDisplay, use anyhow instead! 2021-01-18 14:54:12 -07:00
Lucien Greathouse
27af0c841b Release 6.0.0 2021-01-16 18:35:27 -07:00
Lucien Greathouse
cc4f4df4f9 Support changing ClassName in Reconciler.diff 2021-01-16 18:26:30 -07:00
Lucien Greathouse
5989ab3b85 Add project to test that plugin skips unwritable properties 2021-01-16 15:34:59 -07:00
Lucien Greathouse
040b9c1452 Delete accidentally committed test bash script 2021-01-14 11:55:24 -07:00
Lucien Greathouse
bb7bd2e27e plugin: Update reflection database 2021-01-13 23:31:57 -07:00
tacheometry
02dbd4ba75 fix hyphen (#378)
This uses an em dash instead of double lines (--)
2021-01-06 13:49:05 -07:00
Lucien Greathouse
3b149cc875 Drop SnapshotError in favor of anyhow::Error 2020-12-18 12:16:05 -08:00
Lucien Greathouse
f3745c68d2 Turn rbxm/rbxmx errors into error variants 2020-12-18 11:39:41 -08:00
Lucien Greathouse
eddbe7d0cf Release 6.0.0-rc.4 2020-12-14 14:15:09 -08:00
Lucien Greathouse
faf86d006a Remove outdated warning from Rojo 0.5.0 alpha 14 2020-12-14 13:16:34 -08:00
Lucien Greathouse
71e4dfeb14 Change logo to be brand color on dark theme 2020-12-14 12:59:53 -08:00
Lucien Greathouse
da8ed6ddf9 Update Changelog 2020-12-14 11:22:43 -08:00
Lucien Greathouse
503e687c55 Fix repository link 2020-12-14 11:20:28 -08:00
Lucien Greathouse
dd667cce0b Add images from plugin rewrite 2020-12-14 11:20:07 -08:00
Lucien Greathouse
f911009752 Remove clibrojo, as it's broken 2020-12-14 11:13:44 -08:00
Reselim
cae4c46669 New UI (#367)
* Add Flipper

* Remove old UI

* Add boilerplate UI

* Change plugin version

* Merge upstream

* Bunch of new UI changes

Too lazy to list them all in individual commits

* Touch ripple for buttons and a few other things

* Make the close button on the PluginGui work

* Set button state to guiEnabled

* Implement Connecting, NotConnected; add Header; don't update plugin button on render

* Replace mapLerpColor with mapLerp

* Update blendAlpha to be 0 without any values

* Add ActionFillTransparency to Theme.Button

* Suffix all Theme entries

* Update Flipper

* Add disconnect button

* Remove cancel button

* Add settings page

* Add scrollbar and dark theme support to settings

* Include settings in startSession

* Set context default value to nil

I always thought this was the name, lol...

* Add Error page

* Fix preloadAssets

* Fix preloadAssets import

* Update checkbox colors a little

* Add setting descriptions

* Fix scrolling frame in settings panel

* Remove .vscode

* Rename Throbber to Spinner

* Update merge

* Move Spinner images to assets

* Change casing of directories

* Remove old directories

* Add comments to getDerivedStateFromProps

* Account for offset in host TextBox size

* Turn width variables into constants

* Attempt to fix the comments

* Add a missing comma in Settings

* Remove a double space

* Remove Dummy object

* Move most of the Studio logic out of render

* Don't truncate port input

* Replace merge with Dictionary.merge

* Replace "Got it!" with "Okay"

* Add projectName to setStatus call

* Add Flipper to build.rs
2020-12-14 11:07:39 -08:00
Lucien Greathouse
a937fc38db Update Changelog 2020-12-14 10:22:45 -08:00
Lucien Greathouse
ff43ffce07 Update to insta 1.3.0 2020-12-14 10:22:40 -08:00
Lucien Greathouse
5bb3dc258a Add projectName to serve output 2020-12-14 10:17:52 -08:00
Lucien Greathouse
98238e4516 Fix longstanding message_queue warning 2020-12-14 10:12:51 -08:00
Lucien Greathouse
23b8308282 Fix lockfile not being updated for 6.0.0-rc.3 2020-11-19 11:50:06 -08:00
Lucien Greathouse
a1f7cdc2b6 Release 6.0.0-rc.3 2020-11-19 11:47:07 -08:00
Lucien Greathouse
dcb30351c5 Fix null referent handling 2020-11-19 11:44:53 -08:00
Lucien Greathouse
1c0dc60071 Update CHANGELOG 2020-11-19 11:31:46 -08:00
Lucien Greathouse
0401c3ac0e Mark HttpEnabled as not scriptable as well 2020-11-19 11:20:30 -08:00
Lucien Greathouse
3b800d1cd7 Patch ReflectionDatabase to turn off SourceAssetId 2020-11-19 11:06:03 -08:00
Lucien Greathouse
35efb464e5 Fix incorrect tag in CHANGELOG 2020-11-19 10:33:09 -08:00
Lucien Greathouse
19a955a327 Release memofs 0.1.3 2020-11-19 10:26:37 -08:00
Lucien Greathouse
836b18e68a Release 6.0.0-rc.2 2020-11-19 10:25:25 -08:00
Lucien Greathouse
046dc0d598 plugin: Fix grammar in comments a bit :) 2020-11-16 11:49:30 -08:00
Lucien Greathouse
039d92ce78 plugin: Support ClassName changes in applyPatch 2020-11-16 11:44:56 -08:00
Lucien Greathouse
2136da15d6 plugin: Ensure InstanceMap deletes existing entries before inserting 2020-11-16 11:44:39 -08:00
Lucien Greathouse
e5041d80ef plugin: Fix warning in applyPatch.lua 2020-11-11 17:11:34 -08:00
Lucien Greathouse
f66860bdfe Break apart plugin reconciler (#332)
* Start splitting apart reconciler, with tests

* Reify children in reify

* Baseline hydrate implementation

* Remove debug print

* Scaffold out diff implementation, just supporting name changes

* invariant -> error in decodeValue

* Flesh out diff and add getProperty

* Clear out top-level reconciler interface, start updating code that touches it

* Address review feedback

* Add (experimental) Selene configuration

* Add emptiness checks to PatchSet, remove unimplement invert method

* Improve descendant destruction behavior in InstanceMap

* Track instanceId on all reify errors

* Base implementation of applyPatch, returning partial patches on failure

* Change reify to accept InstanceMap and insert instances into it

* Start testing applyPatch for removals

* Add test for applyPatch adding instances successfully and not

* Add , which is just error with formatting

* Correctly use new diff and applyPatch APIs

* Improve applyPatch logging and fix field name typo

* Better debug output when reify fails

* Print out unapplied patch in debug mode

* Don't write properties if their values are not different.

This was exposed trying to sync the Rojo plugin, which
has a gigantic ModuleScript in it with the reflection
database. This workaround was present in some form in
many versions of Rojo, and I guess we still need it.

This time, I actually documented why it's here so that
I don't forget for the umpteenth time...

* Add placeholder test that needs to happen still

* Introduce easier plugin testing, write applyPatch properties test

* Delete legacy get/setCanonicalProperty files

* Fix trying to remove numbers instead of instances

* Change applyPatch to return partial patches instead of binary success

* Work towards being able to decode and apply refs

* Add helpers for PatchSet assertions

* Apply refs in reify, test all cases

* Improve diagnostics when patches fail to apply

* Stop logging when destroying untracked instances, it's ok

* Remove read before setting property in applyPatch

* Fix diff thinking all properties are changed
2020-11-11 16:30:23 -08:00
Lucien Greathouse
50f0a2bd2e Update CLI dependencies 2020-10-29 10:36:20 -07:00
Lucien Greathouse
7cd9bd383e Update to latest reflection database 2020-10-29 10:35:55 -07:00
Lucien Greathouse
45a20a1633 Remove outdated notices 2020-09-09 17:39:24 -07:00
Lucien Greathouse
ec5b3f80ef Fix theme component error regression 2020-07-03 12:21:49 -07:00
Lucien Greathouse
3b257ea87a Update repo references after Roblox move 2020-06-23 11:55:46 -07:00
Lucien Greathouse
6b82cead9c Move from rojo-rbx org to Roblox org 2020-06-22 14:14:42 -07:00
cliffchapmanrbx
79ae4c52cd Enable CLA bot (#333) 2020-06-22 14:11:53 -07:00
Lucien Greathouse
a4616cda7d Fix test place's CharacterAutoLoads value 2020-06-20 22:16:35 -07:00
Lucien Greathouse
95648361be Recreate test place, just running in Studio 2020-06-20 21:51:26 -07:00
Lucien Greathouse
0c41e9c10b Depend on latest Rojo release from Rojo 2020-06-20 21:50:25 -07:00
Lucien Greathouse
61c7ef3cb0 plugin: lazily access settings() to help with testing 2020-06-20 21:50:14 -07:00
Lucien Greathouse
65898125d0 Update changelog 2020-06-17 23:14:24 -07:00
Lucien Greathouse
da05078ff3 Load project file from VFS instead of through fs.
Fixes #320.

Previously, the root project file was loaded via methods on Project
(which do not know about the VFS) instead of through the VFS like
all other disk access.

This meant that Rojo was unable to build its own plugin because
there is no project file on the real disk, only in the VFS.
2020-06-17 23:13:29 -07:00
Lucien Greathouse
badb5c3636 Stop redundantly adding ignore paths when starting ServeSession 2020-06-17 22:54:35 -07:00
Lucien Greathouse
9453588ab1 Load built-in plugin from absolute path to make errors more apparent 2020-06-17 22:54:13 -07:00
Lucien Greathouse
4cbb3874a4 Use anyhow error reporting instead of custom 2020-06-17 14:56:09 -07:00
Lucien Greathouse
940aff7ef4 Enable globIgnorePaths by default 2020-06-17 14:42:46 -07:00
Lucien Greathouse
a3edb93273 Update Changelog 2020-06-17 14:38:39 -07:00
Lucien Greathouse
782b054b1a Pass build watch argument into Vfs 2020-06-17 14:11:48 -07:00
Lucien Greathouse
fc27b2911e Allow turning off file watching in memofs.
Also preemptively bumping version to 0.1.3 so I don't forget on next release
2020-06-17 14:06:44 -07:00
Lucien Greathouse
486b067567 Flatten snapshot middleware to be much simpler (#324)
* First take at flattening middleware for simpler code and better perf

* Undo debug prints

* Fix using wrong path in snapshot_from_vfs

* Disable some broken tests

* Re-enable (mistakenly?) disabled CSV test

* Fix some tests

* Update project file tests

* Fix benchmark
2020-06-17 13:47:09 -07:00
Lucien Greathouse
bdd1afea57 Run CI on master and PRs to master only 2020-05-20 15:30:44 -07:00
Lucien Greathouse
5ccd02939b Replace rojo-test with regular tests folder again (#323)
* Replace rojo-test with regular tests folder again

* Bump MSRV to 1.43.1
2020-05-20 15:30:05 -07:00
Lucien Greathouse
ca5b8ab309 Restore improperly tested dependency on rojo from rojo-test 2020-05-20 11:56:34 -07:00
Lucien Greathouse
9481fdd38d Add missing Cargo.lock change 2020-05-02 21:47:36 -07:00
Lucien Greathouse
56bf6d282b Stop building Rojo in rojo-test, since it doesn't work as intended 2020-05-02 21:39:13 -07:00
Lucien Greathouse
5364c9c1bc Fix Lua string escaping.
Closes #314.
2020-04-16 12:00:02 -07:00
Lucien Greathouse
a4d4beeb97 Update default place template 2020-03-30 11:34:41 -07:00
Lucien Greathouse
30a01381be Fix malformed CSV files causing crashes; fixes #310 2020-03-30 11:12:20 -07:00
Lucien Greathouse
def99a9e4d Update release job to fix 6.0.0 release 2020-03-29 17:09:52 -07:00
222 changed files with 7836 additions and 4247 deletions

View File

@@ -1,9 +1,13 @@
name: CI
on:
pull_request:
push:
branches: ["*"]
branches:
- master
pull_request:
branches:
- master
jobs:
build:
@@ -12,7 +16,7 @@ jobs:
strategy:
matrix:
rust_version: [stable, "1.40.0"]
rust_version: [stable, "1.43.1"]
steps:
- uses: actions/checkout@v1
@@ -32,10 +36,4 @@ jobs:
run: |
cargo fmt -- --check
cargo clippy
if: matrix.rust_version == 'stable'
- name: Build (All Features)
run: cargo build --locked --verbose --all-features
- name: Run tests (All Features)
run: cargo test --locked --verbose --all-features
if: matrix.rust_version == 'stable'

View File

@@ -10,6 +10,8 @@ jobs:
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Build release binary
run: cargo build --verbose --locked --release
@@ -25,6 +27,8 @@ jobs:
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Install Rust
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
@@ -47,6 +51,8 @@ jobs:
steps:
- uses: actions/checkout@v1
with:
submodules: true
- name: Build
run: cargo build --locked --verbose --release

8
.gitignore vendored
View File

@@ -10,9 +10,15 @@
/*.rbxl
/*.rbxlx
# Test places for the Roblox Studio Plugin
/plugin/*.rbxlx
# Roblox Studio holds 'lock' files on places
*.rbxl.lock
*.rbxlx.lock
# Snapshot files from the 'insta' Rust crate
**/*.snap.new
**/*.snap.new
# Selene generates a roblox.toml file that should not be checked in.
/roblox.toml

5
.gitmodules vendored
View File

@@ -9,4 +9,7 @@
url = https://github.com/LPGhatguy/roblox-lua-promise.git
[submodule "plugin/modules/t"]
path = plugin/modules/t
url = https://github.com/osyrisrblx/t.git
url = https://github.com/osyrisrblx/t.git
[submodule "plugin/modules/flipper"]
path = plugin/modules/flipper
url = https://github.com/Reselim/Flipper

View File

@@ -33,7 +33,7 @@ stds.plugin = {
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP",
"it", "itFOCUS", "itSKIP", "itFIXME",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}

View File

@@ -2,6 +2,36 @@
## Unreleased Changes
## [6.0.2](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.2) (February 9, 2021)
* Fixed `rojo upload` to handle CSRF challenges.
## [6.0.1](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.1) (January 22, 2021)
* Fixed `rojo upload` requests being rejected by Roblox
## [6.0.0](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0) (January 16, 2021)
* Improved server error messages
* The server will now keep running in more error cases
* Fixed Rojo being unable to diff ClassName changes
## [6.0.0 Release Candidate 4](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0-rc.4) (December 14, 2020)
* Added brand new Rojo UI ([#367](https://github.com/rojo-rbx/rojo/pull/367))
* Added `projectName` to `/api/rojo` output.
## [6.0.0 Release Candidate 3](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0-rc.3) (November 19, 2020)
* Fixed the Rojo plugin attempting to write the non-scriptable properties `Instance.SourceAssetId` and `HttpServer.HttpEnabled`.
* Fixed the Rojo plugin's handling of null referents.
## [6.0.0 Release Candidate 2](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0-rc.2) (November 19, 2020)
* Fixed crash when malformed CSV files are put into a project. ([#310](https://github.com/rojo-rbx/rojo/issues/310))
* Fixed incorrect string escaping when producing Lua code from JSON files. ([#314](https://github.com/rojo-rbx/rojo/issues/314))
* Fixed performance issues introduced in Rojo 6.0.0-rc.1. ([#317](https://github.com/rojo-rbx/rojo/issues/317))
* Fixed `rojo plugin install` subcommand failing for everyone except Rojo developers. ([#320](https://github.com/rojo-rbx/rojo/issues/320))
* Updated default place template to take advantage of [#210](https://github.com/rojo-rbx/rojo/pull/210).
* Enabled glob ignore patterns by default and removed the `unstable_glob_ignore` feature.
* `globIgnorePaths` can be set on a project to a list of globs to ignore.
* The Rojo plugin now completes as much as it can from a patch without disconnecting. Warnings are shown in the console.
* Fixed 6.0.0-rc.1 regression causing instances that changed ClassName to instead... not change ClassName.
## [6.0.0 Release Candidate 1](https://github.com/rojo-rbx/rojo/releases/tag/v6.0.0-rc.1) (March 29, 2020)
This release jumped from 0.6.0 to 6.0.0. Rojo has been in use in production for many users for quite a long times, and so 6.0 is a more accurate reflection of Rojo's version than a pre-1.0 version.
@@ -122,7 +152,7 @@ This is a general maintenance release for the Rojo 0.5.x release series.
## [0.5.0 Alpha 9](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.9) (April 4, 2019)
* Changed `rojo build` to use buffered I/O, which can make it up to 2x faster in some cases.
* Building [*Road Not Taken*](https://github.com/rojo-rbx/roads) to an `rbxlx` file dropped from 150ms to 70ms on my machine
* Building [*Road Not Taken*](https://github.com/LPGhatguy/roads) to an `rbxlx` file dropped from 150ms to 70ms on my machine
* Fixed `LocalizationTable` instances being made from `csv` files incorrectly interpreting empty rows and columns. ([#149](https://github.com/rojo-rbx/rojo/pull/149))
* Fixed CSV files with entries that parse as numbers causing Rojo to panic. ([#152](https://github.com/rojo-rbx/rojo/pull/152))
* Improved error messages when malformed CSV files are found in a Rojo project.
@@ -319,4 +349,4 @@ This is a general maintenance release for the Rojo 0.5.x release series.
* More robust syncing with a new reconciler
## [0.1.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.1.0) (November 29, 2017)
* Initial release, functionally very similar to [rbxfs](https://github.com/rojo-rbx/rbxfs)
* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)

View File

@@ -1,5 +1,5 @@
# Contributing to the Rojo Project
Rojo is a big project and can always use more help! This guide covers all repositories underneath the [rojo-rbx organization on GitHub](https://github.com/rojo-rbx).
Rojo is a big project and can always use more help!
Some of the repositories covered are:
@@ -15,8 +15,7 @@ You'll want these tools to work on Rojo:
* Latest stable Rust compiler
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
* Latest stable [Remodel](https://github.com/rojo-rbx/remodel)
* Latest stable [run-in-roblox](https://github.com/rojo-rbx/run-in-roblox)
* [Foreman](https://github.com/Roblox/foreman)
## Documentation
Documentation impacts way more people than the individual lines of code we write.

1046
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "rojo"
version = "6.0.0-rc.1"
version = "6.0.2"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers"
license = "MPL-2.0"
@@ -23,23 +23,11 @@ panic = "abort"
[features]
default = []
# Turn on support for specifying glob ignore path rules in the project format.
unstable_glob_ignore_paths = []
# Enable this feature to live-reload assets from the web UI.
dev_live_assets = []
[workspace]
members = [
"rojo-test",
"rojo-insta-ext",
"clibrojo",
"memofs",
]
default-members = [
".",
"rojo-test",
"rojo-insta-ext",
"memofs",
]
@@ -97,7 +85,7 @@ uuid = { version = "0.8.1", features = ["v4", "serde"] }
winreg = "0.6.2"
[build-dependencies]
memofs = { version = "0.1.0", path = "memofs" }
memofs = { version = "0.1.3", path = "memofs" }
anyhow = "1.0.27"
bincode = "1.2.1"
@@ -108,7 +96,7 @@ maplit = "1.0.1"
rojo-insta-ext = { path = "rojo-insta-ext" }
criterion = "0.3"
insta = { version = "0.13.1", features = ["redactions"] }
insta = { version = "1.3.0", features = ["redactions"] }
lazy_static = "1.2"
paste = "0.1"
pretty_assertions = "0.6.1"

View File

@@ -48,7 +48,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome!
Rojo supports Rust 1.40.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
Rojo supports Rust 1.43.1 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
## License
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.

View File

@@ -4,27 +4,19 @@
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Common": {
"$path": "src/shared"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"Server": {
"$path": "src/server"
}
},
"StarterPlayer": {
"$className": "StarterPlayer",
"StarterPlayerScripts": {
"$className": "StarterPlayerScripts",
"Client": {
"$path": "src/client"
}
@@ -32,7 +24,6 @@
},
"Workspace": {
"$className": "Workspace",
"$properties": {
"FilteringEnabled": true
},
@@ -60,7 +51,6 @@
}
},
"Lighting": {
"$className": "Lighting",
"$properties": {
"Ambient": [
0,
@@ -74,16 +64,9 @@
}
},
"SoundService": {
"$className": "SoundService",
"$properties": {
"RespectFilteringEnabled": true
}
},
"HttpService": {
"$className": "HttpService",
"$properties": {
"HttpEnabled": true
}
}
}
}

BIN
assets/images/back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

5
assets/images/back.svg Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M20,11L20,13L8,13L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11L20,11Z" style="fill:white;fill-rule:nonzero;"/>
</svg>

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g transform="matrix(1,0,0,1,-54,-1)">
<g id="Artboard3" transform="matrix(1.77778,0,0,1.45455,-42,-0.454545)">
<rect x="54" y="1" width="9" height="11" style="fill:none;"/>
<g transform="matrix(3.375,0,0,4.125,-3654,-2753.12)">
<path d="M1099,670L1101,668" style="fill:none;stroke:white;stroke-width:0.5px;"/>
<g transform="matrix(-1,0,0,1,2200,0)">
<path d="M1099,670L1101,668" style="fill:none;stroke:white;stroke-width:0.5px;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g transform="matrix(1,0,0,1,-32,0)">
<g id="Artboard2" transform="matrix(0.8,0,0,0.941176,6.4,0)">
<rect x="32" y="0" width="20" height="17" style="fill:none;"/>
<g transform="matrix(5,0,0,4.25,-5470.5,-2371.5)">
<path d="M1101,560L1102,561L1104,559" style="fill:none;stroke:white;stroke-width:0.75px;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/images/circle-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

BIN
assets/images/circle-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
assets/images/circle-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

BIN
assets/images/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

15
assets/images/close.svg Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g id="Artboard1" transform="matrix(0.0666667,0,0,0.097561,-31,-18.6341)">
<rect x="465" y="191" width="360" height="246" style="fill:none;"/>
<g transform="matrix(134.328,0,0,102.5,-74228.5,-15214.7)">
<g transform="matrix(1.11667,0,0,1,-57.3333,0)">
<path d="M551,152L550,151" style="fill:none;stroke:white;stroke-width:0.3px;"/>
</g>
<g transform="matrix(1.11667,0,0,1,-57.3333,0)">
<path d="M550,152L551,151" style="fill:none;stroke:white;stroke-width:0.3px;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 60 27" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Artboard1" transform="matrix(0.952381,0,0,0.710526,-228.571,-156.316)">
<rect x="240" y="220" width="63" height="38" style="fill:none;"/>
<g transform="matrix(0.0789166,0,0,0.105779,211.848,170.749)">
<g transform="matrix(340.635,0,0,340.635,376,753)">
<path d="M0.302,-0.836L0.306,-0.836L0.302,-0.829L0.302,-0.814C0.333,-0.82 0.349,-0.828 0.349,-0.836L0.371,-0.833C0.408,-0.835 0.441,-0.836 0.472,-0.836L0.476,-0.836C0.524,-0.836 0.58,-0.81 0.646,-0.757C0.66,-0.734 0.668,-0.714 0.672,-0.695L0.672,-0.659C0.664,-0.609 0.65,-0.571 0.632,-0.546C0.621,-0.537 0.597,-0.507 0.56,-0.456L0.556,-0.456L0.556,-0.463L0.566,-0.478L0.56,-0.478C0.474,-0.405 0.378,-0.349 0.27,-0.311C0.243,-0.304 0.216,-0.3 0.19,-0.3C0.224,-0.26 0.287,-0.207 0.378,-0.141C0.387,-0.136 0.445,-0.099 0.552,-0.028C0.641,0.027 0.747,0.087 0.871,0.153L0.871,0.157L0.856,0.157C0.832,0.148 0.82,0.141 0.82,0.138L0.813,0.142L0.806,0.142C0.803,0.142 0.802,0.141 0.802,0.138C0.793,0.14 0.786,0.149 0.78,0.164C0.795,0.168 0.802,0.173 0.802,0.178C0.796,0.183 0.792,0.186 0.788,0.186L0.77,0.182L0.77,0.186C0.77,0.19 0.773,0.193 0.78,0.193L0.78,0.204L0.777,0.204C0.772,0.204 0.717,0.172 0.614,0.109C0.541,0.072 0.451,0.015 0.346,-0.061C0.311,-0.077 0.247,-0.124 0.153,-0.202L0.143,-0.202C0.129,-0.181 0.111,-0.129 0.088,-0.046C0.083,-0.027 0.059,0.001 0.016,0.037L0.008,0.037L0.001,0.026L0.001,0.022C0.001,0.017 0.005,0.006 0.012,-0.01L0.012,-0.014L0.008,-0.014C0.008,-0.007 -0.007,0.004 -0.035,0.019L-0.039,0.019L-0.039,0.012C0.046,-0.24 0.105,-0.404 0.139,-0.481C0.197,-0.614 0.226,-0.69 0.226,-0.709C0.213,-0.709 0.184,-0.693 0.139,-0.659L0.132,-0.659L0.135,-0.666L0.135,-0.673C0.122,-0.673 0.092,-0.652 0.045,-0.608L0.041,-0.608L0.041,-0.612L0.088,-0.666L0.096,-0.677L0.096,-0.681L0.088,-0.681L0.045,-0.645C0.04,-0.645 0.038,-0.647 0.038,-0.651C0.083,-0.701 0.138,-0.744 0.204,-0.778C0.265,-0.794 0.295,-0.812 0.295,-0.833L0.302,-0.836ZM0.632,-0.735L0.632,-0.731C0.632,-0.727 0.635,-0.724 0.639,-0.724L0.639,-0.728C0.639,-0.732 0.637,-0.735 0.632,-0.735ZM0.208,-0.387L0.211,-0.387C0.255,-0.396 0.277,-0.403 0.277,-0.409C0.274,-0.414 0.273,-0.417 0.273,-0.42C0.365,-0.451 0.427,-0.482 0.458,-0.514C0.461,-0.514 0.48,-0.534 0.516,-0.576L0.52,-0.576L0.52,-0.572C0.515,-0.564 0.512,-0.558 0.512,-0.554L0.516,-0.554C0.547,-0.578 0.563,-0.604 0.563,-0.633L0.563,-0.655C0.556,-0.655 0.552,-0.658 0.552,-0.663C0.564,-0.665 0.57,-0.671 0.57,-0.681C0.57,-0.693 0.546,-0.705 0.498,-0.717C0.459,-0.727 0.417,-0.731 0.371,-0.731C0.348,-0.731 0.325,-0.689 0.302,-0.604C0.292,-0.594 0.261,-0.522 0.208,-0.387ZM0.251,-0.695L0.251,-0.691C0.255,-0.691 0.258,-0.695 0.262,-0.702L0.262,-0.706C0.259,-0.706 0.255,-0.702 0.251,-0.695ZM0.255,-0.626L0.259,-0.626C0.266,-0.629 0.27,-0.636 0.27,-0.648C0.266,-0.648 0.261,-0.641 0.255,-0.626ZM0.596,-0.612L0.596,-0.604C0.599,-0.604 0.603,-0.608 0.606,-0.615L0.606,-0.619L0.603,-0.619C0.598,-0.618 0.596,-0.616 0.596,-0.612ZM0.204,-0.369L0.204,-0.365L0.211,-0.365C0.216,-0.368 0.22,-0.369 0.222,-0.369L0.222,-0.365C0.281,-0.376 0.316,-0.388 0.328,-0.401L0.324,-0.401C0.271,-0.389 0.231,-0.378 0.204,-0.369ZM0.19,-0.333L0.193,-0.333L0.259,-0.347L0.255,-0.354C0.212,-0.349 0.19,-0.341 0.19,-0.333ZM0.334,-0.108L0.334,-0.101C0.372,-0.072 0.395,-0.057 0.403,-0.057C0.394,-0.064 0.389,-0.07 0.389,-0.075L0.334,-0.108ZM0.44,-0.028C0.442,-0.019 0.449,-0.014 0.461,-0.014L0.465,-0.014L0.465,-0.018C0.459,-0.018 0.452,-0.021 0.443,-0.028L0.44,-0.028ZM0.675,0.102C0.694,0.119 0.723,0.136 0.762,0.153L0.766,0.146C0.736,0.128 0.708,0.113 0.683,0.102L0.675,0.102ZM0.875,0.16L0.886,0.16C0.89,0.161 0.893,0.163 0.893,0.167C0.888,0.167 0.886,0.171 0.886,0.178L0.878,0.178L0.882,0.171L0.882,0.167L0.875,0.167L0.875,0.16Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(340.635,0,0,340.635,588.398,753)">
<path d="M0.631,-0.705C0.646,-0.705 0.661,-0.696 0.673,-0.678L0.646,-0.678C0.62,-0.678 0.587,-0.67 0.546,-0.653L0.546,-0.65L0.549,-0.65C0.607,-0.654 0.649,-0.656 0.677,-0.656L0.683,-0.656L0.683,-0.65L0.67,-0.65C0.668,-0.65 0.667,-0.651 0.667,-0.653L0.503,-0.629C0.503,-0.627 0.493,-0.624 0.473,-0.62L0.473,-0.614L0.519,-0.623L0.521,-0.623L0.521,-0.617C0.386,-0.581 0.318,-0.555 0.318,-0.538C0.252,-0.483 0.204,-0.431 0.175,-0.383C0.131,-0.304 0.108,-0.243 0.108,-0.201C0.108,-0.157 0.131,-0.125 0.175,-0.104C0.183,-0.102 0.19,-0.101 0.196,-0.101C0.286,-0.136 0.353,-0.196 0.397,-0.28C0.409,-0.318 0.419,-0.363 0.427,-0.417L0.434,-0.417L0.427,-0.359C0.433,-0.359 0.439,-0.374 0.443,-0.404C0.441,-0.412 0.439,-0.42 0.439,-0.429L0.439,-0.438L0.446,-0.438C0.448,-0.407 0.449,-0.386 0.449,-0.374C0.449,-0.358 0.444,-0.331 0.434,-0.292C0.445,-0.302 0.458,-0.335 0.473,-0.392C0.48,-0.447 0.486,-0.474 0.491,-0.474C0.491,-0.472 0.492,-0.471 0.494,-0.471C0.492,-0.469 0.49,-0.457 0.488,-0.435L0.488,-0.432L0.491,-0.432C0.499,-0.454 0.504,-0.48 0.506,-0.508C0.502,-0.516 0.5,-0.522 0.5,-0.526L0.506,-0.529C0.512,-0.529 0.518,-0.513 0.525,-0.48C0.525,-0.4 0.491,-0.297 0.424,-0.17C0.372,-0.093 0.305,-0.042 0.224,-0.019C0.188,-0.006 0.154,-0 0.121,-0C0.04,-0.022 -0.001,-0.083 -0.001,-0.183C-0.001,-0.296 0.045,-0.398 0.136,-0.489C0.165,-0.522 0.227,-0.568 0.321,-0.629C0.425,-0.68 0.529,-0.705 0.631,-0.705ZM0.482,-0.656L0.482,-0.653C0.499,-0.653 0.508,-0.652 0.509,-0.65C0.526,-0.659 0.536,-0.663 0.54,-0.663L0.54,-0.668L0.482,-0.656ZM0.099,-0.417C0.144,-0.458 0.179,-0.494 0.206,-0.523C0.23,-0.538 0.242,-0.55 0.242,-0.559C0.201,-0.535 0.156,-0.493 0.108,-0.432C0.102,-0.427 0.099,-0.422 0.099,-0.417ZM0.105,-0.261C0.108,-0.261 0.118,-0.284 0.136,-0.328C0.158,-0.378 0.203,-0.439 0.272,-0.511L0.272,-0.514C0.204,-0.463 0.167,-0.422 0.16,-0.392C0.142,-0.371 0.124,-0.328 0.105,-0.261ZM0.069,-0.334L0.069,-0.332L0.072,-0.332C0.116,-0.393 0.139,-0.429 0.139,-0.438C0.11,-0.413 0.086,-0.378 0.069,-0.334ZM0.467,-0.31L0.467,-0.307L0.47,-0.307C0.492,-0.371 0.503,-0.413 0.503,-0.432C0.49,-0.409 0.478,-0.369 0.467,-0.31ZM0.087,-0.404L0.087,-0.401C0.091,-0.401 0.093,-0.403 0.093,-0.407L0.093,-0.41C0.089,-0.41 0.087,-0.408 0.087,-0.404ZM0.063,-0.368C0.032,-0.297 0.017,-0.245 0.017,-0.213C0.021,-0.148 0.028,-0.107 0.039,-0.088L0.042,-0.088L0.042,-0.094C0.035,-0.107 0.032,-0.129 0.032,-0.158C0.032,-0.191 0.035,-0.227 0.042,-0.268L0.039,-0.273C0.045,-0.3 0.054,-0.331 0.066,-0.365L0.066,-0.368L0.063,-0.368ZM0.096,-0.24L0.096,-0.237C0.1,-0.237 0.103,-0.239 0.103,-0.243L0.103,-0.246C0.098,-0.245 0.096,-0.243 0.096,-0.24ZM0.397,-0.222L0.397,-0.219L0.4,-0.219C0.408,-0.232 0.412,-0.241 0.412,-0.243C0.407,-0.242 0.402,-0.235 0.397,-0.222ZM0.066,-0.24C0.06,-0.222 0.055,-0.202 0.051,-0.179C0.055,-0.138 0.061,-0.108 0.069,-0.088L0.072,-0.088L0.072,-0.094C0.066,-0.121 0.063,-0.139 0.063,-0.146L0.066,-0.152C0.064,-0.155 0.063,-0.16 0.063,-0.164C0.063,-0.169 0.064,-0.173 0.066,-0.176C0.064,-0.18 0.063,-0.183 0.063,-0.186C0.067,-0.218 0.069,-0.235 0.069,-0.237L0.069,-0.24L0.066,-0.24ZM0.294,-0.125L0.294,-0.122C0.3,-0.122 0.322,-0.143 0.357,-0.186L0.357,-0.188L0.354,-0.188C0.314,-0.151 0.294,-0.129 0.294,-0.125ZM0.379,-0.149C0.379,-0.146 0.36,-0.126 0.321,-0.088L0.324,-0.088C0.351,-0.112 0.37,-0.132 0.382,-0.146L0.385,-0.146L0.385,-0.149L0.379,-0.149ZM0.09,-0.104L0.09,-0.101C0.094,-0.08 0.104,-0.07 0.121,-0.07L0.13,-0.07L0.13,-0.073L0.09,-0.104ZM0.151,-0.055L0.188,-0.055L0.203,-0.058L0.203,-0.061L0.157,-0.061C0.153,-0.06 0.151,-0.058 0.151,-0.055ZM0.096,-0.058L0.096,-0.055C0.104,-0.045 0.113,-0.04 0.124,-0.04L0.13,-0.04L0.13,-0.042C0.127,-0.042 0.116,-0.048 0.096,-0.058Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(340.635,0,0,340.635,758.882,753)">
<path d="M0.208,-0.7L0.223,-0.7C0.227,-0.7 0.229,-0.697 0.229,-0.691C0.294,-0.687 0.328,-0.684 0.331,-0.682C0.333,-0.682 0.333,-0.683 0.333,-0.685C0.364,-0.683 0.387,-0.682 0.401,-0.682L0.462,-0.682C0.466,-0.682 0.468,-0.684 0.468,-0.688L0.45,-0.688L0.45,-0.694L0.49,-0.694L0.49,-0.688L0.475,-0.688L0.475,-0.682L0.557,-0.682C0.564,-0.682 0.573,-0.681 0.584,-0.679C0.603,-0.681 0.612,-0.685 0.612,-0.691L0.658,-0.691C0.678,-0.691 0.688,-0.692 0.689,-0.694C0.706,-0.692 0.72,-0.691 0.731,-0.691L0.903,-0.691L0.921,-0.688L0.928,-0.691C1.035,-0.686 1.096,-0.681 1.111,-0.676L1.13,-0.682L1.13,-0.676L1.068,-0.667L1.068,-0.664C1.075,-0.664 1.078,-0.661 1.078,-0.657C1.078,-0.654 1.075,-0.651 1.068,-0.648C1.064,-0.65 1.061,-0.651 1.059,-0.651C1.059,-0.647 1.055,-0.645 1.047,-0.645L1.047,-0.642C1.048,-0.638 1.05,-0.636 1.053,-0.636L1.059,-0.636C1.072,-0.636 1.087,-0.635 1.105,-0.633C1.113,-0.635 1.12,-0.636 1.126,-0.636C1.139,-0.625 1.145,-0.619 1.145,-0.618C1.138,-0.613 1.132,-0.611 1.126,-0.611L1.099,-0.611C1.088,-0.611 1.079,-0.608 1.071,-0.602L1.029,-0.602C1.024,-0.602 1.02,-0.603 1.017,-0.605L1.01,-0.602C0.993,-0.604 0.98,-0.605 0.971,-0.605C0.962,-0.605 0.958,-0.604 0.958,-0.602L0.928,-0.605L0.695,-0.605C0.69,-0.605 0.686,-0.604 0.683,-0.602C0.679,-0.604 0.675,-0.605 0.67,-0.605L0.563,-0.605C0.555,-0.605 0.547,-0.604 0.539,-0.602C0.531,-0.604 0.523,-0.605 0.514,-0.605L0.453,-0.605C0.429,-0.578 0.401,-0.525 0.37,-0.446C0.37,-0.439 0.334,-0.358 0.263,-0.201L0.177,-0.021C0.185,-0.001 0.189,0.011 0.189,0.013C0.186,0.013 0.177,0.001 0.162,-0.023L0.156,-0.023L0.153,-0.008C0.159,-0.001 0.162,0.006 0.162,0.013L0.162,0.017L0.15,0.007L0.128,0.007C0.128,-0.005 0.108,-0.045 0.067,-0.112C0.012,-0.235 -0.028,-0.332 -0.052,-0.403L-0.052,-0.412C-0.048,-0.412 -0.046,-0.409 -0.046,-0.403L-0.043,-0.403C-0.039,-0.403 -0.037,-0.405 -0.037,-0.409L-0.046,-0.443L-0.046,-0.446L-0.04,-0.446L0.034,-0.293L0.037,-0.293L0.037,-0.296C0.008,-0.37 -0.006,-0.41 -0.006,-0.416L-0.006,-0.421C0.009,-0.398 0.037,-0.343 0.076,-0.256C0.118,-0.177 0.142,-0.135 0.147,-0.13C0.212,-0.285 0.285,-0.442 0.364,-0.602C0.36,-0.602 0.358,-0.604 0.358,-0.608L0.352,-0.605L0.333,-0.605C0.332,-0.605 0.331,-0.606 0.331,-0.608C0.327,-0.606 0.324,-0.605 0.321,-0.605L0.315,-0.608C0.313,-0.608 0.312,-0.607 0.312,-0.605L0.291,-0.608L0.263,-0.608C0.263,-0.615 0.261,-0.618 0.257,-0.618L0.254,-0.618C0.247,-0.618 0.215,-0.621 0.159,-0.626L0.159,-0.63L0.162,-0.636L0.156,-0.636L0.156,-0.639C0.156,-0.641 0.163,-0.646 0.177,-0.654L0.184,-0.654L0.189,-0.651C0.189,-0.653 0.191,-0.659 0.193,-0.669C0.188,-0.671 0.178,-0.674 0.162,-0.676L0.162,-0.682L0.184,-0.682C0.191,-0.682 0.199,-0.688 0.208,-0.7ZM0.291,-0.697L0.352,-0.697C0.356,-0.697 0.358,-0.694 0.358,-0.691L0.291,-0.691L0.291,-0.697ZM0.41,-0.694L0.419,-0.694L0.419,-0.688L0.41,-0.688L0.41,-0.694ZM0.505,-0.694L0.533,-0.694L0.533,-0.688L0.505,-0.688L0.505,-0.694ZM0.603,-0.694L0.603,-0.691C0.603,-0.687 0.601,-0.685 0.597,-0.685L0.563,-0.685L0.563,-0.691L0.588,-0.691L0.603,-0.694ZM0.202,-0.667L0.202,-0.664L0.239,-0.664L0.239,-0.667L0.202,-0.667ZM0.269,-0.339L0.266,-0.327L0.266,-0.324L0.269,-0.324L0.272,-0.336L0.272,-0.339L0.269,-0.339ZM0.257,-0.308C0.237,-0.266 0.211,-0.205 0.177,-0.125C0.195,-0.147 0.223,-0.207 0.26,-0.305L0.26,-0.308L0.257,-0.308Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(340.635,0,0,340.635,891.444,753)">
<path d="M0.631,-0.705C0.646,-0.705 0.661,-0.696 0.673,-0.678L0.646,-0.678C0.62,-0.678 0.587,-0.67 0.546,-0.653L0.546,-0.65L0.549,-0.65C0.607,-0.654 0.649,-0.656 0.677,-0.656L0.683,-0.656L0.683,-0.65L0.67,-0.65C0.668,-0.65 0.667,-0.651 0.667,-0.653L0.503,-0.629C0.503,-0.627 0.493,-0.624 0.473,-0.62L0.473,-0.614L0.519,-0.623L0.521,-0.623L0.521,-0.617C0.386,-0.581 0.318,-0.555 0.318,-0.538C0.252,-0.483 0.204,-0.431 0.175,-0.383C0.131,-0.304 0.108,-0.243 0.108,-0.201C0.108,-0.157 0.131,-0.125 0.175,-0.104C0.183,-0.102 0.19,-0.101 0.196,-0.101C0.286,-0.136 0.353,-0.196 0.397,-0.28C0.409,-0.318 0.419,-0.363 0.427,-0.417L0.434,-0.417L0.427,-0.359C0.433,-0.359 0.439,-0.374 0.443,-0.404C0.441,-0.412 0.439,-0.42 0.439,-0.429L0.439,-0.438L0.446,-0.438C0.448,-0.407 0.449,-0.386 0.449,-0.374C0.449,-0.358 0.444,-0.331 0.434,-0.292C0.445,-0.302 0.458,-0.335 0.473,-0.392C0.48,-0.447 0.486,-0.474 0.491,-0.474C0.491,-0.472 0.492,-0.471 0.494,-0.471C0.492,-0.469 0.49,-0.457 0.488,-0.435L0.488,-0.432L0.491,-0.432C0.499,-0.454 0.504,-0.48 0.506,-0.508C0.502,-0.516 0.5,-0.522 0.5,-0.526L0.506,-0.529C0.512,-0.529 0.518,-0.513 0.525,-0.48C0.525,-0.4 0.491,-0.297 0.424,-0.17C0.372,-0.093 0.305,-0.042 0.224,-0.019C0.188,-0.006 0.154,-0 0.121,-0C0.04,-0.022 -0.001,-0.083 -0.001,-0.183C-0.001,-0.296 0.045,-0.398 0.136,-0.489C0.165,-0.522 0.227,-0.568 0.321,-0.629C0.425,-0.68 0.529,-0.705 0.631,-0.705ZM0.482,-0.656L0.482,-0.653C0.499,-0.653 0.508,-0.652 0.509,-0.65C0.526,-0.659 0.536,-0.663 0.54,-0.663L0.54,-0.668L0.482,-0.656ZM0.099,-0.417C0.144,-0.458 0.179,-0.494 0.206,-0.523C0.23,-0.538 0.242,-0.55 0.242,-0.559C0.201,-0.535 0.156,-0.493 0.108,-0.432C0.102,-0.427 0.099,-0.422 0.099,-0.417ZM0.105,-0.261C0.108,-0.261 0.118,-0.284 0.136,-0.328C0.158,-0.378 0.203,-0.439 0.272,-0.511L0.272,-0.514C0.204,-0.463 0.167,-0.422 0.16,-0.392C0.142,-0.371 0.124,-0.328 0.105,-0.261ZM0.069,-0.334L0.069,-0.332L0.072,-0.332C0.116,-0.393 0.139,-0.429 0.139,-0.438C0.11,-0.413 0.086,-0.378 0.069,-0.334ZM0.467,-0.31L0.467,-0.307L0.47,-0.307C0.492,-0.371 0.503,-0.413 0.503,-0.432C0.49,-0.409 0.478,-0.369 0.467,-0.31ZM0.087,-0.404L0.087,-0.401C0.091,-0.401 0.093,-0.403 0.093,-0.407L0.093,-0.41C0.089,-0.41 0.087,-0.408 0.087,-0.404ZM0.063,-0.368C0.032,-0.297 0.017,-0.245 0.017,-0.213C0.021,-0.148 0.028,-0.107 0.039,-0.088L0.042,-0.088L0.042,-0.094C0.035,-0.107 0.032,-0.129 0.032,-0.158C0.032,-0.191 0.035,-0.227 0.042,-0.268L0.039,-0.273C0.045,-0.3 0.054,-0.331 0.066,-0.365L0.066,-0.368L0.063,-0.368ZM0.096,-0.24L0.096,-0.237C0.1,-0.237 0.103,-0.239 0.103,-0.243L0.103,-0.246C0.098,-0.245 0.096,-0.243 0.096,-0.24ZM0.397,-0.222L0.397,-0.219L0.4,-0.219C0.408,-0.232 0.412,-0.241 0.412,-0.243C0.407,-0.242 0.402,-0.235 0.397,-0.222ZM0.066,-0.24C0.06,-0.222 0.055,-0.202 0.051,-0.179C0.055,-0.138 0.061,-0.108 0.069,-0.088L0.072,-0.088L0.072,-0.094C0.066,-0.121 0.063,-0.139 0.063,-0.146L0.066,-0.152C0.064,-0.155 0.063,-0.16 0.063,-0.164C0.063,-0.169 0.064,-0.173 0.066,-0.176C0.064,-0.18 0.063,-0.183 0.063,-0.186C0.067,-0.218 0.069,-0.235 0.069,-0.237L0.069,-0.24L0.066,-0.24ZM0.294,-0.125L0.294,-0.122C0.3,-0.122 0.322,-0.143 0.357,-0.186L0.357,-0.188L0.354,-0.188C0.314,-0.151 0.294,-0.129 0.294,-0.125ZM0.379,-0.149C0.379,-0.146 0.36,-0.126 0.321,-0.088L0.324,-0.088C0.351,-0.112 0.37,-0.132 0.382,-0.146L0.385,-0.146L0.385,-0.149L0.379,-0.149ZM0.09,-0.104L0.09,-0.101C0.094,-0.08 0.104,-0.07 0.121,-0.07L0.13,-0.07L0.13,-0.073L0.09,-0.104ZM0.151,-0.055L0.188,-0.055L0.203,-0.058L0.203,-0.061L0.157,-0.061C0.153,-0.06 0.151,-0.058 0.151,-0.055ZM0.096,-0.058L0.096,-0.055C0.104,-0.045 0.113,-0.04 0.124,-0.04L0.13,-0.04L0.13,-0.042C0.127,-0.042 0.116,-0.048 0.096,-0.058Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 9 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0,-10)">
<g id="Bottom" transform="matrix(0.243243,0,0,0.179487,-59.1081,-30.2051)">
<rect x="243" y="224" width="37" height="39" style="fill:none;"/>
<clipPath id="_clip1">
<rect x="243" y="224" width="37" height="39"/>
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(-4.11111,6.82303e-16,-3.59619e-16,-3.97959,280,259.816)">
<path d="M7,5.5C7,3.568 5.88,2 4.5,2C3.12,2 2,3.568 2,5.5L2,9L7,9L7,5.5Z" style="fill:white;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 9 1" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0,-8)">
<g id="Middle" transform="matrix(1,0,0,0.111111,0,7.11111)">
<rect x="0" y="8" width="9" height="9" style="fill:none;"/>
<g transform="matrix(1,0,0,9,0,-64)">
<rect x="2" y="8" width="5" height="1" style="fill:white;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 9 7" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Top" transform="matrix(0.243243,0,0,0.179487,-59.1081,-40.2051)">
<rect x="243" y="224" width="37" height="39" style="fill:none;"/>
<g transform="matrix(4.11111,0,0,3.97959,243,227.184)">
<path d="M7,5.5C7,3.568 5.88,2 4.5,2C3.12,2 2,3.568 2,5.5L2,9L7,9L7,5.5Z" style="fill:white;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Artboard1" transform="matrix(0.5,0,0,0.5,0,0)">
<rect x="0" y="0" width="48" height="48" style="fill:none;"/>
<path d="M24,0C37.246,0 48,10.754 48,24C48,37.246 37.246,48 24,48C10.754,48 0,37.246 0,24C0,10.754 10.754,0 24,0ZM24,8.4C32.61,8.4 39.6,15.39 39.6,24C39.6,32.61 32.61,39.6 24,39.6C15.39,39.6 8.4,32.61 8.4,24C8.4,15.39 15.39,8.4 24,8.4Z" style="fill:white;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Artboard1" transform="matrix(0.5,0,0,0.5,0,0)">
<rect x="0" y="0" width="48" height="48" style="fill:none;"/>
<path d="M48,24C48,10.745 37.255,0 24,0L24,8.4C32.616,8.4 39.6,15.384 39.6,24L48,24Z" style="fill:white;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 704 B

View File

@@ -35,6 +35,7 @@ fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
let options = BuildCommand {
project: input,
watch: false,
output,
};

View File

@@ -62,6 +62,9 @@ fn main() -> Result<(), anyhow::Error> {
"t" => VfsSnapshot::dir(hashmap! {
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
}),
"flipper" => VfsSnapshot::dir(hashmap! {
"src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))?
}),
}),
});

View File

@@ -1,13 +0,0 @@
[package]
name = "clibrojo"
version = "0.1.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rojo = { path = ".." }

View File

@@ -1,19 +0,0 @@
# Rojo as a C Library
This is an experiment to expose a C API for Rojo that would be suitable for embedding it into an existing C/C++ application.
I'm hoping to expand it to drop the HTTP layer and communicate through a channel, which could make it feasible to embed into an existing Roblox IDE with minimal changes or additional code.
## Building
This project is currently not built by default and could break/disappear at any time.
```bash
cargo build -p clibrojo
```
On Windows, Cargo will generate a `clibrojo.dll` and associated `.lib` file. Link these into your project.
To generate the associated C header file to include in the project, use [cbindgen](https://github.com/eqrion/cbindgen):
```bash
cbindgen --crate clibrojo --output include/rojo.h
```

View File

@@ -1,14 +0,0 @@
use std::{ffi::CStr, os::raw::c_char, path::PathBuf};
use librojo::commands::{serve, ServeOptions};
#[no_mangle]
pub extern "C" fn rojo_serve(path: *const c_char) {
let path = unsafe { PathBuf::from(CStr::from_ptr(path).to_str().unwrap()) };
serve(&ServeOptions {
fuzzy_project_path: path,
port: None,
})
.unwrap();
}

3
foreman.toml Normal file
View File

@@ -0,0 +1,3 @@
[tools]
rojo = { source = "rojo-rbx/rojo", version = "6.0.0-rc.3" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }

View File

@@ -2,6 +2,12 @@
## Unreleased Changes
## 0.1.3 (2020-11-19)
* Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching.
## 0.1.2 (2020-03-29)
* `VfsSnapshot` now implements Serde's `Serialize` and `Deserialize` traits.
## 0.1.1 (2020-03-18)
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.

View File

@@ -1,7 +1,7 @@
[package]
name = "memofs"
description = "Virtual filesystem with configurable backends."
version = "0.1.2"
version = "0.1.3"
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
edition = "2018"
readme = "README.md"

View File

@@ -19,4 +19,4 @@ memofs is currently an unstable minimum viable library. Its primary consumer is
* Configurable caching (write-through, write-around, write-back)
## License
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.

View File

@@ -140,13 +140,18 @@ pub enum VfsEvent {
/// the public interfaces to this type.
struct VfsInner {
backend: Box<dyn VfsBackend>,
watch_enabled: bool,
}
impl VfsInner {
fn read<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<Vec<u8>>> {
let path = path.as_ref();
let contents = self.backend.read(path)?;
self.backend.watch(path)?;
if self.watch_enabled {
self.backend.watch(path)?;
}
Ok(Arc::new(contents))
}
@@ -159,7 +164,11 @@ impl VfsInner {
fn read_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<ReadDir> {
let path = path.as_ref();
let dir = self.backend.read_dir(path)?;
self.backend.watch(path)?;
if self.watch_enabled {
self.backend.watch(path)?;
}
Ok(dir)
}
@@ -215,6 +224,7 @@ impl Vfs {
pub fn new<B: VfsBackend>(backend: B) -> Self {
let lock = VfsInner {
backend: Box::new(backend),
watch_enabled: true,
};
Self {
@@ -229,6 +239,16 @@ impl Vfs {
}
}
/// Turns automatic file watching on or off. Enabled by default.
///
/// Turning off file watching may be useful for single-use cases, especially
/// on platforms like macOS where registering file watches has significant
/// performance cost.
pub fn set_watch_enabled(&self, enabled: bool) {
let mut inner = self.inner.lock().unwrap();
inner.watch_enabled = enabled;
}
/// Read a file from the VFS, or the underlying backend if it isn't
/// resident.
///
@@ -318,6 +338,15 @@ pub struct VfsLock<'a> {
}
impl VfsLock<'_> {
/// Turns automatic file watching on or off. Enabled by default.
///
/// Turning off file watching may be useful for single-use cases, especially
/// on platforms like macOS where registering file watches has significant
/// performance cost.
pub fn set_watch_enabled(&mut self, enabled: bool) {
self.inner.watch_enabled = enabled;
}
/// Read a file from the VFS, or the underlying backend if it isn't
/// resident.
///

View File

@@ -25,6 +25,9 @@
},
"t": {
"$path": "modules/t/lib"
},
"Flipper": {
"$path": "modules/flipper/src"
}
}
}

View File

@@ -8,7 +8,7 @@ Error.Kind = {
},
ConnectFailed = {
message = "Couldn't connect to the Rojo server.\n" ..
"Make sure the server is running -- use 'rojo serve' to run it!",
"Make sure the server is running use 'rojo serve' to run it!",
},
Timeout = {
message = "HTTP request timed out.",
@@ -63,4 +63,4 @@ function Error.fromRobloxErrorString(message)
return Error.new(Error.Kind.Unknown, message)
end
return Error
return Error

View File

@@ -53,4 +53,8 @@ function Log.warn(template, ...)
end
end
function Log.error(template, ...)
error(Fmt.fmt(template, ...))
end
return Log

View File

@@ -1,2 +1,2 @@
# rbx\_dom\_lua
Roblox Lua implementation of rbx-dom mechanisms, intended to work with rbx\_dom\_weak and friends.
# rbx_dom_lua
Roblox Lua implementation of rbx-dom mechanisms, intended to work with rbx_dom_weak and friends.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local SlicedImage = require(script.Parent.SlicedImage)
local e = Roact.createElement
local function BorderedContainer(props)
return Theme.with(function(theme)
return e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = theme.BorderedContainer.BackgroundColor,
transparency = props.transparency,
size = props.size,
position = props.position,
anchorPoint = props.anchorPoint,
layoutOrder = props.layoutOrder,
}, {
Content = e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, props[Roact.Children]),
Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = theme.BorderedContainer.BorderColor,
transparency = props.transparency,
size = UDim2.new(1, 0, 1, 0),
}),
})
end)
end
return BorderedContainer

View File

@@ -0,0 +1,96 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Flipper = require(Rojo.Flipper)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local bindingUtil = require(Plugin.App.bindingUtil)
local SlicedImage = require(script.Parent.SlicedImage)
local e = Roact.createElement
local Checkbox = Roact.Component:extend("Checkbox")
function Checkbox:init()
self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0)
self.binding = bindingUtil.fromMotor(self.motor)
end
function Checkbox:didUpdate(lastProps)
if lastProps.active ~= self.props.active then
self.motor:setGoal(
Flipper.Spring.new(self.props.active and 1 or 0, {
frequency = 6,
dampingRatio = 1.1,
})
)
end
end
function Checkbox:render()
return Theme.with(function(theme)
theme = theme.Checkbox
local activeTransparency = Roact.joinBindings({
self.binding:map(function(value)
return 1 - value
end),
self.props.transparency,
}):map(bindingUtil.blendAlpha)
return e("ImageButton", {
Size = UDim2.new(0, 28, 0, 28),
Position = self.props.position,
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
ZIndex = self.props.zIndex,
BackgroundTransparency = 1,
[Roact.Event.Activated] = self.props.onClick,
}, {
Active = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = theme.Active.BackgroundColor,
transparency = activeTransparency,
size = UDim2.new(1, 0, 1, 0),
zIndex = 2,
}, {
Icon = e("ImageLabel", {
Image = Assets.Images.Checkbox.Active,
ImageColor3 = theme.Active.IconColor,
ImageTransparency = activeTransparency,
Size = UDim2.new(0, 16, 0, 16),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
}),
}),
Inactive = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = theme.Inactive.BorderColor,
transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0),
}, {
Icon = e("ImageLabel", {
Image = Assets.Images.Checkbox.Inactive,
ImageColor3 = theme.Inactive.IconColor,
ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 16, 0, 16),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
}),
}),
})
end)
end
return Checkbox

View File

@@ -0,0 +1,55 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local Config = require(Plugin.Config)
local Version = require(Plugin.Version)
local e = Roact.createElement
local function Header(props)
return Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 0, 32),
LayoutOrder = props.layoutOrder,
BackgroundTransparency = 1,
}, {
Logo = e("ImageLabel", {
Image = Assets.Images.Logo,
ImageColor3 = theme.Header.LogoColor,
ImageTransparency = props.transparency,
Size = UDim2.new(0, 60, 0, 27),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Version = e("TextLabel", {
Text = Version.display(Config.version),
Font = Enum.Font.Gotham,
TextSize = 14,
TextColor3 = theme.Header.VersionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = 2,
BackgroundTransparency = 1,
}),
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 15),
}),
})
end)
end
return Header

View File

@@ -0,0 +1,79 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Flipper = require(Rojo.Flipper)
local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil)
local HOVER_SPRING_PROPS = {
frequency = 5,
dampingRatio = 1.1,
}
local e = Roact.createElement
local IconButton = Roact.Component:extend("IconButton")
function IconButton:init()
self.motor = Flipper.SingleMotor.new(0)
self.binding = bindingUtil.fromMotor(self.motor)
end
function IconButton:render()
local iconSize = self.props.iconSize
return e("ImageButton", {
Size = UDim2.new(0, iconSize * 1.5, 0, iconSize * 1.5),
Position = self.props.position,
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
ZIndex = self.props.zIndex,
BackgroundTransparency = 1,
[Roact.Event.Activated] = self.props.onClick,
[Roact.Event.MouseEnter] = function()
self.motor:setGoal(
Flipper.Spring.new(1, HOVER_SPRING_PROPS)
)
end,
[Roact.Event.MouseLeave] = function()
self.motor:setGoal(
Flipper.Spring.new(0, HOVER_SPRING_PROPS)
)
end,
}, {
Icon = e("ImageLabel", {
Image = self.props.icon,
ImageColor3 = self.props.color,
ImageTransparency = self.props.transparency,
Size = UDim2.new(0, iconSize, 0, iconSize),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
}),
HoverCircle = e("ImageLabel", {
Image = Assets.Images.Circles[128],
ImageColor3 = self.props.color,
ImageTransparency = Roact.joinBindings({
hover = self.binding,
transparency = self.props.transparency,
}):map(function(values)
return bindingUtil.blendAlpha({ 0.85, 1 - values.hover, values.transparency })
end),
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}),
})
end
return IconButton

View File

@@ -0,0 +1,42 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local bindingUtil = require(Plugin.App.bindingUtil)
local e = Roact.createElement
local function ScrollingFrame(props)
return Theme.with(function(theme)
return e("ScrollingFrame", {
ScrollBarThickness = 9,
ScrollBarImageColor3 = theme.ScrollBarColor,
ScrollBarImageTransparency = props.transparency:map(function(value)
return bindingUtil.blendAlpha({ 0.65, value })
end),
TopImage = Assets.Images.ScrollBar.Top,
MidImage = Assets.Images.ScrollBar.Middle,
BottomImage = Assets.Images.ScrollBar.Bottom,
ElasticBehavior = Enum.ElasticBehavior.Always,
ScrollingDirection = Enum.ScrollingDirection.Y,
Size = props.size,
Position = props.position,
AnchorPoint = props.anchorPoint,
CanvasSize = props.contentSize:map(function(value)
return UDim2.new(0, 0, 0, value.Y)
end),
BorderSizePixel = 0,
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize]
}, props[Roact.Children])
end)
end
return ScrollingFrame

View File

@@ -0,0 +1,29 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local e = Roact.createElement
local function SlicedImage(props)
local slice = props.slice
return e("ImageLabel", {
Image = slice.Image,
ImageColor3 = props.color,
ImageTransparency = props.transparency,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = slice.Center,
SliceScale = slice.Scale,
Size = props.size,
Position = props.position,
AnchorPoint = props.anchorPoint,
ZIndex = props.zIndex,
LayoutOrder = props.layoutOrder,
BackgroundTransparency = 1,
}, props[Roact.Children])
end
return SlicedImage

View File

@@ -0,0 +1,66 @@
local RunService = game:GetService("RunService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local ROTATIONS_PER_SECOND = 1.75
local e = Roact.createElement
local Spinner = Roact.PureComponent:extend("Spinner")
function Spinner:init()
self.rotation, self.setRotation = Roact.createBinding(0)
end
function Spinner:render()
return Theme.with(function(theme)
return e("ImageLabel", {
Image = Assets.Images.Spinner.Background,
ImageColor3 = theme.Spinner.BackgroundColor,
ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 24, 0, 24),
Position = self.props.position,
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
BackgroundTransparency = 1,
}, {
Foreground = e("ImageLabel", {
Image = Assets.Images.Spinner.Foreground,
ImageColor3 = theme.Spinner.ForegroundColor,
ImageTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 1, 0),
Rotation = self.rotation:map(function(value)
return value * 360
end),
BackgroundTransparency = 1,
}),
})
end)
end
function Spinner:didMount()
self.stepper = RunService.RenderStepped:Connect(function(deltaTime)
local rotation = self.rotation:getValue()
rotation = rotation + deltaTime * ROTATIONS_PER_SECOND
rotation = rotation % 1
self.setRotation(rotation)
end)
end
function Spinner:willUnmount()
self.stepper:Disconnect()
end
return Spinner

View File

@@ -0,0 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local StudioPluginContext = Roact.createContext(nil)
return StudioPluginContext

View File

@@ -0,0 +1,84 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Dictionary = require(Plugin.Dictionary)
local StudioPluginContext = require(script.Parent.StudioPluginContext)
local e = Roact.createElement
local StudioPluginGui = Roact.PureComponent:extend("StudioPluginGui")
StudioPluginGui.defaultProps = {
initDockState = Enum.InitialDockState.Right,
active = false,
overridePreviousState = false,
floatingSize = Vector2.new(0, 0),
minimumSize = Vector2.new(0, 0),
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
}
function StudioPluginGui:init()
local floatingSize = self.props.floatingSize
local minimumSize = self.props.minimumSize
local dockWidgetPluginGuiInfo = DockWidgetPluginGuiInfo.new(
self.props.initDockState,
self.props.active,
self.props.overridePreviousState,
floatingSize.X, floatingSize.Y,
minimumSize.X, minimumSize.Y
)
local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(self.props.id, dockWidgetPluginGuiInfo)
pluginGui.Name = self.props.id
pluginGui.Title = self.props.title
pluginGui.ZIndexBehavior = self.props.zIndexBehavior
if self.props.onInitialState then
self.props.onInitialState(pluginGui.Enabled)
end
pluginGui:BindToClose(function()
if self.props.onClose then
self.props.onClose()
else
pluginGui.Enabled = false
end
end)
self.pluginGui = pluginGui
end
function StudioPluginGui:render()
return e(Roact.Portal, {
target = self.pluginGui,
}, self.props[Roact.Children])
end
function StudioPluginGui:didUpdate(lastProps)
if self.props.active ~= lastProps.active then
-- This is intentionally in didUpdate to make sure the initial active state
-- (if the PluginGui is open initially) is preserved.
self.pluginGui.Enabled = self.props.active
end
end
function StudioPluginGui:willUnmount()
self.pluginGui:Destroy()
end
local function StudioPluginGuiWrapper(props)
return e(StudioPluginContext.Consumer, {
render = function(plugin)
return e(StudioPluginGui, Dictionary.merge(props, {
plugin = plugin,
}))
end,
})
end
return StudioPluginGuiWrapper

View File

@@ -0,0 +1,66 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Dictionary = require(Plugin.Dictionary)
local StudioToolbarContext = require(script.Parent.StudioToolbarContext)
local e = Roact.createElement
local StudioToggleButton = Roact.Component:extend("StudioToggleButton")
StudioToggleButton.defaultProps = {
enabled = true,
active = false,
}
function StudioToggleButton:init()
local button = self.props.toolbar:CreateButton(
self.props.name,
self.props.tooltip,
self.props.icon,
self.props.text
)
button.Click:Connect(function()
if self.props.onClick then
self.props.onClick()
end
end)
button.ClickableWhenViewportHidden = true
self.button = button
end
function StudioToggleButton:render()
return nil
end
function StudioToggleButton:didUpdate(lastProps)
if self.props.enabled ~= lastProps.enabled then
self.button.Enabled = self.props.enabled
end
if self.props.active ~= lastProps.active then
self.button:SetActive(self.props.active)
end
end
function StudioToggleButton:willUnmount()
self.button:Destroy()
end
local function StudioToggleButtonWrapper(props)
return e(StudioToolbarContext.Consumer, {
render = function(toolbar)
return e(StudioToggleButton, Dictionary.merge(props, {
toolbar = toolbar,
}))
end,
})
end
return StudioToggleButtonWrapper

View File

@@ -0,0 +1,45 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Dictionary = require(Plugin.Dictionary)
local StudioToolbarContext = require(script.Parent.StudioToolbarContext)
local StudioPluginContext = require(script.Parent.StudioPluginContext)
local e = Roact.createElement
local StudioToolbar = Roact.Component:extend("StudioToolbar")
function StudioToolbar:init()
self.toolbar = self.props.plugin:CreateToolbar(self.props.name)
end
function StudioToolbar:render()
return e(StudioToolbarContext.Provider, {
value = self.toolbar,
}, self.props[Roact.Children])
end
function StudioToolbar:didUpdate(lastProps)
if self.props.name ~= lastProps.name then
self.toolbar.Name = self.props.name
end
end
function StudioToolbar:willUnmount()
self.toolbar:Destroy()
end
local function StudioToolbarWrapper(props)
return e(StudioPluginContext.Consumer, {
render = function(plugin)
return e(StudioToolbar, Dictionary.merge(props, {
plugin = plugin,
}))
end,
})
end
return StudioToolbarWrapper

View File

@@ -0,0 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local StudioToolbarContext = Roact.createContext(nil)
return StudioToolbarContext

View File

@@ -0,0 +1,137 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Flipper = require(Rojo.Flipper)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil)
local SlicedImage = require(script.Parent.SlicedImage)
local TouchRipple = require(script.Parent.TouchRipple)
local SPRING_PROPS = {
frequency = 5,
dampingRatio = 1,
}
local e = Roact.createElement
local TextButton = Roact.Component:extend("TextButton")
function TextButton:init()
self.motor = Flipper.GroupMotor.new({
hover = 0,
enabled = self.props.enabled and 1 or 0,
})
self.binding = bindingUtil.fromMotor(self.motor)
end
function TextButton:didUpdate(lastProps)
if lastProps.enabled ~= self.props.enabled then
self.motor:setGoal({
enabled = Flipper.Spring.new(self.props.enabled and 1 or 0),
})
end
end
function TextButton:render()
return Theme.with(function(theme)
local textSize = TextService:GetTextSize(
self.props.text, 18, Enum.Font.GothamSemibold,
Vector2.new(math.huge, math.huge)
)
local style = self.props.style
theme = theme.Button[style]
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
return e("ImageButton", {
Size = UDim2.new(0, 15 + textSize.X + 15, 0, 34),
Position = self.props.position,
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
BackgroundTransparency = 1,
[Roact.Event.Activated] = self.props.onClick,
[Roact.Event.MouseEnter] = function()
self.motor:setGoal({
hover = Flipper.Spring.new(1, SPRING_PROPS),
})
end,
[Roact.Event.MouseLeave] = function()
self.motor:setGoal({
hover = Flipper.Spring.new(0, SPRING_PROPS),
})
end,
}, {
TouchRipple = e(TouchRipple, {
color = theme.ActionFillColor,
transparency = self.props.transparency:map(function(value)
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, value })
end),
zIndex = 2,
}),
Text = e("TextLabel", {
Text = self.props.text,
Font = Enum.Font.GothamSemibold,
TextSize = 18,
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}),
Border = style == "Bordered" and e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0),
zIndex = 0,
}),
HoverOverlay = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = theme.ActionFillColor,
transparency = Roact.joinBindings({
hover = bindingHover:map(function(value)
return 1 - value
end),
transparency = self.props.transparency,
}):map(function(values)
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
end),
size = UDim2.new(1, 0, 1, 0),
zIndex = -1,
}),
Background = style == "Solid" and e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BackgroundColor, theme.Disabled.BackgroundColor),
transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0),
zIndex = -2,
}),
})
end)
end
return TextButton

View File

@@ -0,0 +1,145 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Flipper = require(Rojo.Flipper)
local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil)
local EXPAND_SPRING = {
frequency = 7,
dampingRatio = 2,
}
local TouchRipple = Roact.Component:extend("TouchRipple")
function TouchRipple:init()
self.ref = Roact.createRef()
self.motor = Flipper.GroupMotor.new({
scale = 0,
opacity = 0,
})
self.binding = bindingUtil.fromMotor(self.motor)
self.position, self.setPosition = Roact.createBinding(
Vector2.new(0, 0)
)
end
function TouchRipple:reset()
self.motor:setGoal({
scale = Flipper.Instant.new(0),
opacity = Flipper.Instant.new(0),
})
-- Forces motor to update
self.motor:step(0)
end
function TouchRipple:calculateRadius(position)
local container = self.ref.current
if container then
local corner = Vector2.new(
math.floor((1 - position.X) + 0.5),
math.floor((1 - position.Y) + 0.5)
)
local size = container.AbsoluteSize
local ratio = size / math.min(size.X, size.Y)
return ((corner * ratio) - (position * ratio)).Magnitude
else
return 0
end
end
function TouchRipple:render()
local scale = bindingUtil.deriveProperty(self.binding, "scale")
local transparency = bindingUtil.deriveProperty(self.binding, "opacity"):map(function(value)
return 1 - value
end)
transparency = Roact.joinBindings({
transparency,
self.props.transparency,
}):map(bindingUtil.blendAlpha)
return Roact.createElement("Frame", {
ClipsDescendants = true,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = self.props.zIndex,
BackgroundTransparency = 1,
[Roact.Ref] = self.ref,
[Roact.Event.InputBegan] = function(object, input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
self:reset()
local position = Vector2.new(input.Position.X, input.Position.Y)
local relativePosition = (position - object.AbsolutePosition) / object.AbsoluteSize
self.setPosition(relativePosition)
self.motor:setGoal({
scale = Flipper.Spring.new(1, EXPAND_SPRING),
opacity = Flipper.Spring.new(1, EXPAND_SPRING),
})
input:GetPropertyChangedSignal("UserInputState"):Connect(function()
local userInputState = input.UserInputState
if
userInputState == Enum.UserInputState.Cancel
or userInputState == Enum.UserInputState.End
then
self.motor:setGoal({
opacity = Flipper.Spring.new(0, {
frequency = 5,
dampingRatio = 1,
}),
})
end
end)
end
end,
}, {
Circle = Roact.createElement("ImageLabel", {
Image = Assets.Images.Circles[500],
ImageColor3 = self.props.color,
ImageTransparency = transparency,
Size = Roact.joinBindings({
scale = scale,
position = self.position,
}):map(function(values)
local targetSize = self:calculateRadius(values.position) * 2
local currentSize = targetSize * values.scale
local container = self.ref.current
if container then
local containerSize = container.AbsoluteSize
local containerAspect = containerSize.X / containerSize.Y
return UDim2.new(
currentSize / math.max(containerAspect, 1), 0,
currentSize * math.min(containerAspect, 1), 0
)
end
end),
Position = self.position:map(function(value)
return UDim2.new(value.X, 0, value.Y, 0)
end),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
}),
})
end
return TouchRipple

70
plugin/src/App/Page.lua Normal file
View File

@@ -0,0 +1,70 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Flipper = require(Rojo.Flipper)
local Dictionary = require(Plugin.Dictionary)
local bindingUtil = require(script.Parent.bindingUtil)
local e = Roact.createElement
local Page = Roact.Component:extend("Page")
function Page:init()
self:setState({
rendered = self.props.active
})
self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0)
self.binding = bindingUtil.fromMotor(self.motor)
self.motor:onStep(function(value)
local rendered = value > 0
self:setState(function(state)
if state.rendered ~= rendered then
return {
rendered = rendered,
}
end
end)
end)
end
function Page:render()
if not self.state.rendered then
return nil
end
local transparency = self.binding:map(function(value)
return 1 - value
end)
return e("Frame", {
Position = transparency:map(function(value)
value = self.props.active and value or -value
return UDim2.new(0, value * 30, 0, 0)
end),
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
Component = e(self.props.component, Dictionary.merge(self.props, {
transparency = transparency,
}))
})
end
function Page:didUpdate(lastProps)
if self.props.active ~= lastProps.active then
self.motor:setGoal(
Flipper.Spring.new(self.props.active and 1 or 0, {
frequency = 6,
dampingRatio = 1,
})
)
end
end
return Page

View File

@@ -0,0 +1,125 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local Header = require(Plugin.App.Components.Header)
local IconButton = require(Plugin.App.Components.IconButton)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local e = Roact.createElement
local function ConnectionDetails(props)
return Theme.with(function(theme)
return e(BorderedContainer, {
transparency = props.transparency,
size = UDim2.new(1, 0, 0, 70),
layoutOrder = props.layoutOrder,
}, {
TextContainer = e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1
}, {
ProjectName = e("TextLabel", {
Text = props.projectName,
Font = Enum.Font.GothamBold,
TextSize = 20,
TextColor3 = theme.ConnectionDetails.ProjectNameColor,
TextTransparency = props.transparency,
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, 20),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Address = e("TextLabel", {
Text = props.address,
Font = Enum.Font.Code,
TextSize = 15,
TextColor3 = theme.ConnectionDetails.AddressColor,
TextTransparency = props.transparency,
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, 15),
LayoutOrder = 2,
BackgroundTransparency = 1,
}),
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
}),
}),
Disconnect = e(IconButton, {
icon = Assets.Images.Icons.Close,
iconSize = 24,
color = theme.ConnectionDetails.DisconnectColor,
transparency = props.transparency,
position = UDim2.new(1, 0, 0.5, 0),
anchorPoint = Vector2.new(1, 0.5),
onClick = props.onDisconnect,
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15),
}),
})
end)
end
local ConnectedPage = Roact.Component:extend("ConnectedPage")
function ConnectedPage:render()
return Roact.createFragment({
Header = e(Header, {
transparency = self.props.transparency,
layoutOrder = 1,
}),
ConnectionDetails = e(ConnectionDetails, {
projectName = self.state.projectName,
address = self.state.address,
transparency = self.props.transparency,
layoutOrder = 2,
onDisconnect = self.props.onDisconnect,
}),
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
})
end
function ConnectedPage.getDerivedStateFromProps(props)
-- If projectName or address ever get removed from props, make sure we still have
-- the properties! The component still needs to have its data for it to be properly
-- animated out without the labels changing.
return {
projectName = props.projectName,
address = props.address,
}
end
return ConnectedPage

View File

@@ -0,0 +1,20 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Spinner = require(Plugin.App.Components.Spinner)
local e = Roact.createElement
local ConnectingPage = Roact.Component:extend("ConnectingPage")
function ConnectingPage:render()
return e(Spinner, {
position = UDim2.new(0.5, 0, 0.5, 0),
anchorPoint = Vector2.new(0.5, 0.5),
transparency = self.props.transparency,
})
end
return ConnectingPage

View File

@@ -0,0 +1,153 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Theme = require(Plugin.App.Theme)
local TextButton = require(Plugin.App.Components.TextButton)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local e = Roact.createElement
local ERROR_PADDING = Vector2.new(13, 10)
local Error = Roact.Component:extend("Error")
function Error:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
end
function Error:render()
return e(BorderedContainer, {
size = Roact.joinBindings({
containerSize = self.props.containerSize,
contentSize = self.contentSize,
}):map(function(values)
local maximumSize = values.containerSize
maximumSize -= Vector2.new(14, 14) * 2 -- Page padding
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
local outerSize = values.contentSize + ERROR_PADDING * 2
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
end),
transparency = self.props.transparency,
}, {
ScrollingFrame = e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, 0),
contentSize = self.contentSize:map(function(value)
return value + ERROR_PADDING * 2
end),
transparency = self.props.transparency,
[Roact.Change.AbsoluteSize] = function(object)
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
local textBounds = TextService:GetTextSize(
self.props.errorMessage, 16, Enum.Font.Code,
Vector2.new(containerSize.X, math.huge)
)
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
end,
}, {
ErrorMessage = Theme.with(function(theme)
return e("TextLabel", {
Text = self.props.errorMessage,
Font = Enum.Font.Code,
TextSize = 16,
TextColor3 = theme.ErrorColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextTransparency = self.props.transparency,
TextWrapped = true,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
})
end),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, ERROR_PADDING.X),
PaddingRight = UDim.new(0, ERROR_PADDING.X),
PaddingTop = UDim.new(0, ERROR_PADDING.Y),
PaddingBottom = UDim.new(0, ERROR_PADDING.Y),
}),
}),
})
end
local ErrorPage = Roact.Component:extend("ErrorPage")
function ErrorPage:init()
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
end
function ErrorPage:render()
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = function(object)
self.setContainerSize(object.AbsoluteSize)
end,
}, {
Error = e(Error, {
errorMessage = self.state.errorMessage,
containerSize = self.containerSize,
transparency = self.props.transparency,
layoutOrder = 1,
}),
Buttons = e("Frame", {
Size = UDim2.new(1, 0, 0, 35),
LayoutOrder = 2,
BackgroundTransparency = 1,
}, {
Close = e(TextButton, {
text = "Okay",
style = "Bordered",
transparency = self.props.transparency,
layoutOrder = 1,
onClick = self.props.onClose,
}),
Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Right,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
}),
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 14),
PaddingRight = UDim.new(0, 14),
PaddingTop = UDim.new(0, 14),
PaddingBottom = UDim.new(0, 14),
}),
})
end
function ErrorPage.getDerivedStateFromProps(props)
-- If errorMessage ever gets removed from props, make sure we still have the
-- property! The component still needs to have its data for it to be properly
-- animated out without the labels changing.
return {
errorMessage = props.errorMessage,
}
end
return ErrorPage

View File

@@ -0,0 +1,154 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Config = require(Plugin.Config)
local Theme = require(Plugin.App.Theme)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local TextButton = require(Plugin.App.Components.TextButton)
local Header = require(Plugin.App.Components.Header)
local PORT_WIDTH = 74
local DIVIDER_WIDTH = 1
local HOST_OFFSET = 12
local e = Roact.createElement
local function AddressEntry(props)
return Theme.with(function(theme)
return e(BorderedContainer, {
transparency = props.transparency,
size = UDim2.new(1, 0, 0, 36),
layoutOrder = props.layoutOrder,
}, {
Host = e("TextBox", {
Text = "",
Font = Enum.Font.Code,
TextSize = 18,
TextColor3 = theme.AddressEntry.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
PlaceholderText = Config.defaultHost,
PlaceholderColor3 = theme.AddressEntry.PlaceholderColor,
Size = UDim2.new(1, -(HOST_OFFSET + DIVIDER_WIDTH + PORT_WIDTH), 1, 0),
Position = UDim2.new(0, HOST_OFFSET, 0, 0),
ClipsDescendants = true,
BackgroundTransparency = 1,
[Roact.Ref] = props.hostRef,
}),
Port = e("TextBox", {
Text = "",
Font = Enum.Font.Code,
TextSize = 18,
TextColor3 = theme.AddressEntry.TextColor,
TextTransparency = props.transparency,
PlaceholderText = Config.defaultPort,
PlaceholderColor3 = theme.AddressEntry.PlaceholderColor,
Size = UDim2.new(0, PORT_WIDTH, 1, 0),
Position = UDim2.new(1, 0, 0, 0),
AnchorPoint = Vector2.new(1, 0),
ClipsDescendants = true,
BackgroundTransparency = 1,
[Roact.Ref] = props.portRef,
[Roact.Change.Text] = function(object)
local text = object.Text
text = text:gsub("%D", "")
object.Text = text
end,
}, {
Divider = e("Frame", {
BackgroundColor3 = theme.BorderedContainer.BorderColor,
BackgroundTransparency = props.transparency,
Size = UDim2.new(0, DIVIDER_WIDTH, 1, 0),
AnchorPoint = Vector2.new(1, 0),
BorderSizePixel = 0,
}),
}),
})
end)
end
local NotConnectedPage = Roact.Component:extend("NotConnectedPage")
function NotConnectedPage:init()
self.hostRef = Roact.createRef()
self.portRef = Roact.createRef()
end
function NotConnectedPage:render()
return Roact.createFragment({
Header = e(Header, {
transparency = self.props.transparency,
layoutOrder = 1,
}),
AddressEntry = e(AddressEntry, {
hostRef = self.hostRef,
portRef = self.portRef,
transparency = self.props.transparency,
layoutOrder = 2,
}),
Buttons = e("Frame", {
Size = UDim2.new(1, 0, 0, 34),
LayoutOrder = 3,
BackgroundTransparency = 1,
}, {
Settings = e(TextButton, {
text = "Settings",
style = "Bordered",
transparency = self.props.transparency,
layoutOrder = 1,
onClick = self.props.onNavigateSettings,
}),
Connect = e(TextButton, {
text = "Connect",
style = "Solid",
transparency = self.props.transparency,
layoutOrder = 2,
onClick = function()
local hostText = self.hostRef.current.Text
local portText = self.portRef.current.Text
self.props.onConnect(
#hostText > 0 and hostText or Config.defaultHost,
#portText > 0 and portText or Config.defaultPort
)
end,
}),
Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Right,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
}),
Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
})
end
return NotConnectedPage

View File

@@ -0,0 +1,230 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local PluginSettings = require(Plugin.App.PluginSettings)
local Checkbox = require(Plugin.App.Components.Checkbox)
local IconButton = require(Plugin.App.Components.IconButton)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local e = Roact.createElement
local DIVIDER_FADE_SIZE = 0.1
local function getTextBounds(text, textSize, font, lineHeight, bounds)
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
local lineCount = textBounds.Y / textSize
local lineHeightAbsolute = textSize * lineHeight
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
end
local function Navbar(props)
return Theme.with(function(theme)
theme = theme.Settings.Navbar
return e("Frame", {
Size = UDim2.new(1, 0, 0, 46),
LayoutOrder = props.layoutOrder,
BackgroundTransparency = 1,
}, {
Back = e(IconButton, {
icon = Assets.Images.Icons.Back,
iconSize = 24,
color = theme.BackButtonColor,
transparency = props.transparency,
position = UDim2.new(0, 0, 0.5, 0),
anchorPoint = Vector2.new(0, 0.5),
onClick = props.onBack,
}),
Text = e("TextLabel", {
Text = "Settings",
Font = Enum.Font.Gotham,
TextSize = 18,
TextColor3 = theme.TextColor,
TextTransparency = props.transparency,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
})
})
end)
end
local Setting = Roact.Component:extend("Setting")
function Setting:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
end
function Setting:render()
return Theme.with(function(theme)
theme = theme.Settings
return PluginSettings.with(function(settings)
return e("Frame", {
Size = self.contentSize:map(function(value)
return UDim2.new(1, 0, 0, 20 + value.Y + 20)
end),
LayoutOrder = self.props.layoutOrder,
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = function(object)
self.setContainerSize(object.AbsoluteSize)
end,
}, {
Checkbox = e(Checkbox, {
active = settings:get(self.props.id),
transparency = self.props.transparency,
position = UDim2.new(1, 0, 0.5, 0),
anchorPoint = Vector2.new(1, 0.5),
onClick = function()
local currentValue = settings:get(self.props.id)
settings:set(self.props.id, not currentValue)
end,
}),
Text = e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
Name = e("TextLabel", {
Text = self.props.name,
Font = Enum.Font.GothamBold,
TextSize = 17,
TextColor3 = theme.Setting.NameColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 17),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Description = e("TextLabel", {
Text = self.props.description,
Font = Enum.Font.Gotham,
LineHeight = 1.2,
TextSize = 14,
TextColor3 = theme.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
TextWrapped = true,
Size = self.containerSize:map(function(value)
local textBounds = getTextBounds(
self.props.description, 14, Enum.Font.Gotham, 1.2,
Vector2.new(value.X - 50, math.huge)
)
return UDim2.new(1, -50, 0, textBounds.Y)
end),
LayoutOrder = 2,
BackgroundTransparency = 1,
}),
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 6),
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Padding = e("UIPadding", {
PaddingTop = UDim.new(0, 20),
PaddingBottom = UDim.new(0, 20),
}),
}),
Divider = e("Frame", {
BackgroundColor3 = theme.DividerColor,
BackgroundTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 1),
BorderSizePixel = 0,
}, {
Gradient = e("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0),
NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0),
NumberSequenceKeypoint.new(1, 1),
}),
}),
}),
})
end)
end)
end
local SettingsPage = Roact.Component:extend("SettingsPage")
function SettingsPage:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
end
function SettingsPage:render()
return Theme.with(function(theme)
theme = theme.Settings
return e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, 0),
contentSize = self.contentSize,
transparency = self.props.transparency,
}, {
Navbar = e(Navbar, {
onBack = self.props.onBack,
transparency = self.props.transparency,
layoutOrder = 0,
}),
OpenScriptsExternally = e(Setting, {
id = "openScriptsExternally",
name = "Open Scripts Externally",
description = "Attempt to open scripts in an external editor",
transparency = self.props.transparency,
layoutOrder = 1,
}),
TwoWaySync = e(Setting, {
id = "twoWaySync",
name = "Two-Way Sync",
description = "Editing files in Studio will sync them into the filesystem",
transparency = self.props.transparency,
layoutOrder = 2,
}),
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
})
end)
end
return SettingsPage

View File

@@ -0,0 +1,7 @@
return {
NotConnected = require(script.NotConnected),
Settings = require(script.Settings),
Connecting = require(script.Connecting),
Connected = require(script.Connected),
Error = require(script.Error),
}

239
plugin/src/App/Theme.lua Normal file
View File

@@ -0,0 +1,239 @@
--[[
Theming system taking advantage of Roact's new context API.
Doesn't use colors provided by Studio and instead just branches on theme
name. This isn't exactly best practice.
]]
-- Studio does not exist outside Roblox Studio, so we'll lazily initialize it
-- when possible.
local _Studio
local function getStudio()
if _Studio == nil then
_Studio = settings():GetService("Studio")
end
return _Studio
end
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local Log = require(Rojo.Log)
local strict = require(script.Parent.Parent.strict)
-- Copying hex colors back and forth from design programs is faster
local function hexColor(decimal)
local red = bit32.band(bit32.rshift(decimal, 16), 2^8 - 1)
local green = bit32.band(bit32.rshift(decimal, 8), 2^8 - 1)
local blue = bit32.band(decimal, 2^8 - 1)
return Color3.fromRGB(red, green, blue)
end
local BRAND_COLOR = hexColor(0xE13835)
local lightTheme = strict("LightTheme", {
BackgroundColor = hexColor(0xF0F0F0),
Button = {
Solid = {
ActionFillColor = hexColor(0xFFFFFF),
ActionFillTransparency = 0.8,
Enabled = {
TextColor = hexColor(0xFFFFFF),
BackgroundColor = BRAND_COLOR,
},
Disabled = {
TextColor = hexColor(0xFFFFFF),
BackgroundColor = BRAND_COLOR,
},
},
Bordered = {
ActionFillColor = hexColor(0x000000),
ActionFillTransparency = 0.9,
Enabled = {
TextColor = hexColor(0x393939),
BorderColor = hexColor(0xACACAC),
},
Disabled = {
TextColor = hexColor(0x393939),
BorderColor = hexColor(0xACACAC),
},
},
},
Checkbox = {
Active = {
IconColor = hexColor(0xFFFFFF),
BackgroundColor = BRAND_COLOR,
},
Inactive = {
IconColor = hexColor(0xCACACA),
BorderColor = hexColor(0xAFAFAF),
},
},
AddressEntry = {
TextColor = hexColor(0x000000),
PlaceholderColor = hexColor(0x8C8C8C)
},
BorderedContainer = {
BorderColor = hexColor(0xCBCBCB),
BackgroundColor = hexColor(0xE0E0E0),
},
Spinner = {
ForegroundColor = BRAND_COLOR,
BackgroundColor = hexColor(0xE0E0E0),
},
ConnectionDetails = {
ProjectNameColor = hexColor(0x00000),
AddressColor = hexColor(0x00000),
DisconnectColor = BRAND_COLOR,
},
Settings = {
DividerColor = hexColor(0xCBCBCB),
Navbar = {
BackButtonColor = hexColor(0x000000),
TextColor = hexColor(0x000000),
},
Setting = {
NameColor = hexColor(0x000000),
DescriptionColor = hexColor(0x5F5F5F),
},
},
Header = {
LogoColor = BRAND_COLOR,
VersionColor = hexColor(0x727272),
},
ErrorColor = hexColor(0x000000),
ScrollBarColor = hexColor(0x000000),
})
local darkTheme = strict("DarkTheme", {
BackgroundColor = hexColor(0x272727),
Button = {
Solid = {
ActionFillColor = hexColor(0xFFFFFF),
ActionFillTransparency = 0.8,
Enabled = {
TextColor = hexColor(0xFFFFFF),
BackgroundColor = BRAND_COLOR,
},
Disabled = {
TextColor = hexColor(0xFFFFFF),
BackgroundColor = BRAND_COLOR,
},
},
Bordered = {
ActionFillColor = hexColor(0xFFFFFF),
ActionFillTransparency = 0.9,
Enabled = {
TextColor = hexColor(0xDBDBDB),
BorderColor = hexColor(0x535353),
},
Disabled = {
TextColor = hexColor(0xDBDBDB),
BorderColor = hexColor(0x535353),
},
},
},
Checkbox = {
Active = {
IconColor = hexColor(0xFFFFFF),
BackgroundColor = BRAND_COLOR,
},
Inactive = {
IconColor = hexColor(0x484848),
BorderColor = hexColor(0x5A5A5A),
},
},
AddressEntry = {
TextColor = hexColor(0xFFFFFF),
PlaceholderColor = hexColor(0x717171)
},
BorderedContainer = {
BorderColor = hexColor(0x535353),
BackgroundColor = hexColor(0x323232),
},
Spinner = {
ForegroundColor = BRAND_COLOR,
BackgroundColor = hexColor(0x323232),
},
ConnectionDetails = {
ProjectNameColor = hexColor(0xFFFFFF),
AddressColor = hexColor(0xFFFFFF),
DisconnectColor = hexColor(0xFFFFFF),
},
Settings = {
DividerColor = hexColor(0x535353),
Navbar = {
BackButtonColor = hexColor(0xFFFFFF),
TextColor = hexColor(0xFFFFFF),
},
Setting = {
NameColor = hexColor(0xFFFFFF),
DescriptionColor = hexColor(0xD3D3D3),
},
},
Header = {
LogoColor = BRAND_COLOR,
VersionColor = hexColor(0xD3D3D3)
},
ErrorColor = hexColor(0xFFFFFF),
ScrollBarColor = hexColor(0xFFFFFF),
})
local Context = Roact.createContext(lightTheme)
local StudioProvider = Roact.Component:extend("StudioProvider")
-- Pull the current theme from Roblox Studio and update state with it.
function StudioProvider:updateTheme()
local studioTheme = getStudio().Theme
if studioTheme.Name == "Light" then
self:setState({
theme = lightTheme,
})
elseif studioTheme.Name == "Dark" then
self:setState({
theme = darkTheme,
})
else
Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name)
self:setState({
theme = lightTheme,
})
end
end
function StudioProvider:init()
self:updateTheme()
end
function StudioProvider:render()
return Roact.createElement(Context.Provider, {
value = self.state.theme,
}, self.props[Roact.Children])
end
function StudioProvider:didMount()
self.connection = getStudio().ThemeChanged:Connect(function()
self:updateTheme()
end)
end
function StudioProvider:willUnmount()
self.connection:Disconnect()
end
local function with(callback)
return Roact.createElement(Context.Consumer, {
render = callback,
})
end
return {
StudioProvider = StudioProvider,
Consumer = Context.Consumer,
with = with,
}

View File

@@ -0,0 +1,58 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local Log = require(Rojo.Log)
local LERP_DATA_TYPES = {
Color3 = true,
UDim = true,
UDim2 = true,
Vector2 = true,
Vector3 = true,
}
local function fromMotor(motor)
local motorBinding, setMotorBinding = Roact.createBinding(motor:getValue())
motor:onStep(setMotorBinding)
return motorBinding
end
local function mapLerp(binding, value1, value2)
local valueType = typeof(value1)
if valueType ~= typeof(value2) then
Log.error("Type mismatch between values ({}, {}})", valueType, typeof(value2))
end
return binding:map(function(position)
if valueType == "number" then
return value1 - (value2 - value1) * position
elseif LERP_DATA_TYPES[valueType] then
return value1:lerp(value2, position)
else
Log.error("Unable to interpolate type {}", valueType)
end
end)
end
local function deriveProperty(binding, propertyName)
return binding:map(function(values)
return values[propertyName]
end)
end
local function blendAlpha(alphaValues)
local alpha = 0
for _, value in pairs(alphaValues) do
alpha = alpha + (1 - alpha) * value
end
return alpha
end
return {
fromMotor = fromMotor,
mapLerp = mapLerp,
deriveProperty = deriveProperty,
blendAlpha = blendAlpha,
}

226
plugin/src/App/init.lua Normal file
View File

@@ -0,0 +1,226 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Log = require(Rojo.Log)
local Assets = require(Plugin.Assets)
local Version = require(Plugin.Version)
local Config = require(Plugin.Config)
local strict = require(Plugin.strict)
local Dictionary = require(Plugin.Dictionary)
local ServeSession = require(Plugin.ServeSession)
local ApiContext = require(Plugin.ApiContext)
local preloadAssets = require(Plugin.preloadAssets)
local Theme = require(script.Theme)
local PluginSettings = require(script.PluginSettings)
local Page = require(script.Page)
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
local StudioToggleButton = require(script.Components.Studio.StudioToggleButton)
local StudioPluginGui = require(script.Components.Studio.StudioPluginGui)
local StudioPluginContext = require(script.Components.Studio.StudioPluginContext)
local StatusPages = require(script.StatusPages)
local AppStatus = strict("AppStatus", {
NotConnected = "NotConnected",
Settings = "Settings",
Connecting = "Connecting",
Connected = "Connected",
Error = "Error",
})
local e = Roact.createElement
local App = Roact.Component:extend("App")
function App:init()
preloadAssets()
self:setState({
appStatus = AppStatus.NotConnected,
guiEnabled = false,
})
end
function App:startSession(host, port, sessionOptions)
local baseUrl = ("http://%s:%s"):format(host, port)
local apiContext = ApiContext.new(baseUrl)
local serveSession = ServeSession.new({
apiContext = apiContext,
openScriptsExternally = sessionOptions.openScriptsExternally,
twoWaySync = sessionOptions.twoWaySync,
})
serveSession:onStatusChanged(function(status, details)
if status == ServeSession.Status.Connecting then
self:setState({
appStatus = AppStatus.Connecting,
})
elseif status == ServeSession.Status.Connected then
local address = ("%s:%s"):format(host, port)
self:setState({
appStatus = AppStatus.Connected,
projectName = details,
address = address,
})
elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil
-- Details being present indicates that this
-- disconnection was from an error.
if details ~= nil then
Log.warn("Disconnected from an error: {}", details)
self:setState({
appStatus = AppStatus.Error,
errorMessage = tostring(details),
})
else
self:setState({
appStatus = AppStatus.NotConnected,
})
end
end
end)
serveSession:start()
self.serveSession = serveSession
end
function App:render()
local pluginName = "Rojo " .. Version.display(Config.version)
local function createPageElement(appStatus, additionalProps)
additionalProps = additionalProps or {}
local props = Dictionary.merge(additionalProps, {
component = StatusPages[appStatus],
active = self.state.appStatus == appStatus,
})
return e(Page, props)
end
return e(StudioPluginContext.Provider, {
value = self.props.plugin,
}, {
e(Theme.StudioProvider, nil, {
e(PluginSettings.StudioProvider, {
plugin = self.props.plugin,
}, {
gui = e(StudioPluginGui, {
id = pluginName,
title = pluginName,
active = self.state.guiEnabled,
initDockState = Enum.InitialDockState.Right,
initEnabled = false,
overridePreviousState = false,
floatingSize = Vector2.new(300, 200),
minimumSize = Vector2.new(300, 200),
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
onInitialState = function(initialState)
self:setState({
guiEnabled = initialState,
})
end,
onClose = function()
self:setState({
guiEnabled = false,
})
end,
}, {
NotConnectedPage = PluginSettings.with(function(settings)
return createPageElement(AppStatus.NotConnected, {
onConnect = function(host, port)
self:startSession(host, port, {
openScriptsExternally = settings:get("openScriptsExternally"),
twoWaySync = settings:get("twoWaySync"),
})
end,
onNavigateSettings = function()
self:setState({
appStatus = AppStatus.Settings,
})
end,
})
end),
Connecting = createPageElement(AppStatus.Connecting),
Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName,
address = self.state.address,
onDisconnect = function()
Log.trace("Disconnecting session")
self.serveSession:stop()
self.serveSession = nil
self:setState({
appStatus = AppStatus.NotConnected,
})
Log.trace("Session terminated by user")
end,
}),
Settings = createPageElement(AppStatus.Settings, {
onBack = function()
self:setState({
appStatus = AppStatus.NotConnected,
})
end,
}),
Error = createPageElement(AppStatus.Error, {
errorMessage = self.state.errorMessage,
onClose = function()
self:setState({
appStatus = AppStatus.NotConnected,
})
end,
}),
Background = Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = theme.BackgroundColor,
ZIndex = 0,
BorderSizePixel = 0,
})
end),
}),
toolbar = e(StudioToolbar, {
name = pluginName,
}, {
button = e(StudioToggleButton, {
name = "Rojo",
tooltip = "Show or hide the Rojo panel",
icon = Assets.Images.PluginButton,
active = self.state.guiEnabled,
enabled = true,
onClick = function()
self:setState(function(state)
return {
guiEnabled = not state.guiEnabled,
}
end)
end,
})
}),
}),
}),
})
end
return App

View File

@@ -3,16 +3,45 @@ local strict = require(script.Parent.strict)
local Assets = {
Sprites = {},
Slices = {
RoundBox = {
asset = "rbxassetid://2773204550",
offset = Vector2.new(0, 0),
size = Vector2.new(32, 32),
center = Rect.new(4, 4, 4, 4),
RoundedBackground = {
Image = "rbxassetid://5981360418",
Center = Rect.new(10, 10, 10, 10),
Scale = 0.5,
},
RoundedBorder = {
Image = "rbxassetid://5981360137",
Center = Rect.new(10, 10, 10, 10),
Scale = 0.5,
},
},
Images = {
Logo = "rbxassetid://3405346157",
Icon = "rbxassetid://3405341609",
Logo = "rbxassetid://5990772764",
PluginButton = "rbxassetid://3405341609",
Icons = {
Close = "rbxassetid://6012985953",
Back = "rbxassetid://6017213752",
},
Checkbox = {
Active = "rbxassetid://6016251644",
Inactive = "rbxassetid://6016251963",
},
Spinner = {
Foreground = "rbxassetid://3222731032",
Background = "rbxassetid://3222730627",
},
ScrollBar = {
Top = "rbxassetid://6017290134",
Middle = "rbxassetid://6017289904",
Bottom = "rbxassetid://6017289712",
},
Circles = {
[16] = "rbxassetid://3056541177",
[32] = "rbxassetid://3088713341",
[64] = "rbxassetid://4918677124",
[128] = "rbxassetid://2600845734",
[500] = "rbxassetid://2609138523"
},
},
StartSession = "",
SessionActive = "",

View File

@@ -1,241 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Log = require(Rojo.Log)
local ApiContext = require(Plugin.ApiContext)
local Assets = require(Plugin.Assets)
local Config = require(Plugin.Config)
local DevSettings = require(Plugin.DevSettings)
local ServeSession = require(Plugin.ServeSession)
local Version = require(Plugin.Version)
local preloadAssets = require(Plugin.preloadAssets)
local strict = require(Plugin.strict)
local ConnectPanel = require(Plugin.Components.ConnectPanel)
local ConnectingPanel = require(Plugin.Components.ConnectingPanel)
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
local ErrorPanel = require(Plugin.Components.ErrorPanel)
local SettingsPanel = require(Plugin.Components.SettingsPanel)
local e = Roact.createElement
local function showUpgradeMessage(lastVersion)
local message = (
"Rojo detected an upgrade from version %s to version %s." ..
"\nMake sure you have also upgraded your server!" ..
"\n\nRojo plugin version %s is intended for use with server version %s."
):format(
Version.display(lastVersion), Version.display(Config.version),
Version.display(Config.version), Config.expectedServerVersionString
)
Log.info(message)
end
--[[
Check if the user is using a newer version of Rojo than last time. If they
are, show them a reminder to make sure they check their server version.
]]
local function checkUpgrade(plugin)
-- When developing Rojo, there's no use in doing version checks
if DevSettings:isEnabled() then
return
end
local lastVersion = plugin:GetSetting("LastRojoVersion")
if lastVersion then
local wasUpgraded = Version.compare(Config.version, lastVersion) == 1
if wasUpgraded then
showUpgradeMessage(lastVersion)
end
end
plugin:SetSetting("LastRojoVersion", Config.version)
end
local AppStatus = strict("AppStatus", {
NotStarted = "NotStarted",
Connecting = "Connecting",
Connected = "Connected",
Error = "Error",
Settings = "Settings",
})
local App = Roact.Component:extend("App")
function App:init()
self:setState({
appStatus = AppStatus.NotStarted,
errorMessage = nil,
})
self.signals = {}
self.serveSession = nil
self.displayedVersion = Version.display(Config.version)
local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion)
self.toggleButton = toolbar:CreateButton(
"Rojo",
"Show or hide the Rojo panel",
Assets.Images.Icon)
self.toggleButton.ClickableWhenViewportHidden = true
self.toggleButton.Click:Connect(function()
self.dockWidget.Enabled = not self.dockWidget.Enabled
end)
local widgetInfo = DockWidgetPluginGuiInfo.new(
Enum.InitialDockState.Right,
false, -- Initially enabled state
false, -- Whether to override the widget's previous state
360, 190, -- Floating size
360, 190 -- Minimum size
)
self.dockWidget = self.props.plugin:CreateDockWidgetPluginGui("Rojo-" .. self.displayedVersion, widgetInfo)
self.dockWidget.Name = "Rojo " .. self.displayedVersion
self.dockWidget.Title = "Rojo " .. self.displayedVersion
self.dockWidget.AutoLocalize = false
self.dockWidget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
self.signals.dockWidgetEnabled = self.dockWidget:GetPropertyChangedSignal("Enabled"):Connect(function()
self.toggleButton:SetActive(self.dockWidget.Enabled)
end)
end
function App:startSession(address, port, sessionOptions)
Log.trace("Starting new session")
local baseUrl = ("http://%s:%s"):format(address, port)
self.serveSession = ServeSession.new({
apiContext = ApiContext.new(baseUrl),
openScriptsExternally = sessionOptions.openScriptsExternally,
twoWaySync = sessionOptions.twoWaySync,
})
self.serveSession:onStatusChanged(function(status, details)
if status == ServeSession.Status.Connecting then
self:setState({
appStatus = AppStatus.Connecting,
})
elseif status == ServeSession.Status.Connected then
self:setState({
appStatus = AppStatus.Connected,
})
elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil
-- Details being present indicates that this
-- disconnection was from an error.
if details ~= nil then
Log.warn("Disconnected from an error: {}", details)
self:setState({
appStatus = AppStatus.Error,
errorMessage = tostring(details),
})
else
self:setState({
appStatus = AppStatus.NotStarted,
})
end
end
end)
self.serveSession:start()
end
function App:render()
local children
if self.state.appStatus == AppStatus.NotStarted then
children = {
ConnectPanel = e(ConnectPanel, {
startSession = function(address, port, settings)
self:startSession(address, port, settings)
end,
openSettings = function()
self:setState({
appStatus = AppStatus.Settings,
})
end,
cancel = function()
Log.trace("Canceling session configuration")
self:setState({
appStatus = AppStatus.NotStarted,
})
end,
}),
}
elseif self.state.appStatus == AppStatus.Connecting then
children = {
ConnectingPanel = e(ConnectingPanel),
}
elseif self.state.appStatus == AppStatus.Connected then
children = {
ConnectionActivePanel = e(ConnectionActivePanel, {
stopSession = function()
Log.trace("Disconnecting session")
self.serveSession:stop()
self.serveSession = nil
self:setState({
appStatus = AppStatus.NotStarted,
})
Log.trace("Session terminated by user")
end,
}),
}
elseif self.state.appStatus == AppStatus.Settings then
children = {
e(SettingsPanel, {
back = function()
self:setState({
appStatus = AppStatus.NotStarted,
})
end,
}),
}
elseif self.state.appStatus == AppStatus.Error then
children = {
ErrorPanel = e(ErrorPanel, {
errorMessage = self.state.errorMessage,
onDismiss = function()
self:setState({
appStatus = AppStatus.NotStarted,
})
end,
}),
}
end
return e(Roact.Portal, {
target = self.dockWidget,
}, children)
end
function App:didMount()
Log.trace("Rojo {} initializing", self.displayedVersion)
checkUpgrade(self.props.plugin)
preloadAssets()
end
function App:willUnmount()
if self.serveSession ~= nil then
self.serveSession:stop()
self.serveSession = nil
end
for _, signal in pairs(self.signals) do
signal:Disconnect()
end
end
return App

View File

@@ -1,39 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement
local function Checkbox(props)
local checked = props.checked
local layoutOrder = props.layoutOrder
local onChange = props.onChange
return Theme.with(function(theme)
return e("ImageButton", {
LayoutOrder = layoutOrder,
Size = UDim2.new(0, 20, 0, 20),
BorderSizePixel = 2,
BorderColor3 = theme.Text2,
BackgroundColor3 = theme.Background2,
[Roact.Event.Activated] = function()
onChange(not checked)
end,
}, {
Indicator = e("Frame", {
Size = UDim2.new(0, 18, 0, 18),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BorderSizePixel = 0,
BackgroundColor3 = theme.Brand1,
BackgroundTransparency = checked and 0 or 1,
})
})
end)
end
return Checkbox

View File

@@ -1,183 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Config = require(Plugin.Config)
local Theme = require(Plugin.Components.Theme)
local Panel = require(Plugin.Components.Panel)
local FitList = require(Plugin.Components.FitList)
local FitText = require(Plugin.Components.FitText)
local FormButton = require(Plugin.Components.FormButton)
local FormTextInput = require(Plugin.Components.FormTextInput)
local PluginSettings = require(Plugin.Components.PluginSettings)
local e = Roact.createElement
local ConnectPanel = Roact.Component:extend("ConnectPanel")
function ConnectPanel:init()
self:setState({
address = "",
port = "",
})
end
function ConnectPanel:render()
local startSession = self.props.startSession
local openSettings = self.props.openSettings
return Theme.with(function(theme)
return PluginSettings.with(function(settings)
return e(Panel, nil, {
Layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
Inputs = e(FitList, {
containerProps = {
BackgroundTransparency = 1,
LayoutOrder = 1,
},
layoutProps = {
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 8),
},
paddingProps = {
PaddingTop = UDim.new(0, 20),
PaddingBottom = UDim.new(0, 10),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
Address = e(FitList, {
containerProps = {
LayoutOrder = 1,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.TitleFont,
TextSize = 20,
Text = "Address",
TextColor3 = theme.Text1,
}),
Input = e(FormTextInput, {
layoutOrder = 2,
width = UDim.new(0, 220),
value = self.state.address,
placeholderValue = Config.defaultHost,
onValueChange = function(newValue)
self:setState({
address = newValue,
})
end,
}),
}),
Port = e(FitList, {
containerProps = {
LayoutOrder = 2,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.TitleFont,
TextSize = 20,
Text = "Port",
TextColor3 = theme.Text1,
}),
Input = e(FormTextInput, {
layoutOrder = 2,
width = UDim.new(0, 80),
value = self.state.port,
placeholderValue = Config.defaultPort,
onValueChange = function(newValue)
self:setState({
port = newValue,
})
end,
}),
}),
}),
Buttons = e(FitList, {
fitAxes = "Y",
containerProps = {
BackgroundTransparency = 1,
LayoutOrder = 2,
Size = UDim2.new(1, 0, 0, 0),
},
layoutProps = {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
Padding = UDim.new(0, 8),
},
paddingProps = {
PaddingTop = UDim.new(0, 0),
PaddingBottom = UDim.new(0, 20),
PaddingLeft = UDim.new(0, 24),
PaddingRight = UDim.new(0, 24),
},
}, {
e(FormButton, {
layoutOrder = 1,
text = "Settings",
secondary = true,
onClick = function()
if openSettings ~= nil then
openSettings()
end
end,
}),
e(FormButton, {
layoutOrder = 2,
text = "Connect",
onClick = function()
if startSession ~= nil then
local address = self.state.address
if address:len() == 0 then
address = Config.defaultHost
end
local port = self.state.port
if port:len() == 0 then
port = Config.defaultPort
end
local sessionOptions = {
openScriptsExternally = settings:get("openScriptsExternally"),
twoWaySync = settings:get("twoWaySync"),
}
startSession(address, port, sessionOptions)
end
end,
}),
}),
})
end)
end)
end
return ConnectPanel

View File

@@ -1,35 +0,0 @@
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin")
local Theme = require(Plugin.Components.Theme)
local Panel = require(Plugin.Components.Panel)
local FitText = require(Plugin.Components.FitText)
local e = Roact.createElement
local ConnectingPanel = Roact.Component:extend("ConnectingPanel")
function ConnectingPanel:render()
return Theme.with(function(theme)
return e(Panel, nil, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 8),
}),
Text = e(FitText, {
Padding = Vector2.new(12, 6),
Font = theme.ButtonFont,
TextSize = 18,
Text = "Connecting...",
TextColor3 = theme.Text1,
BackgroundTransparency = 1,
}),
})
end)
end
return ConnectingPanel

View File

@@ -1,47 +0,0 @@
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin")
local Theme = require(Plugin.Components.Theme)
local Panel = require(Plugin.Components.Panel)
local FitText = require(Plugin.Components.FitText)
local FormButton = require(Plugin.Components.FormButton)
local e = Roact.createElement
local ConnectionActivePanel = Roact.Component:extend("ConnectionActivePanel")
function ConnectionActivePanel:render()
local stopSession = self.props.stopSession
return Theme.with(function(theme)
return e(Panel, nil, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 8),
}),
Text = e(FitText, {
Padding = Vector2.new(12, 6),
Font = theme.ButtonFont,
TextSize = 18,
Text = "Connected to Live-Sync Server",
TextColor3 = theme.Text1,
BackgroundTransparency = 1,
}),
DisconnectButton = e(FormButton, {
layoutOrder = 2,
text = "Disconnect",
secondary = true,
onClick = function()
stopSession()
end,
}),
})
end)
end
return ConnectionActivePanel

View File

@@ -1,70 +0,0 @@
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin")
local Theme = require(Plugin.Components.Theme)
local Panel = require(Plugin.Components.Panel)
local FitText = require(Plugin.Components.FitText)
local FitScrollingFrame = require(Plugin.Components.FitScrollingFrame)
local FormButton = require(Plugin.Components.FormButton)
local e = Roact.createElement
local BUTTON_HEIGHT = 60
local HOR_PADDING = 8
local ErrorPanel = Roact.Component:extend("ErrorPanel")
function ErrorPanel:render()
local errorMessage = self.props.errorMessage
local onDismiss = self.props.onDismiss
return Theme.with(function(theme)
return e(Panel, nil, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 8),
}),
ErrorContainer = e(FitScrollingFrame, {
containerProps = {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(1, -HOR_PADDING * 2, 1, -BUTTON_HEIGHT),
Position = UDim2.new(0, HOR_PADDING, 0, 0),
ScrollBarImageColor3 = theme.Text1,
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
ScrollingDirection = Enum.ScrollingDirection.Y,
},
}, {
Text = e(FitText, {
Size = UDim2.new(1, 0, 0, 0),
LayoutOrder = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
FitAxis = "Y",
Font = theme.ButtonFont,
TextSize = 18,
Text = errorMessage,
TextWrap = true,
TextColor3 = theme.Text1,
BackgroundTransparency = 1,
}),
}),
DismissButton = e(FormButton, {
layoutOrder = 2,
text = "Dismiss",
secondary = true,
onClick = function()
onDismiss()
end,
}),
})
end)
end
return ErrorPanel

View File

@@ -1,63 +0,0 @@
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Dictionary = require(script.Parent.Parent.Dictionary)
local e = Roact.createElement
local FitList = Roact.Component:extend("FitList")
function FitList:init()
self.sizeBinding, self.setSize = Roact.createBinding(UDim2.new())
end
function FitList:render()
local containerKind = self.props.containerKind or "Frame"
local fitAxes = self.props.fitAxes or "XY"
local containerProps = self.props.containerProps
local layoutProps = self.props.layoutProps
local paddingProps = self.props.paddingProps
local padding
if paddingProps ~= nil then
padding = e("UIPadding", paddingProps)
end
local children = Dictionary.merge(self.props[Roact.Children], {
["$Layout"] = e("UIListLayout", Dictionary.merge({
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Change.AbsoluteContentSize] = function(instance)
local contentSize = instance.AbsoluteContentSize
if paddingProps ~= nil then
contentSize = contentSize + Vector2.new(
paddingProps.PaddingLeft.Offset + paddingProps.PaddingRight.Offset,
paddingProps.PaddingTop.Offset + paddingProps.PaddingBottom.Offset)
end
local combinedSize
if fitAxes == "X" then
combinedSize = UDim2.new(0, contentSize.X, containerProps.Size.Y.Scale, containerProps.Size.Y.Offset)
elseif fitAxes == "Y" then
combinedSize = UDim2.new(containerProps.Size.X.Scale, containerProps.Size.X.Offset, 0, contentSize.Y)
elseif fitAxes == "XY" then
combinedSize = UDim2.new(0, contentSize.X, 0, contentSize.Y)
else
error("Invalid fitAxes value")
end
self.setSize(combinedSize)
end,
}, layoutProps)),
["$Padding"] = padding,
})
local fullContainerProps = Dictionary.merge(containerProps, {
Size = self.sizeBinding,
})
return e(containerKind, fullContainerProps, children)
end
return FitList

View File

@@ -1,33 +0,0 @@
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Dictionary = require(script.Parent.Parent.Dictionary)
local e = Roact.createElement
local FitScrollingFrame = Roact.Component:extend("FitScrollingFrame")
function FitScrollingFrame:init()
self.sizeBinding, self.setSize = Roact.createBinding(UDim2.new())
end
function FitScrollingFrame:render()
local containerProps = self.props.containerProps
local layoutProps = self.props.layoutProps
local children = Dictionary.merge(self.props[Roact.Children], {
["$Layout"] = e("UIListLayout", Dictionary.merge({
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Change.AbsoluteContentSize] = function(instance)
self.setSize(UDim2.new(0, 0, 0, instance.AbsoluteContentSize.Y))
end,
}, layoutProps)),
})
local fullContainerProps = Dictionary.merge(containerProps, {
CanvasSize = self.sizeBinding,
})
return e("ScrollingFrame", fullContainerProps, children)
end
return FitScrollingFrame

View File

@@ -1,88 +0,0 @@
local TextService = game:GetService("TextService")
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Dictionary = require(script.Parent.Parent.Dictionary)
local e = Roact.createElement
local FitText = Roact.Component:extend("FitText")
function FitText:init()
self.ref = Roact.createRef()
self.sizeBinding, self.setSize = Roact.createBinding(UDim2.new())
end
function FitText:render()
local kind = self.props.Kind or "TextLabel"
local containerProps = Dictionary.merge(self.props, {
FitAxis = Dictionary.None,
Kind = Dictionary.None,
Padding = Dictionary.None,
MinSize = Dictionary.None,
Size = self.sizeBinding,
[Roact.Ref] = self.ref,
[Roact.Change.AbsoluteSize] = function()
self:updateTextMeasurements()
end
})
return e(kind, containerProps)
end
function FitText:didMount()
self:updateTextMeasurements()
end
function FitText:didUpdate()
self:updateTextMeasurements()
end
function FitText:updateTextMeasurements()
local minSize = self.props.MinSize or Vector2.new(0, 0)
local padding = self.props.Padding or Vector2.new(0, 0)
local fitAxis = self.props.FitAxis or "XY"
local baseSize = self.props.Size
local text = self.props.Text or ""
local font = self.props.Font or Enum.Font.Legacy
local textSize = self.props.TextSize or 12
local containerSize = self.ref.current.AbsoluteSize
local textBounds
if fitAxis == "XY" then
textBounds = Vector2.new(9e6, 9e6)
elseif fitAxis == "X" then
textBounds = Vector2.new(9e6, containerSize.Y - padding.Y * 2)
elseif fitAxis == "Y" then
textBounds = Vector2.new(containerSize.X - padding.X * 2, 9e6)
end
local measuredText = TextService:GetTextSize(text, textSize, font, textBounds)
local computedX = math.max(minSize.X, padding.X * 2 + measuredText.X)
local computedY = math.max(minSize.Y, padding.Y * 2 + measuredText.Y)
local totalSize
if fitAxis == "XY" then
totalSize = UDim2.new(
0, computedX,
0, computedY)
elseif fitAxis == "X" then
totalSize = UDim2.new(
0, computedX,
baseSize.Y.Scale, baseSize.Y.Offset)
elseif fitAxis == "Y" then
totalSize = UDim2.new(
baseSize.X.Scale, baseSize.X.Offset,
0, computedY)
end
self.setSize(totalSize)
end
return FitText

View File

@@ -1,64 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.Components.Theme)
local FitList = require(Plugin.Components.FitList)
local FitText = require(Plugin.Components.FitText)
local e = Roact.createElement
local RoundBox = Assets.Slices.RoundBox
local function FormButton(props)
local text = props.text
local layoutOrder = props.layoutOrder
local onClick = props.onClick
local textColor
local backgroundColor
return Theme.with(function(theme)
if props.secondary then
textColor = theme.Brand1
backgroundColor = theme.Background2
else
textColor = theme.TextOnAccent
backgroundColor = theme.Brand1
end
return e(FitList, {
containerKind = "ImageButton",
containerProps = {
LayoutOrder = layoutOrder,
BackgroundTransparency = 1,
Image = RoundBox.asset,
ImageRectOffset = RoundBox.offset,
ImageRectSize = RoundBox.size,
SliceCenter = RoundBox.center,
ScaleType = Enum.ScaleType.Slice,
ImageColor3 = backgroundColor,
[Roact.Event.Activated] = function()
if onClick ~= nil then
onClick()
end
end,
},
}, {
Text = e(FitText, {
Kind = "TextLabel",
Text = text,
TextSize = 18,
TextColor3 = textColor,
Font = theme.ButtonFont,
Padding = Vector2.new(16, 8),
BackgroundTransparency = 1,
}),
})
end)
end
return FormButton

View File

@@ -1,82 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement
local RoundBox = Assets.Slices.RoundBox
local TEXT_SIZE = 22
local PADDING = 8
local FormTextInput = Roact.Component:extend("FormTextInput")
function FormTextInput:init()
self:setState({
focused = false,
})
end
function FormTextInput:render()
local value = self.props.value
local placeholderValue = self.props.placeholderValue
local onValueChange = self.props.onValueChange
local layoutOrder = self.props.layoutOrder
local width = self.props.width
local shownPlaceholder
if self.state.focused then
shownPlaceholder = ""
else
shownPlaceholder = placeholderValue
end
return Theme.with(function(theme)
return e("ImageLabel", {
LayoutOrder = layoutOrder,
Image = RoundBox.asset,
ImageRectOffset = RoundBox.offset,
ImageRectSize = RoundBox.size,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = RoundBox.center,
ImageColor3 = theme.Background2,
Size = UDim2.new(width.Scale, width.Offset, 0, TEXT_SIZE + PADDING * 2),
BackgroundTransparency = 1,
}, {
InputInner = e("TextBox", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Font = theme.InputFont,
ClearTextOnFocus = false,
TextXAlignment = Enum.TextXAlignment.Center,
TextSize = TEXT_SIZE,
Text = value,
PlaceholderText = shownPlaceholder,
PlaceholderColor3 = theme.Text2,
TextColor3 = theme.Text1,
[Roact.Change.Text] = function(rbx)
onValueChange(rbx.Text)
end,
[Roact.Event.Focused] = function()
self:setState({
focused = true,
})
end,
[Roact.Event.FocusLost] = function()
self:setState({
focused = false,
})
end,
}),
})
end)
end
return FormTextInput

View File

@@ -1,38 +0,0 @@
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin")
local RojoFooter = require(Plugin.Components.RojoFooter)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement
local Panel = Roact.Component:extend("Panel")
function Panel:init()
self.footerSize, self.setFooterSize = Roact.createBinding(Vector2.new())
end
function Panel:render()
return Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = theme.Background1,
BorderSizePixel = 1,
}, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
Body = e("Frame", {
Size = UDim2.new(0, 360, 1, -32),
BackgroundTransparency = 1,
}, self.props[Roact.Children]),
Footer = e(RojoFooter),
})
end)
end
return Panel

View File

@@ -1,70 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Roact = require(Rojo.Roact)
local Config = require(Plugin.Config)
local Version = require(Plugin.Version)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement
local RojoFooter = Roact.Component:extend("RojoFooter")
function RojoFooter:init()
self.footerSize, self.setFooterSize = Roact.createBinding(Vector2.new())
self.footerVersionSize, self.setFooterVersionSize = Roact.createBinding(Vector2.new())
end
function RojoFooter:render()
return Theme.with(function(theme)
return e("Frame", {
LayoutOrder = 3,
Size = UDim2.new(1, 0, 0, 32),
BackgroundColor3 = theme.Background2,
BorderSizePixel = 0,
ZIndex = 2,
}, {
Padding = e("UIPadding", {
PaddingTop = UDim.new(0, 4),
PaddingBottom = UDim.new(0, 4),
PaddingLeft = UDim.new(0, 8),
PaddingRight = UDim.new(0, 8),
}),
LogoContainer = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 0, 32),
}, {
Logo = e("ImageLabel", {
Image = Assets.Images.Logo,
Size = UDim2.new(0, 80, 0, 40),
ScaleType = Enum.ScaleType.Fit,
BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 1, -10),
AnchorPoint = Vector2.new(0, 1),
}),
}),
Version = e("TextLabel", {
Position = UDim2.new(1, 0, 0, 0),
Size = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(1, 0),
Font = theme.TitleFont,
TextSize = 18,
Text = Version.display(Config.version),
TextXAlignment = Enum.TextXAlignment.Right,
TextColor3 = theme.Text2,
BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = function(rbx)
self.setFooterVersionSize(rbx.AbsoluteSize)
end,
}),
})
end)
end
return RojoFooter

View File

@@ -1,119 +0,0 @@
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
local Plugin = script:FindFirstAncestor("Plugin")
local Checkbox = require(Plugin.Components.Checkbox)
local FitList = require(Plugin.Components.FitList)
local FitText = require(Plugin.Components.FitText)
local FormButton = require(Plugin.Components.FormButton)
local Panel = require(Plugin.Components.Panel)
local PluginSettings = require(Plugin.Components.PluginSettings)
local Theme = require(Plugin.Components.Theme)
local e = Roact.createElement
local SettingsPanel = Roact.Component:extend("SettingsPanel")
function SettingsPanel:render()
local back = self.props.back
return Theme.with(function(theme)
return PluginSettings.with(function(settings)
return e(Panel, nil, {
Layout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 16),
}),
OpenScriptsExternally = e(FitList, {
containerProps = {
LayoutOrder = 1,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.MainFont,
TextSize = 16,
Text = "Open Scripts Externally",
TextColor3 = theme.Text1,
}),
Padding = e("Frame", {
Size = UDim2.new(0, 8, 0, 0),
BackgroundTransparency = 1,
LayoutOrder = 2,
}),
Input = e(Checkbox, {
layoutOrder = 3,
checked = settings:get("openScriptsExternally"),
onChange = function(newValue)
settings:set("openScriptsExternally", not settings:get("openScriptsExternally"))
end,
}),
}),
TwoWaySync = e(FitList, {
containerProps = {
LayoutOrder = 2,
BackgroundTransparency = 1,
},
layoutProps = {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
},
}, {
Label = e(FitText, {
Kind = "TextLabel",
LayoutOrder = 1,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Font = theme.MainFont,
TextSize = 16,
Text = "Two-Way Sync (Experimental!)",
TextColor3 = theme.Text1,
}),
Padding = e("Frame", {
Size = UDim2.new(0, 8, 0, 0),
BackgroundTransparency = 1,
LayoutOrder = 2,
}),
Input = e(Checkbox, {
layoutOrder = 3,
checked = settings:get("twoWaySync"),
onChange = function(newValue)
settings:set("twoWaySync", not settings:get("twoWaySync"))
end,
}),
}),
BackButton = e(FormButton, {
layoutOrder = 4,
text = "Okay",
secondary = true,
onClick = function()
back()
end,
}),
})
end)
end)
end
return SettingsPanel

View File

@@ -1,104 +0,0 @@
--[[
Theming system taking advantage of Roact's new context API.
Doesn't use colors provided by Studio and instead just branches on theme
name. This isn't exactly best practice.
]]
local Studio = settings():GetService("Studio")
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local Log = require(Rojo.Log)
local strict = require(script.Parent.Parent.strict)
local lightTheme = strict("Theme", {
ButtonFont = Enum.Font.GothamSemibold,
InputFont = Enum.Font.Code,
TitleFont = Enum.Font.GothamBold,
MainFont = Enum.Font.Gotham,
Brand1 = Color3.fromRGB(225, 56, 53),
Text1 = Color3.fromRGB(64, 64, 64),
Text2 = Color3.fromRGB(160, 160, 160),
TextOnAccent = Color3.fromRGB(235, 235, 235),
Background1 = Color3.fromRGB(255, 255, 255),
Background2 = Color3.fromRGB(235, 235, 235),
})
local darkTheme = strict("Theme", {
ButtonFont = Enum.Font.GothamSemibold,
InputFont = Enum.Font.Code,
TitleFont = Enum.Font.GothamBold,
MainFont = Enum.Font.Gotham,
Brand1 = Color3.fromRGB(225, 56, 53),
Text1 = Color3.fromRGB(235, 235, 235),
Text2 = Color3.fromRGB(200, 200, 200),
TextOnAccent = Color3.fromRGB(235, 235, 235),
Background1 = Color3.fromRGB(48, 48, 48),
Background2 = Color3.fromRGB(64, 64, 64),
})
local Context = Roact.createContext(lightTheme)
local StudioProvider = Roact.Component:extend("StudioProvider")
-- Pull the current theme from Roblox Studio and update state with it.
function StudioProvider:updateTheme()
local studioTheme = Studio.Theme
if studioTheme.Name == "Light" then
self:setState({
theme = lightTheme,
})
elseif studioTheme.Name == "Dark" then
self:setState({
theme = darkTheme,
})
else
Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name)
self:setState({
theme = lightTheme,
})
end
end
function StudioProvider:init()
self:updateTheme()
end
function StudioProvider:render()
return Roact.createElement(Context.Provider, {
value = self.state.theme,
}, self.props[Roact.Children])
end
function StudioProvider:didMount()
self.connection = Studio.ThemeChanged:Connect(function()
self:updateTheme()
end)
end
function StudioProvider:willUnmount()
self.connection:Disconnect()
end
local function with(callback)
return Roact.createElement(Context.Consumer, {
render = callback,
})
end
return {
StudioProvider = StudioProvider,
Consumer = Context.Consumer,
with = with,
}

View File

@@ -5,7 +5,7 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
return strict("Config", {
isDevBuild = isDevBuild,
codename = "Epiphany",
version = {6, 0, 0, "-rc.1"},
version = {6, 0, 2},
expectedServerVersionString = "6.0 or newer",
protocolVersion = 3,
defaultHost = "localhost",

View File

@@ -33,6 +33,16 @@ function InstanceMap.new(onInstanceChanged)
return setmetatable(self, InstanceMap)
end
function InstanceMap:size()
local size = 0
for _ in pairs(self.fromIds) do
size = size + 1
end
return size
end
--[[
Disconnect all connections and release all instance references.
]]
@@ -69,6 +79,9 @@ function InstanceMap:__fmtDebug(output)
end
function InstanceMap:insert(id, instance)
self:removeId(id)
self:removeInstance(instance)
self.fromIds[id] = instance
self.fromInstances[instance] = id
self:__connectSignals(instance)
@@ -81,8 +94,6 @@ function InstanceMap:removeId(id)
self:__disconnectSignals(instance)
self.fromIds[id] = nil
self.fromInstances[instance] = nil
else
Log.warn("Attempted to remove nonexistant ID {}", id)
end
end
@@ -93,8 +104,6 @@ function InstanceMap:removeInstance(instance)
if id ~= nil then
self.fromInstances[instance] = nil
self.fromIds[id] = nil
else
Log.warn("Attempted to remove nonexistant instance {}", instance)
end
end
@@ -102,10 +111,14 @@ function InstanceMap:destroyInstance(instance)
local id = self.fromInstances[instance]
if id ~= nil then
self:destroyId(id)
else
Log.warn("Attempted to destroy untracked instance {}", instance)
self:removeId(id)
end
for _, descendantInstance in ipairs(instance:GetDescendants()) do
self:removeInstance(descendantInstance)
end
instance:Destroy()
end
function InstanceMap:destroyId(id)
@@ -113,21 +126,11 @@ function InstanceMap:destroyId(id)
self:removeId(id)
if instance ~= nil then
local descendantsToDestroy = {}
for otherInstance in pairs(self.fromInstances) do
if otherInstance:IsDescendantOf(instance) then
table.insert(descendantsToDestroy, otherInstance)
end
end
for _, otherInstance in ipairs(descendantsToDestroy) do
self:removeInstance(otherInstance)
for _, descendantInstance in ipairs(instance:GetDescendants()) do
self:removeInstance(descendantInstance)
end
instance:Destroy()
else
Log.warn("Attempted to destroy nonexistant ID {}", id)
end
end

View File

@@ -16,10 +16,169 @@ PatchSet.validate = t.interface({
})
--[[
Invert the given PatchSet using the given instance map.
Create a new, empty PatchSet.
]]
function PatchSet.invert(patchSet, instanceMap)
error("not yet implemented", 2)
function PatchSet.newEmpty()
return {
removed = {},
added = {},
updated = {},
}
end
--[[
Tells whether the given PatchSet is empty.
]]
function PatchSet.isEmpty(patchSet)
return next(patchSet.removed) == nil and
next(patchSet.added) == nil and
next(patchSet.updated) == nil
end
--[[
Tells whether the given PatchSet has any remove operations.
]]
function PatchSet.hasRemoves(patchSet)
return next(patchSet.removed) ~= nil
end
--[[
Tells whether the given PatchSet has any add operations.
]]
function PatchSet.hasAdditions(patchSet)
return next(patchSet.added) ~= nil
end
--[[
Tells whether the given PatchSet has any update operations.
]]
function PatchSet.hasUpdates(patchSet)
return next(patchSet.updated) ~= nil
end
--[[
Merge multiple PatchSet objects into the given PatchSet.
]]
function PatchSet.assign(target, ...)
for i = 1, select("#", ...) do
local sourcePatch = select(i, ...)
for _, removed in ipairs(sourcePatch.removed) do
table.insert(target.removed, removed)
end
for id, added in pairs(sourcePatch.added) do
target.added[id] = added
end
for _, update in ipairs(sourcePatch.updated) do
table.insert(target.updated, update)
end
end
return target
end
--[[
Create a list of human-readable statements summarizing the contents of this
patch, intended to be displayed to users.
]]
function PatchSet.humanSummary(instanceMap, patchSet)
local statements = {}
for _, idOrInstance in ipairs(patchSet.removed) do
local instance, id
if type(idOrInstance) == "string" then
id = idOrInstance
instance = instanceMap.fromIds[id]
else
instance = idOrInstance
id = instanceMap.fromInstances[instance]
end
if instance ~= nil then
table.insert(statements, string.format("- Delete instance %s", instance:GetFullName()))
else
table.insert(statements, string.format("- Delete instance with ID %s", id))
end
end
local additionsMentioned = {}
local function addAllDescendents(virtualInstance)
additionsMentioned[virtualInstance.Id] = true
for _, childId in ipairs(virtualInstance.Children) do
addAllDescendents(patchSet.added[childId])
end
end
for id, addition in pairs(patchSet.added) do
if additionsMentioned[id] then
continue
end
local virtualInstance = addition
while true do
if virtualInstance.Parent == nil then
break
end
local virtualParent = patchSet.added[virtualInstance.Parent]
if virtualParent == nil then
break
end
virtualInstance = virtualParent
end
local parentDisplayName = "nil (how strange!)"
if virtualInstance.Parent ~= nil then
local parent = instanceMap.fromIds[virtualInstance.Parent]
if parent ~= nil then
parentDisplayName = parent:GetFullName()
end
end
table.insert(statements, string.format(
"- Add instance %q (ClassName %q) to %s",
virtualInstance.Name, virtualInstance.ClassName, parentDisplayName))
end
for _, update in ipairs(patchSet.updated) do
local updatedProperties = {}
if update.changedMetadata ~= nil then
table.insert(updatedProperties, "Rojo's Metadata")
end
if update.changedName ~= nil then
table.insert(updatedProperties, "Name")
end
if update.changedClassName ~= nil then
table.insert(updatedProperties, "ClassName")
end
for name in pairs(update.changedProperties) do
table.insert(updatedProperties, name)
end
local instance = instanceMap.fromIds[update.id]
local displayName
if instance ~= nil then
displayName = instance:GetFullName()
else
displayName = "[unknown instance]"
end
table.insert(statements, string.format(
"- Update properties on %s: %s",
displayName, table.concat(updatedProperties, ",")))
end
return table.concat(statements, "\n")
end
return PatchSet

View File

@@ -1,389 +0,0 @@
--[[
This module defines the meat of the Rojo plugin and how it manages tracking
and mutating the Roblox DOM.
]]
local RbxDom = require(script.Parent.Parent.RbxDom)
local t = require(script.Parent.Parent.t)
local Types = require(script.Parent.Types)
local invariant = require(script.Parent.invariant)
local getCanonicalProperty = require(script.Parent.getCanonicalProperty)
local setCanonicalProperty = require(script.Parent.setCanonicalProperty)
local PatchSet = require(script.Parent.PatchSet)
--[[
Attempt to safely set the parent of an instance.
This function will always succeed, even if the actual set failed. This is
important for some types like services that will throw even if their current
parent is already set to the requested parent.
TODO: See if we can eliminate this by being more nuanced with property
assignment?
]]
local function safeSetParent(instance, newParent)
pcall(function()
instance.Parent = newParent
end)
end
--[[
Similar to setting Parent, some instances really don't like being renamed.
TODO: Should we be throwing away these results or can we be more careful?
]]
local function safeSetName(instance, name)
pcall(function()
instance.Name = name
end)
end
local Reconciler = {}
Reconciler.__index = Reconciler
function Reconciler.new(instanceMap)
local self = {
-- Tracks all of the instances known by the reconciler by ID.
__instanceMap = instanceMap,
}
return setmetatable(self, Reconciler)
end
--[[
See Reconciler:__hydrateInternal().
]]
function Reconciler:hydrate(apiInstances, id, instance)
local hydratePatch = {
removed = {},
added = {},
updated = {},
}
self:__hydrateInternal(apiInstances, id, instance, hydratePatch)
return hydratePatch
end
--[[
Applies a patch to the Roblox DOM using the reconciler's internal state.
TODO: This function might only apply some of the patch in the future and
require content negotiation with the Rojo server to handle types that aren't
editable by scripts.
]]
local applyPatchSchema = Types.ifEnabled(t.tuple(
PatchSet.validate
))
function Reconciler:applyPatch(patch)
assert(applyPatchSchema(patch))
for _, removedIdOrInstance in ipairs(patch.removed) do
local removedInstance
if Types.RbxId(removedIdOrInstance) then
-- If this value is an ID, it's assumed to be an instance that the
-- Rojo server knows about.
removedInstance = self.__instanceMap.fromIds[removedIdOrInstance]
self.__instanceMap:removeId(removedIdOrInstance)
end
-- If this entry was an ID that we didn't know about, removedInstance
-- will be nil, which we guard against in case of minor tree desync.
if removedInstance ~= nil then
-- Ensure that if any descendants are tracked by Rojo, that we
-- properly un-track them.
for _, descendantInstance in ipairs(removedInstance:GetDescendants()) do
self.__instanceMap:removeInstance(descendantInstance)
end
removedInstance:Destroy()
end
end
-- TODO: This loop assumes that apiInstance.ParentId is never nil. The Rojo
-- plugin can't create a new top-level DataModel anyways, so this should
-- only be violated in cases that are already erroneous.
for id, apiInstance in pairs(patch.added) do
if self.__instanceMap.fromIds[id] == nil then
-- Find the first ancestor of this instance that is marked for an
-- addition.
--
-- This helps us make sure we only reify each instance once, and we
-- start from the top.
while patch.added[apiInstance.Parent] ~= nil do
id = apiInstance.Parent
apiInstance = patch.added[id]
end
local parentInstance = self.__instanceMap.fromIds[apiInstance.Parent]
if parentInstance == nil then
invariant(
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
id,
apiInstance.Parent,
self.__instanceMap
)
end
self:__reifyInstance(patch.added, id, parentInstance)
end
end
for _, update in ipairs(patch.updated) do
local instance = self.__instanceMap.fromIds[update.id]
if instance == nil then
invariant(
"Cannot update an instance that does not exist in the reconciler's state.\nInstance {}\nState: {:#?}",
update.id,
self.__instanceMap
)
end
if update.changedClassName ~= nil then
error("TODO: Support changing class name by destroying + recreating instance.")
end
if update.changedName ~= nil then
instance.Name = update.changedName
end
if update.changedMetadata ~= nil then
print("TODO: Support changing metadata, if necessary.")
end
if update.changedProperties ~= nil then
for propertyName, propertyValue in pairs(update.changedProperties) do
-- TODO: Gracefully handle this error instead?
assert(setCanonicalProperty(instance, propertyName, self:__decodeApiValue(propertyValue)))
end
end
end
end
--[[
Transforms a value into one that can be sent over the network back to the
Rojo server.
This operation can fail, and so it returns bool, value.
]]
function Reconciler:encodeApiValue(value)
if typeof(value) == "string" then
return true, {
Type = "String",
Value = value,
}
end
return false
end
--[[
Transforms a value encoded by rbx_dom_weak on the server side into a value
usable by Rojo's reconciler, potentially using RbxDom.
]]
function Reconciler:__decodeApiValue(apiValue)
assert(Types.ApiValue(apiValue))
-- Refs are represented as IDs in the same space that Rojo's protocol uses.
if apiValue.Type == "Ref" then
-- TODO: This ref could be pointing at an instance we haven't created
-- yet!
return self.__instanceMap.fromIds[apiValue.Value]
end
local success, decodedValue = RbxDom.EncodedValue.decode(apiValue)
if not success then
error(decodedValue, 2)
end
return decodedValue
end
--[[
Constructs an instance from an ApiInstance without any of its children.
]]
local reifySingleInstanceSchema = Types.ifEnabled(t.tuple(
Types.ApiInstance
))
function Reconciler:__reifySingleInstance(apiInstance)
assert(reifySingleInstanceSchema(apiInstance))
-- Instance.new can fail if we're passing in something that can't be
-- created, like a service, something enabled with a feature flag, or
-- something that requires higher security than we have.
local ok, instance = pcall(Instance.new, apiInstance.ClassName)
if not ok then
return false, instance
end
-- TODO: When can setting Name fail here?
safeSetName(instance, apiInstance.Name)
for key, value in pairs(apiInstance.Properties) do
setCanonicalProperty(instance, key, self:__decodeApiValue(value))
end
return true, instance
end
--[[
Construct an instance and all of its descendants, parent it to the given
instance, and insert it into the reconciler's internal state.
]]
local reifyInstanceSchema = Types.ifEnabled(t.tuple(
t.map(Types.RbxId, Types.VirtualInstance),
Types.RbxId,
t.Instance
))
function Reconciler:__reifyInstance(apiInstances, id, parentInstance)
assert(reifyInstanceSchema(apiInstances, id, parentInstance))
local apiInstance = apiInstances[id]
local ok, instance = self:__reifySingleInstance(apiInstance)
-- TODO: Propagate this error upward to handle it elsewhere?
if not ok then
error(("Couldn't create an instance of type %q, a child of %s"):format(
apiInstance.ClassName,
parentInstance:GetFullName()
))
end
self.__instanceMap:insert(id, instance)
for _, childId in ipairs(apiInstance.Children) do
self:__reifyInstance(apiInstances, childId, instance)
end
safeSetParent(instance, parentInstance)
return instance
end
--[[
Populates the reconciler's internal state, maps IDs to instances that the
Rojo plugin knows about, and generates a patch that would update the Roblox
tree to match Rojo's view of the tree.
]]
local hydrateSchema = Types.ifEnabled(t.tuple(
t.map(Types.RbxId, Types.VirtualInstance),
Types.RbxId,
t.Instance,
PatchSet.validate
))
function Reconciler:__hydrateInternal(apiInstances, id, instance, hydratePatch)
assert(hydrateSchema(apiInstances, id, instance, hydratePatch))
self.__instanceMap:insert(id, instance)
local apiInstance = apiInstances[id]
local function markIdAdded(id)
local apiInstance = apiInstances[id]
hydratePatch.added[id] = apiInstance
for _, childId in ipairs(apiInstance.Children) do
markIdAdded(childId)
end
end
local changedName = nil
local changedProperties = {}
if apiInstance.Name ~= instance.Name then
changedName = apiInstance.Name
end
for propertyName, virtualValue in pairs(apiInstance.Properties) do
local success, existingValue = getCanonicalProperty(instance, propertyName)
if success then
local decodedValue = self:__decodeApiValue(virtualValue)
if existingValue ~= decodedValue then
changedProperties[propertyName] = virtualValue
end
end
end
-- If any properties differed from the virtual instance we read, add it to
-- the hydrate patch so that we can catch up.
if changedName ~= nil or next(changedProperties) ~= nil then
table.insert(hydratePatch.updated, {
id = id,
changedName = changedName,
changedClassName = nil,
changedProperties = changedProperties,
changedMetadata = nil,
})
end
local existingChildren = instance:GetChildren()
-- For each existing child, we'll track whether it's been paired with an
-- instance that the Rojo server knows about.
local isExistingChildVisited = {}
for i = 1, #existingChildren do
isExistingChildVisited[i] = false
end
for _, childId in ipairs(apiInstance.Children) do
local apiChild = apiInstances[childId]
local childInstance
for childIndex, instance in ipairs(existingChildren) do
if not isExistingChildVisited[childIndex] then
-- We guard accessing Name and ClassName in order to avoid
-- tripping over children of DataModel that Rojo won't have
-- permissions to access at all.
local ok, name, className = pcall(function()
return instance.Name, instance.ClassName
end)
-- This rule is very conservative and could be loosened in the
-- future, or more heuristics could be introduced.
if ok and name == apiChild.Name and className == apiChild.ClassName then
childInstance = instance
isExistingChildVisited[childIndex] = true
break
end
end
end
if childInstance ~= nil then
-- We found an instance that matches the instance from the API, yay!
self:__hydrateInternal(apiInstances, childId, childInstance, hydratePatch)
else
markIdAdded(childId)
end
end
-- Any unvisited children at this point aren't known by Rojo and we can
-- destroy them unless the user has explicitly asked us to preserve children
-- of this instance.
local shouldClearUnknown = self:__shouldClearUnknownChildren(apiInstance)
if shouldClearUnknown then
for childIndex, visited in ipairs(isExistingChildVisited) do
if not visited then
table.insert(hydratePatch.removed, existingChildren[childIndex])
end
end
end
end
function Reconciler:__shouldClearUnknownChildren(apiInstance)
if apiInstance.Metadata ~= nil then
return not apiInstance.Metadata.ignoreUnknownInstances
else
return true
end
end
return Reconciler

Some files were not shown because too many files have changed in this diff Show More