Compare commits

..

19 Commits

Author SHA1 Message Date
Micah
5bd3c74db0 Release Rojo v7.4.4 (#964) 2024-08-22 10:06:13 -07:00
Micah
f4e2f5aefc Release Rojo 7.4.3 (#959) 2024-08-06 11:26:00 -07:00
Micah
8ceb40a24e Update rbx-binary to 0.7.6 (#958) 2024-08-06 11:21:41 -07:00
Micah
3e53d67412 In the plugin, don't write properties if they're nil and also a number (#955) 2024-08-02 10:02:32 -07:00
Micah
844f51d916 Update version of aftman used in release workflow 2024-07-23 11:14:18 -07:00
Micah
26974ffd4c Release v7.4.2 (#950) 2024-07-23 11:02:23 -07:00
Micah
91f5b4a675 Update memofs in 7.4.x branch (#949)
Backports the release of memofs v0.3.0
2024-07-23 10:42:32 -07:00
Micah
d179240139 Update rbx_dom for 7.4.x branch (#948) 2024-07-23 10:35:06 -07:00
Micah
67b6a7e198 Backport #917 to Rojo 7.4.x branch (#947) 2024-07-22 12:11:28 -07:00
Micah
3b721242c1 Backport #893 and #903 to Rojo 7.4 (#946)
As part of prep for a 7.4.2 release, this backports changes to the 7.4.X
branch that we can reasonably ship in 7.4.2 without too many code
changes.
2024-07-22 11:55:28 -07:00
EgoMoose
c6ceaa5c87 Trim plugin version string (#889)
This PR is a very small change that fixes the string pattern that reads
the rojo version from `Version.txt`. Currently this reads an extra
new-line character which makes reading the version text in the plugin
difficult.

It seems the rust side of things already trims the string when
comparing, but the lua side does not.

Current:

![pO6gtOXAZq](https://github.com/rojo-rbx/rojo/assets/6201941/1a03fced-f2b5-4a4e-a82d-e11fb0a52af7)

Fix:

![RobloxStudioBeta_GHmiJKAoa3](https://github.com/rojo-rbx/rojo/assets/6201941/3ce711df-fdc6-4f20-8771-5f5118ee013f)

Apologies if I skipped over some process of submitting a bug and / or am
basing on the wrong branch etc.
2024-03-13 09:49:33 -07:00
Kenneth Loeffler
af9629c53f Release 7.4.1 (#872) 2024-02-20 17:41:45 -08:00
Micah
9509909f46 Backport #870 (optional project names) to 7.4.x (#871)
Unlike most of the other backports, this code couldn't be directly
translated so it had to be re-implemented. Luckily, it is very simple.
This implementation is a bit messy and heavy handed with potential
panics, but I think it's probably fine since file names that aren't
UTF-8 aren't really supported anyway. The original implementation is a
lot cleaner though.

The test snapshots are (almost) all identical between the 7.5
implementation and this one. The sole exception is with the path in the
`snapshot_middleware::project` test, since I didn't feel like adding a
`name` parameter to `snapshot_project` in this implementation.
2024-02-20 17:25:05 -08:00
Kenneth Loeffler
88efbd433f Backport #868 to 7.4 (custom pivot geter/setter) (#869)
This PR backports some changes to rbx_dom_lua to fix serving model
pivots
2024-02-20 12:22:27 -08:00
Kenneth Loeffler
f716928683 Add entry for model pivot build fix to 7.4.x changelog (#867) 2024-02-20 12:09:13 -08:00
Kenneth Loeffler
e23d024ba3 Insert Model.NeedsPivotMigration in insert_instance when missing (#865) 2024-02-20 09:11:26 -08:00
Kenneth Loeffler
591419611e Backport #854 to Rojo 7.4 (Lua LF normalization) (#857) 2024-02-14 10:18:46 -08:00
Kenneth Loeffler
f68beab1df Backport #847 to 7.4 (gracefully handle gateway timeouts) (#851)
This PR adds a fix for gateway timeout handling to the 7.4.x branch

Co-authored-by: boatbomber <zack@boatbomber.com>
2024-02-03 20:30:10 -08:00
Kenneth Loeffler
2798610afd Backport #848, #846, #845, #844 to 7.4 (#849)
Co-authored-by: boatbomber <zack@boatbomber.com>
2024-02-01 13:23:51 -08:00
253 changed files with 4622 additions and 15973 deletions

View File

@@ -1,2 +0,0 @@
((nil . ((eglot-luau-rojo-project-path . "plugin.project.json")
(eglot-luau-rojo-sourcemap-enabled . 't))))

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
*.lua linguist-language=Luau

View File

@@ -11,12 +11,12 @@ jobs:
name: Check Actions name: Check Actions
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Changelog check - name: Changelog check
uses: Zomzog/changelog-checker@v1.3.0 uses: Zomzog/changelog-checker@v1.3.0
with: with:
fileName: CHANGELOG.md fileName: CHANGELOG.md
noChangelogLabel: skip changelog noChangelogLabel: skip changelog
checkNotification: Simple checkNotification: Simple
env: env:

View File

@@ -19,20 +19,24 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
- name: Rust cache - name: Rust cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Setup Aftman - name: Setup Aftman
uses: ok-nick/setup-aftman@v0.4.2 uses: ok-nick/setup-aftman@v0.3.0
with: with:
version: 'v0.3.0' version: 'v0.2.7'
- name: Build - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
@@ -45,20 +49,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@1.70.0 uses: actions-rs/toolchain@v1
with:
toolchain: 1.70.0
override: true
profile: minimal
- name: Rust cache - name: Rust cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Setup Aftman - name: Setup Aftman
uses: ok-nick/setup-aftman@v0.4.2 uses: ok-nick/setup-aftman@v0.3.0
with: with:
version: 'v0.3.0' version: 'v0.2.7'
- name: Build - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
@@ -68,22 +76,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
with: with:
toolchain: stable
override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Rust cache - name: Rust cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Setup Aftman - name: Setup Aftman
uses: ok-nick/setup-aftman@v0.4.2 uses: ok-nick/setup-aftman@v0.3.0
with: with:
version: 'v0.3.0' version: 'v0.2.7'
- name: Stylua - name: Stylua
run: stylua --check plugin/src run: stylua --check plugin/src

View File

@@ -8,39 +8,51 @@ jobs:
create-release: create-release:
name: Create Release name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
- uses: actions/checkout@v4
- name: Create Release - name: Create Release
id: create_release
uses: actions/create-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | with:
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }} tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: false
build-plugin: build-plugin:
needs: ["create-release"] needs: ["create-release"]
name: Build Roblox Studio Plugin name: Build Roblox Studio Plugin
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Setup Aftman - name: Setup Aftman
uses: ok-nick/setup-aftman@v0.4.2 uses: ok-nick/setup-aftman@v0.1.0
with: with:
token: ${{ secrets.GITHUB_TOKEN }}
trust-check: false
version: 'v0.3.0' version: 'v0.3.0'
- name: Build Plugin - name: Build Plugin
run: rojo build plugin.project.json --output Rojo.rbxm run: rojo build plugin --output Rojo.rbxm
- name: Upload Plugin to Release - name: Upload Plugin to Release
uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | with:
gh release upload ${{ github.ref_name }} Rojo.rbxm upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: Rojo.rbxm
asset_name: Rojo.rbxm
asset_content_type: application/octet-stream
- name: Upload Plugin to Artifacts - name: Upload Plugin to Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: Rojo.rbxm name: Rojo.rbxm
path: Rojo.rbxm path: Rojo.rbxm
@@ -53,7 +65,7 @@ jobs:
# https://doc.rust-lang.org/rustc/platform-support.html # https://doc.rust-lang.org/rustc/platform-support.html
include: include:
- host: linux - host: linux
os: ubuntu-latest os: ubuntu-20.04
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
label: linux-x86_64 label: linux-x86_64
@@ -77,19 +89,31 @@ jobs:
env: env:
BIN: rojo BIN: rojo
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Get Version from Tag
shell: bash
# https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
run: |
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
echo "Version is: ${{ env.PROJECT_VERSION }}"
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
with: with:
targets: ${{ matrix.target }} toolchain: stable
target: ${{ matrix.target }}
override: true
profile: minimal
- name: Setup Aftman - name: Setup Aftman
uses: ok-nick/setup-aftman@v0.4.2 uses: ok-nick/setup-aftman@v0.1.0
with: with:
version: 'v0.3.0' token: ${{ secrets.GITHUB_TOKEN }}
trust-check: false
version: 'v0.2.6'
- name: Build Release - name: Build Release
run: cargo build --release --locked --verbose --target ${{ matrix.target }} run: cargo build --release --locked --verbose --target ${{ matrix.target }}
@@ -98,34 +122,37 @@ jobs:
# easily. # easily.
CARGO_TARGET_DIR: output CARGO_TARGET_DIR: output
- name: Generate Artifact Name # On platforms that use OpenSSL, ensure it is statically linked to
shell: bash # make binaries more portable.
env: OPENSSL_STATIC: 1
TAG_NAME: ${{ github.ref_name }}
run: |
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV"
- name: Create Archive and Upload to Release - name: Create Release Archive
shell: bash shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
mkdir staging mkdir staging
if [ "${{ matrix.host }}" = "windows" ]; then if [ "${{ matrix.host }}" = "windows" ]; then
cp "output/${{ matrix.target }}/release/$BIN.exe" staging/ cp "output/${{ matrix.target }}/release/$BIN.exe" staging/
cd staging cd staging
7z a ../$ARTIFACT_NAME * 7z a ../release.zip *
else else
cp "output/${{ matrix.target }}/release/$BIN" staging/ cp "output/${{ matrix.target }}/release/$BIN" staging/
cd staging cd staging
zip ../$ARTIFACT_NAME * zip ../release.zip *
fi fi
gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME - name: Upload Archive to Release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: release.zip
asset_name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
asset_content_type: application/octet-stream
- name: Upload Archive to Artifacts - name: Upload Archive to Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
path: ${{ env.ARTIFACT_NAME }} name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
name: ${{ env.ARTIFACT_NAME }} path: release.zip

8
.gitignore vendored
View File

@@ -10,8 +10,8 @@
/*.rbxl /*.rbxl
/*.rbxlx /*.rbxlx
# Sourcemap for the Rojo plugin (for better intellisense) # Test places for the Roblox Studio Plugin
/sourcemap.json /plugin/*.rbxlx
# Roblox Studio holds 'lock' files on places # Roblox Studio holds 'lock' files on places
*.rbxl.lock *.rbxl.lock
@@ -19,7 +19,3 @@
# Snapshot files from the 'insta' Rust crate # Snapshot files from the 'insta' Rust crate
**/*.snap.new **/*.snap.new
# Macos file system junk
._*
.DS_STORE

View File

@@ -1,8 +0,0 @@
{
"recommendations": [
"JohnnyMorganz.luau-lsp",
"JohnnyMorganz.stylua",
"Kampfkarren.selene-vscode",
"rust-lang.rust-analyzer"
]
}

View File

@@ -1,4 +0,0 @@
{
"luau-lsp.sourcemap.rojoProjectFile": "plugin.project.json",
"luau-lsp.sourcemap.autogenerate": true
}

View File

@@ -1,120 +1,6 @@
# Rojo Changelog # Rojo Changelog
## 7.5.1 - April 25th, 2025 ## Unreleased Changes
* Fixed output spam related to `Instance.Capabilities` in the plugin
## 7.5.0 - April 25th, 2025
* Fixed an edge case that caused model pivots to not be built correctly in some cases ([#1027])
* Add `blockedPlaceIds` project config field to allow blocking place ids from being live synced ([#1021])
* Adds support for `.plugin.lua(u)` files - this applies the `Plugin` RunContext. ([#1008])
* Added support for Roblox's `Content` type. This replaces the old `Content` type with `ContentId` to reflect Roblox's change.
If you were previously using the fully-qualified syntax for `Content` you will need to switch it to `ContentId`.
* Added support for `Enum` attributes
* Significantly improved performance of `.rbxm` parsing
* Support for a `$schema` field in all special JSON files (`.project.json`, `.model.json`, and `.meta.json`) ([#974])
* Projects may now manually link `Ref` properties together using `Attributes`. ([#843])
This has two parts: using `id` or `$id` in JSON files or a `Rojo_Target` attribute, an Instance
is given an ID. Then, that ID may be used elsewhere in the project to point to an Instance
using an attribute named `Rojo_Target_PROP_NAME`, where `PROP_NAME` is the name of a property.
As an example, here is a `model.json` for an ObjectValue that refers to itself:
```json
{
"id": "arbitrary string",
"attributes": {
"Rojo_Target_Value": "arbitrary string"
}
}
```
This is a very rough implementation and the usage will become more ergonomic
over time.
* Updated Undo/Redo history to be more robust ([#915])
* Added popout diff visualizer for table properties like Attributes and Tags ([#834])
* Updated Theme to use Studio colors ([#838])
* Improved patch visualizer UX ([#883])
* Added update notifications for newer compatible versions in the Studio plugin. ([#832])
* Added experimental setting for Auto Connect in playtests ([#840])
* Improved settings UI ([#886])
* `Open Scripts Externally` option can now be changed while syncing ([#911])
* The sync reminder notification will now tell you what was last synced and when ([#987])
* Fixed notification and tooltip text sometimes getting cut off ([#988])
* Projects may now specify rules for syncing files as if they had a different file extension. ([#813])
This is specified via a new field on project files, `syncRules`:
```json
{
"syncRules": [
{
"pattern": "*.foo",
"use": "text",
"exclude": "*.exclude.foo",
},
{
"pattern": "*.bar.baz",
"use": "json",
"suffix": ".bar.baz",
},
],
"name": "SyncRulesAreCool",
"tree": {
"$path": "src"
}
}
```
The `pattern` field is a glob used to match the sync rule to files. If present, the `suffix` field allows you to specify parts of a file's name get cut off by Rojo to name the Instance, including the file extension. If it isn't specified, Rojo will only cut off the first part of the file extension, up to the first dot.
Additionally, the `exclude` field allows files to be excluded from the sync rule if they match a pattern specified by it. If it's not present, all files that match `pattern` will be modified using the sync rule.
The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type.
| `use` value | file extension |
|:---------------|:----------------|
| `serverScript` | `.server.lua` |
| `clientScript` | `.client.lua` |
| `moduleScript` | `.lua` |
| `json` | `.json` |
| `toml` | `.toml` |
| `csv` | `.csv` |
| `text` | `.txt` |
| `jsonModel` | `.model.json` |
| `rbxm` | `.rbxm` |
| `rbxmx` | `.rbxmx` |
| `project` | `.project.json` |
| `ignore` | None! |
Additionally, there are `use` values for specific script types ([#909]):
| `use` value | script type |
|:-------------------------|:---------------------------------------|
| `legacyServerScript` | `Script` with `Enum.RunContext.Legacy` |
| `legacyClientScript` | `LocalScript` |
| `runContextServerScript` | `Script` with `Enum.RunContext.Server` |
| `runContextClientScript` | `Script` with `Enum.RunContext.Client` |
| `pluginScript` | `Script` with `Enum.RunContext.Plugin` |
**All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced!
[#813]: https://github.com/rojo-rbx/rojo/pull/813
[#832]: https://github.com/rojo-rbx/rojo/pull/832
[#834]: https://github.com/rojo-rbx/rojo/pull/834
[#838]: https://github.com/rojo-rbx/rojo/pull/838
[#840]: https://github.com/rojo-rbx/rojo/pull/840
[#843]: https://github.com/rojo-rbx/rojo/pull/843
[#883]: https://github.com/rojo-rbx/rojo/pull/883
[#886]: https://github.com/rojo-rbx/rojo/pull/886
[#909]: https://github.com/rojo-rbx/rojo/pull/909
[#911]: https://github.com/rojo-rbx/rojo/pull/911
[#915]: https://github.com/rojo-rbx/rojo/pull/915
[#974]: https://github.com/rojo-rbx/rojo/pull/974
[#987]: https://github.com/rojo-rbx/rojo/pull/987
[#988]: https://github.com/rojo-rbx/rojo/pull/988
[#1008]: https://github.com/rojo-rbx/rojo/pull/1008
[#1021]: https://github.com/rojo-rbx/rojo/pull/1021
[#1027]: https://github.com/rojo-rbx/rojo/pull/1027
## [7.4.4] - August 22nd, 2024 ## [7.4.4] - August 22nd, 2024
* Fixed issue with reading attributes from `Lighting` in new place files * Fixed issue with reading attributes from `Lighting` in new place files
@@ -124,7 +10,7 @@
* Fixed issue with building binary files introduced in 7.4.2 * Fixed issue with building binary files introduced in 7.4.2
* Fixed `value of type nil cannot be converted to number` warning spam in output. [#955] * Fixed `value of type nil cannot be converted to number` warning spam in output. [#955]
[#955]: https://github.com/rojo-rbx/rojo/pull/955 [#955]: https://github.com/rojo-rbx/rojo/pull/893
## [7.4.2] - July 23, 2024 ## [7.4.2] - July 23, 2024
* Added Never option to Confirmation ([#893]) * Added Never option to Confirmation ([#893])
@@ -139,8 +25,7 @@
## [7.4.1] - February 20, 2024 ## [7.4.1] - February 20, 2024
* Made the `name` field optional on project files ([#870]) * Made the `name` field optional on project files ([#870])
Files named `default.project.json` inherit the name of the folder they're in and all other projects
Files named `default.project.json` inherit the name of the folder they're in and all other projects
are named as expect (e.g. `foo.project.json` becomes an Instance named `foo`) are named as expect (e.g. `foo.project.json` becomes an Instance named `foo`)
There is no change in behavior if `name` is set. There is no change in behavior if `name` is set.

View File

@@ -16,23 +16,6 @@ You'll want these tools to work on Rojo:
* Latest stable Rust compiler * Latest stable Rust compiler
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo) * Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
* [Foreman](https://github.com/Roblox/foreman) * [Foreman](https://github.com/Roblox/foreman)
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
When working on the Studio plugin, we recommend using this command to automatically rebuild the plugin when you save a change:
*(Make sure you've enabled the Studio setting to reload plugins on file change!)*
```bash
bash scripts/watch-build-plugin.sh
```
You can also run the plugin's unit tests with the following:
*(Make sure you have `run-in-roblox` installed first!)*
```bash
bash scripts/unit-test-plugin.sh
```
## Documentation ## Documentation
Documentation impacts way more people than the individual lines of code we write. Documentation impacts way more people than the individual lines of code we write.

1129
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.5.1" version = "7.4.4"
rust-version = "1.70.0" rust-version = "1.70.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers" description = "Enables professional-grade development tools for Roblox developers"
@@ -26,9 +26,7 @@ default = []
# Enable this feature to live-reload assets from the web UI. # Enable this feature to live-reload assets from the web UI.
dev_live_assets = [] dev_live_assets = []
# Run Rojo with this feature to open a Tracy session. profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
# Currently uses protocol v63, last supported in Tracy 0.9.1.
profile-with-tracy = ["profiling/profile-with-tracy"]
[workspace] [workspace]
members = ["crates/*"] members = ["crates/*"]
@@ -51,44 +49,46 @@ memofs = { version = "0.3.0", path = "crates/memofs" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" } # rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" } # rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "1.0.0" rbx_binary = "0.7.7"
rbx_dom_weak = "3.0.0" rbx_dom_weak = "2.9.0"
rbx_reflection = "5.0.0" rbx_reflection = "4.7.0"
rbx_reflection_database = "1.0.3" rbx_reflection_database = "0.2.12"
rbx_xml = "1.0.0" rbx_xml = "0.13.5"
anyhow = "1.0.80" anyhow = "1.0.44"
backtrace = "0.3.69" backtrace = "0.3.61"
bincode = "1.3.3" bincode = "1.3.3"
crossbeam-channel = "0.5.12" crossbeam-channel = "0.5.1"
csv = "1.3.0" csv = "1.1.6"
env_logger = "0.9.3" env_logger = "0.9.0"
fs-err = "2.11.0" fs-err = "2.6.0"
futures = "0.3.30" futures = "0.3.17"
globset = "0.4.14" globset = "0.4.8"
humantime = "2.1.0" humantime = "2.1.0"
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] } hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
jod-thread = "0.1.2" jod-thread = "0.1.2"
log = "0.4.21" log = "0.4.14"
num_cpus = "1.16.0" maplit = "1.0.2"
opener = "0.5.2" num_cpus = "1.15.0"
rayon = "1.9.0" opener = "0.5.0"
reqwest = { version = "0.11.24", default-features = false, features = [ rayon = "1.7.0"
reqwest = { version = "0.11.10", features = [
"blocking", "blocking",
"json", "json",
"rustls-tls", "native-tls-vendored",
] } ] }
ritz = "0.1.0" ritz = "0.1.0"
roblox_install = "1.0.0" roblox_install = "1.0.0"
serde = { version = "1.0.197", features = ["derive", "rc"] } serde = { version = "1.0.130", features = ["derive", "rc"] }
serde_json = "1.0.114" serde_json = "1.0.68"
toml = "0.5.11" toml = "0.5.9"
termcolor = "1.4.1" termcolor = "1.1.2"
thiserror = "1.0.57" thiserror = "1.0.30"
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] } tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
uuid = { version = "1.7.0", features = ["v4", "serde"] } uuid = { version = "1.0.0", features = ["v4", "serde"] }
clap = { version = "3.2.25", features = ["derive"] } clap = { version = "3.1.18", features = ["derive"] }
profiling = "1.0.15" profiling = "1.0.6"
tracy-client = { version = "0.13.2", optional = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.10.1" winreg = "0.10.1"
@@ -96,20 +96,20 @@ winreg = "0.10.1"
[build-dependencies] [build-dependencies]
memofs = { version = "0.3.0", path = "crates/memofs" } memofs = { version = "0.3.0", path = "crates/memofs" }
embed-resource = "1.8.0" embed-resource = "1.6.4"
anyhow = "1.0.80" anyhow = "1.0.44"
bincode = "1.3.3" bincode = "1.3.3"
fs-err = "2.11.0" fs-err = "2.6.0"
maplit = "1.0.2" maplit = "1.0.2"
semver = "1.0.22" semver = "1.0.19"
[dev-dependencies] [dev-dependencies]
rojo-insta-ext = { path = "crates/rojo-insta-ext" } rojo-insta-ext = { path = "crates/rojo-insta-ext" }
criterion = "0.3.6" criterion = "0.3.5"
insta = { version = "1.36.1", features = ["redactions", "yaml"] } insta = { version = "1.8.0", features = ["redactions", "yaml"] }
paste = "1.0.14" paste = "1.0.5"
pretty_assertions = "1.4.0" pretty_assertions = "1.2.1"
serde_yaml = "0.8.26" serde_yaml = "0.8.21"
tempfile = "3.10.1" tempfile = "3.2.0"
walkdir = "2.5.0" walkdir = "2.3.2"

View File

@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<a href="https://rojo.space"><img src="assets/brand_images/logo-512.png" alt="Rojo" height="217" /></a> <a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
</div> </div>
<div>&nbsp;</div> <div>&nbsp;</div>
@@ -43,4 +43,4 @@ Pull requests are welcome!
Rojo supports Rust 1.70.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.70.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
## License ## License
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details. Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.

View File

@@ -1,5 +1,5 @@
[tools] [tools]
rojo = "rojo-rbx/rojo@7.4.1" rojo = "rojo-rbx/rojo@7.4.1"
selene = "Kampfkarren/selene@0.27.1" selene = "Kampfkarren/selene@0.26.1"
stylua = "JohnnyMorganz/stylua@0.20.0" stylua = "JohnnyMorganz/stylua@0.18.2"
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"

View File

Before

Width:  |  Height:  |  Size: 975 B

After

Width:  |  Height:  |  Size: 975 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 229 B

After

Width:  |  Height:  |  Size: 229 B

View File

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 584 B

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 B

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

View File

@@ -20,10 +20,6 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
let file_name = entry.file_name().to_str().unwrap().to_owned(); let file_name = entry.file_name().to_str().unwrap().to_owned();
if file_name.starts_with(".git") {
continue;
}
// We can skip any TestEZ test files since they aren't necessary for // We can skip any TestEZ test files since they aren't necessary for
// the plugin to run. // the plugin to run.
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") { if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
@@ -45,12 +41,12 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
fn main() -> Result<(), anyhow::Error> { fn main() -> Result<(), anyhow::Error> {
let out_dir = env::var_os("OUT_DIR").unwrap(); let out_dir = env::var_os("OUT_DIR").unwrap();
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
let plugin_dir = root_dir.join("plugin"); let plugin_root = PathBuf::from(root_dir).join("plugin");
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?; let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
let plugin_version = let plugin_version =
Version::parse(fs::read_to_string(plugin_dir.join("Version.txt"))?.trim())?; Version::parse(fs::read_to_string(plugin_root.join("Version.txt"))?.trim())?;
assert_eq!( assert_eq!(
our_version, plugin_version, our_version, plugin_version,
@@ -58,16 +54,14 @@ fn main() -> Result<(), anyhow::Error> {
); );
let snapshot = VfsSnapshot::dir(hashmap! { let snapshot = VfsSnapshot::dir(hashmap! {
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?, "default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
"plugin" => VfsSnapshot::dir(hashmap! { "fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?, "http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?, "log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?, "rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?, "src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?, "Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?, "Version.txt" => snapshot_from_fs_path(&plugin_root.join("Version.txt"))?,
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
}),
}); });
let out_path = Path::new(&out_dir).join("plugin.bincode"); let out_path = Path::new(&out_dir).join("plugin.bincode");

View File

@@ -11,7 +11,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
crossbeam-channel = "0.5.12" crossbeam-channel = "0.5.1"
fs-err = "2.11.0" fs-err = "2.3.0"
notify = "4.0.17" notify = "4.0.15"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View File

@@ -6,5 +6,5 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
serde = "1.0.197" serde = "1.0.99"
serde_yaml = "0.8.26" serde_yaml = "0.8.9"

View File

@@ -5,13 +5,19 @@ use serde::Serialize;
/// Enables redacting any value that serializes as a string. /// Enables redacting any value that serializes as a string.
/// ///
/// Used for transforming Rojo instance IDs into something deterministic. /// Used for transforming Rojo instance IDs into something deterministic.
#[derive(Default)]
pub struct RedactionMap { pub struct RedactionMap {
ids: HashMap<String, usize>, ids: HashMap<String, usize>,
last_id: usize, last_id: usize,
} }
impl RedactionMap { impl RedactionMap {
pub fn new() -> Self {
Self {
ids: HashMap::new(),
last_id: 0,
}
}
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> { pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
let id = id.to_string(); let id = id.to_string();

View File

@@ -1 +1 @@
7.5.1 7.4.4

View File

@@ -3,25 +3,25 @@
"tree": { "tree": {
"$className": "Folder", "$className": "Folder",
"Plugin": { "Plugin": {
"$path": "plugin/src" "$path": "src"
}, },
"Packages": { "Packages": {
"$path": "plugin/Packages", "$path": "Packages",
"Log": { "Log": {
"$path": "plugin/log" "$path": "log"
}, },
"Http": { "Http": {
"$path": "plugin/http" "$path": "http"
}, },
"Fmt": { "Fmt": {
"$path": "plugin/fmt" "$path": "fmt"
}, },
"RbxDom": { "RbxDom": {
"$path": "plugin/rbx_dom_lua" "$path": "rbx_dom_lua"
} }
}, },
"Version": { "Version": {
"$path": "plugin/Version.txt" "$path": "Version.txt"
} }
} }
} }

View File

@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
elseif valueType == "table" then elseif valueType == "table" then
local valueMeta = getmetatable(value) local valueMeta = getmetatable(value)
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
-- This type implement's the metamethod we made up to line up with -- This type implement's the metamethod we made up to line up with
-- Rust's 'Debug' trait. -- Rust's 'Debug' trait.
@@ -242,4 +242,4 @@ return {
debugOutputBuffer = debugOutputBuffer, debugOutputBuffer = debugOutputBuffer,
fmt = fmt, fmt = fmt,
debugify = debugify, debugify = debugify,
} }

View File

@@ -31,4 +31,4 @@ function Response:json()
return HttpService:JSONDecode(self.body) return HttpService:JSONDecode(self.body)
end end
return Response return Response

View File

@@ -2,4 +2,4 @@ return function()
it("should load", function() it("should load", function()
require(script.Parent) require(script.Parent)
end) end)
end end

View File

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

View File

@@ -2,4 +2,4 @@ return function()
it("should load", function() it("should load", function()
require(script.Parent) require(script.Parent)
end) end)
end end

View File

@@ -188,38 +188,6 @@ types = {
}, },
Content = { Content = {
fromPod = function(pod): Content
if type(pod) == "string" then
if pod == "None" then
return Content.none
else
error(`unexpected Content value '{pod}'`)
end
else
local ty, value = next(pod)
if ty == "Uri" then
return Content.fromUri(value)
elseif ty == "Object" then
error("Object deserializing is not currently implemented")
else
error(`Unknown Content type '{ty}' (could not deserialize)`)
end
end
end,
toPod = function(roblox: Content)
if roblox.SourceType == Enum.ContentSourceType.None then
return "None"
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
return { Uri = roblox.Uri }
elseif roblox.SourceType == Enum.ContentSourceType.Object then
error("Object serializing is not currently implemented")
else
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
end
end,
},
ContentId = {
fromPod = identity, fromPod = identity,
toPod = identity, toPod = identity,
}, },
@@ -237,19 +205,6 @@ types = {
end, end,
}, },
EnumItem = {
fromPod = function(pod)
return Enum[pod.type]:FromValue(pod.value)
end,
toPod = function(roblox)
return {
type = tostring(roblox.EnumType),
value = roblox.Value,
}
end,
},
Faces = { Faces = {
fromPod = function(pod) fromPod = function(pod)
local faces = {} local faces = {}
@@ -345,12 +300,7 @@ types = {
local keypoints = {} local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do for index, keypoint in ipairs(pod.keypoints) do
-- TODO: Add a test for NaN or Infinity values and envelopes keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope)
-- Right now it isn't possible because it'd fail the roundtrip.
-- It's more important that it works right now, though.
local value = keypoint.value or 0
local envelope = keypoint.envelope or 0
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
end end
return NumberSequence.new(keypoints) return NumberSequence.new(keypoints)

View File

@@ -5,7 +5,6 @@ Error.Kind = {
UnknownProperty = "UnknownProperty", UnknownProperty = "UnknownProperty",
PropertyNotReadable = "PropertyNotReadable", PropertyNotReadable = "PropertyNotReadable",
PropertyNotWritable = "PropertyNotWritable", PropertyNotWritable = "PropertyNotWritable",
CannotParseBinaryString = "CannotParseBinaryString",
Roblox = "Roblox", Roblox = "Roblox",
} }

View File

@@ -15,12 +15,6 @@
0.0 0.0
] ]
}, },
"TestEnumItem": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"TestNumber": { "TestNumber": {
"Float64": 1337.0 "Float64": 1337.0
}, },
@@ -176,23 +170,9 @@
}, },
"ty": "ColorSequence" "ty": "ColorSequence"
}, },
"ContentId": { "Content": {
"value": { "value": {
"ContentId": "rbxassetid://12345" "Content": "rbxassetid://12345"
},
"ty": "ContentId"
},
"Content_None": {
"value": {
"Content": "None"
},
"ty": "Content"
},
"Content_Uri": {
"value": {
"Content": {
"Uri": "rbxasset://abc/123.rojo"
}
}, },
"ty": "Content" "ty": "Content"
}, },
@@ -202,15 +182,6 @@
}, },
"ty": "Enum" "ty": "Enum"
}, },
"EnumItem": {
"value": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"ty": "EnumItem"
},
"Faces": { "Faces": {
"value": { "value": {
"Faces": [ "Faces": [

View File

@@ -1,8 +1,6 @@
local CollectionService = game:GetService("CollectionService") local CollectionService = game:GetService("CollectionService")
local ScriptEditorService = game:GetService("ScriptEditorService") local ScriptEditorService = game:GetService("ScriptEditorService")
local Error = require(script.Parent.Error)
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors --- A list of `Enum.Material` values that are used for Terrain.MaterialColors
local TERRAIN_MATERIAL_COLORS = { local TERRAIN_MATERIAL_COLORS = {
Enum.Material.Grass, Enum.Material.Grass,
@@ -53,10 +51,6 @@ return {
return true, instance:GetAttributes() return true, instance:GetAttributes()
end, end,
write = function(instance, _, value) write = function(instance, _, value)
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
local existing = instance:GetAttributes() local existing = instance:GetAttributes()
local didAllWritesSucceed = true local didAllWritesSucceed = true
@@ -166,14 +160,9 @@ return {
return true, colors return true, colors
end, end,
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 }) write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
for material, color in value do for material, color in value do
instance:SetMaterialColor(material, color) instance:SetMaterialColor(material, color)
end end
return true return true
end, end,
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10)) local TestEZ = require(ReplicatedStorage.Packages.TestEZ)
local Rojo = ReplicatedStorage.Rojo local Rojo = ReplicatedStorage.Rojo

View File

@@ -45,7 +45,14 @@ end
local function rejectWrongPlaceId(infoResponseBody) local function rejectWrongPlaceId(infoResponseBody)
if infoResponseBody.expectedPlaceIds ~= nil then if infoResponseBody.expectedPlaceIds ~= nil then
local foundId = table.find(infoResponseBody.expectedPlaceIds, game.PlaceId) local foundId = false
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
if id == game.PlaceId then
foundId = true
break
end
end
if not foundId then if not foundId then
local idList = {} local idList = {}
@@ -55,30 +62,10 @@ local function rejectWrongPlaceId(infoResponseBody)
local message = ( local message = (
"Found a Rojo server, but its project is set to only be used with a specific list of places." "Found a Rojo server, but its project is set to only be used with a specific list of places."
.. "\nYour place ID is %u, but needs to be one of these:" .. "\nYour place ID is %s, but needs to be one of these:"
.. "\n%s" .. "\n%s"
.. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file." .. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
):format(game.PlaceId, table.concat(idList, "\n")) ):format(tostring(game.PlaceId), table.concat(idList, "\n"))
return Promise.reject(message)
end
end
if infoResponseBody.unexpectedPlaceIds ~= nil then
local foundId = table.find(infoResponseBody.unexpectedPlaceIds, game.PlaceId)
if foundId then
local idList = {}
for _, id in ipairs(infoResponseBody.unexpectedPlaceIds) do
table.insert(idList, "- " .. tostring(id))
end
local message = (
"Found a Rojo server, but its project is set to not be used with a specific list of places."
.. "\nYour place ID is %u, but needs to not be one of these:"
.. "\n%s"
.. "\n\nTo change this list, edit 'blockedPlaceIds' in your .project.json file."
):format(game.PlaceId, table.concat(idList, "\n"))
return Promise.reject(message) return Promise.reject(message)
end end

View File

@@ -32,7 +32,7 @@ end
function Checkbox:render() function Checkbox:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local checkboxTheme = theme.Checkbox theme = theme.Checkbox
local activeTransparency = Roact.joinBindings({ local activeTransparency = Roact.joinBindings({
self.binding:map(function(value) self.binding:map(function(value)
@@ -57,21 +57,20 @@ function Checkbox:render()
end, end,
}, { }, {
StateTip = e(Tooltip.Trigger, { StateTip = e(Tooltip.Trigger, {
text = (if self.props.locked text = (if self.props.locked then "[LOCKED] " else "")
then (self.props.lockedTooltip or "(Cannot be changed right now)") .. "\n" .. (if self.props.active then "Enabled" else "Disabled"),
else "") .. (if self.props.active then "Enabled" else "Disabled"),
}), }),
Active = e(SlicedImage, { Active = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = checkboxTheme.Active.BackgroundColor, color = theme.Active.BackgroundColor,
transparency = activeTransparency, transparency = activeTransparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
zIndex = 2, zIndex = 2,
}, { }, {
Icon = e("ImageLabel", { Icon = e("ImageLabel", {
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active, Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
ImageColor3 = checkboxTheme.Active.IconColor, ImageColor3 = theme.Active.IconColor,
ImageTransparency = activeTransparency, ImageTransparency = activeTransparency,
Size = UDim2.new(0, 16, 0, 16), Size = UDim2.new(0, 16, 0, 16),
@@ -84,7 +83,7 @@ function Checkbox:render()
Inactive = e(SlicedImage, { Inactive = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = checkboxTheme.Inactive.BorderColor, color = theme.Inactive.BorderColor,
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
}, { }, {
@@ -92,7 +91,7 @@ function Checkbox:render()
Image = if self.props.locked Image = if self.props.locked
then Assets.Images.Checkbox.Locked then Assets.Images.Checkbox.Locked
else Assets.Images.Checkbox.Inactive, else Assets.Images.Checkbox.Inactive,
ImageColor3 = checkboxTheme.Inactive.IconColor, ImageColor3 = theme.Inactive.IconColor,
ImageTransparency = self.props.transparency, ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 16, 0, 16), Size = UDim2.new(0, 16, 0, 16),

View File

@@ -1,126 +0,0 @@
local StudioService = game:GetService("StudioService")
local AssetService = game:GetService("AssetService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local e = Roact.createElement
local EditableImage = require(Plugin.App.Components.EditableImage)
local imageCache = {}
local function getImageSizeAndPixels(image)
if not imageCache[image] then
local editableImage = AssetService:CreateEditableImageAsync(image)
imageCache[image] = {
Size = editableImage.Size,
Pixels = editableImage:ReadPixels(Vector2.zero, editableImage.Size),
}
end
return imageCache[image].Size, table.clone(imageCache[image].Pixels)
end
local function getRecoloredClassIcon(className, color)
local iconProps = StudioService:GetClassIcon(className)
if iconProps and color then
local success, editableImageSize, editableImagePixels = pcall(function()
local size, pixels = getImageSizeAndPixels(iconProps.Image)
local minVal, maxVal = math.huge, -math.huge
for i = 1, #pixels, 4 do
if pixels[i + 3] == 0 then
continue
end
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
minVal = math.min(minVal, pixelVal)
maxVal = math.max(maxVal, pixelVal)
end
local hue, sat, val = color:ToHSV()
for i = 1, #pixels, 4 do
if pixels[i + 3] == 0 then
continue
end
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
local newVal = val
if minVal < maxVal then
-- Remap minVal - maxVal to val*0.9 - val
newVal = val * (0.9 + 0.1 * (pixelVal - minVal) / (maxVal - minVal))
end
local newPixelColor = Color3.fromHSV(hue, sat, newVal)
pixels[i], pixels[i + 1], pixels[i + 2] = newPixelColor.R, newPixelColor.G, newPixelColor.B
end
return size, pixels
end)
if success then
iconProps.EditableImagePixels = editableImagePixels
iconProps.EditableImageSize = editableImageSize
end
end
return iconProps
end
local ClassIcon = Roact.PureComponent:extend("ClassIcon")
function ClassIcon:init()
self.state = {
iconProps = nil,
}
end
function ClassIcon:updateIcon()
local props = self.props
local iconProps = getRecoloredClassIcon(props.className, props.color)
self:setState({
iconProps = iconProps,
})
end
function ClassIcon:didMount()
self:updateIcon()
end
function ClassIcon:didUpdate(lastProps)
if lastProps.className ~= self.props.className or lastProps.color ~= self.props.color then
self:updateIcon()
end
end
function ClassIcon:render()
local iconProps = self.state.iconProps
if not iconProps then
return nil
end
return e(
"ImageLabel",
{
Size = self.props.size,
Position = self.props.position,
LayoutOrder = self.props.layoutOrder,
AnchorPoint = self.props.anchorPoint,
ImageTransparency = self.props.transparency,
Image = iconProps.Image,
ImageRectOffset = iconProps.ImageRectOffset,
ImageRectSize = iconProps.ImageRectSize,
BackgroundTransparency = 1,
},
if iconProps.EditableImagePixels
then e(EditableImage, {
size = iconProps.EditableImageSize,
pixels = iconProps.EditableImagePixels,
})
else nil
)
end
return ClassIcon

View File

@@ -1,5 +1,4 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
@@ -8,8 +7,6 @@ Highlighter.matchStudioSettings()
local e = Roact.createElement local e = Roact.createElement
local Theme = require(Plugin.App.Theme)
local CodeLabel = Roact.PureComponent:extend("CodeLabel") local CodeLabel = Roact.PureComponent:extend("CodeLabel")
function CodeLabel:init() function CodeLabel:init()
@@ -43,24 +40,22 @@ function CodeLabel:updateHighlights()
end end
function CodeLabel:render() function CodeLabel:render()
return Theme.with(function(theme) return e("TextLabel", {
return e("TextLabel", { Size = self.props.size,
Size = self.props.size, Position = self.props.position,
Position = self.props.position, Text = self.props.text,
Text = self.props.text, BackgroundTransparency = 1,
BackgroundTransparency = 1, Font = Enum.Font.RobotoMono,
FontFace = theme.Font.Code, TextSize = 16,
TextSize = theme.TextSize.Code, TextXAlignment = Enum.TextXAlignment.Left,
TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top,
TextYAlignment = Enum.TextYAlignment.Top, TextColor3 = Color3.fromRGB(255, 255, 255),
TextColor3 = Color3.fromRGB(255, 255, 255), [Roact.Ref] = self.labelRef,
[Roact.Ref] = self.labelRef, }, {
}, { SyntaxHighlights = e("Folder", {
SyntaxHighlights = e("Folder", { [Roact.Ref] = self.highlightsRef,
[Roact.Ref] = self.highlightsRef, }),
}), })
})
end)
end end
return CodeLabel return CodeLabel

View File

@@ -1,3 +1,5 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages local Packages = Rojo.Packages
@@ -8,11 +10,9 @@ local Flipper = require(Packages.Flipper)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local bindingUtil = require(Plugin.App.bindingUtil) local bindingUtil = require(Plugin.App.bindingUtil)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local SlicedImage = require(script.Parent.SlicedImage) local SlicedImage = require(script.Parent.SlicedImage)
local ScrollingFrame = require(script.Parent.ScrollingFrame) local ScrollingFrame = require(script.Parent.ScrollingFrame)
local Tooltip = require(script.Parent.Tooltip)
local e = Roact.createElement local e = Roact.createElement
@@ -44,29 +44,29 @@ end
function Dropdown:render() function Dropdown:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local dropdownTheme = theme.Dropdown theme = theme.Dropdown
local optionButtons = {} local optionButtons = {}
local width = -1 local width = -1
for i, option in self.props.options do for i, option in self.props.options do
local text = tostring(option or "") local text = tostring(option or "")
local textBounds = getTextBoundsAsync(text, theme.Font.Main, theme.TextSize.Body, math.huge) local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
if textBounds.X > width then if textSize.X > width then
width = textBounds.X width = textSize.X
end end
optionButtons[text] = e("TextButton", { optionButtons[text] = e("TextButton", {
Text = text, Text = text,
LayoutOrder = i, LayoutOrder = i,
Size = UDim2.new(1, 0, 0, 24), Size = UDim2.new(1, 0, 0, 24),
BackgroundColor3 = dropdownTheme.BackgroundColor, BackgroundColor3 = theme.BackgroundColor,
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
BackgroundTransparency = self.props.transparency, BackgroundTransparency = self.props.transparency,
BorderSizePixel = 0, BorderSizePixel = 0,
TextColor3 = dropdownTheme.TextColor, TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextSize = theme.TextSize.Body, TextSize = 15,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
[Roact.Event.Activated] = function() [Roact.Event.Activated] = function()
if self.props.locked then if self.props.locked then
@@ -103,13 +103,15 @@ function Dropdown:render()
}, { }, {
Border = e(SlicedImage, { Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = dropdownTheme.BorderColor, color = theme.BorderColor,
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
}, { }, {
DropArrow = e("ImageLabel", { DropArrow = e("ImageLabel", {
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow, Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
ImageColor3 = dropdownTheme.IconColor, ImageColor3 = self.openBinding:map(function(a)
return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a)
end),
ImageTransparency = self.props.transparency, ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 18, 0, 18), Size = UDim2.new(0, 18, 0, 18),
@@ -120,21 +122,15 @@ function Dropdown:render()
end), end),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, {
StateTip = if self.props.locked
then e(Tooltip.Trigger, {
text = self.props.lockedTooltip or "(Cannot be changed right now)",
})
else nil,
}), }),
Active = e("TextLabel", { Active = e("TextLabel", {
Size = UDim2.new(1, -30, 1, 0), Size = UDim2.new(1, -30, 1, 0),
Position = UDim2.new(0, 6, 0, 0), Position = UDim2.new(0, 6, 0, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1,
Text = self.props.active, Text = self.props.active,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 15,
TextColor3 = dropdownTheme.TextColor, TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
}), }),
@@ -142,7 +138,7 @@ function Dropdown:render()
Options = if self.state.open Options = if self.state.open
then e(SlicedImage, { then e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = dropdownTheme.BackgroundColor, color = theme.BackgroundColor,
position = UDim2.new(1, 0, 1, 3), position = UDim2.new(1, 0, 1, 3),
size = self.openBinding:map(function(a) size = self.openBinding:map(function(a)
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0) return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
@@ -151,7 +147,7 @@ function Dropdown:render()
}, { }, {
Border = e(SlicedImage, { Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = dropdownTheme.BorderColor, color = theme.BorderColor,
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
}), }),

View File

@@ -1,41 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local e = Roact.createElement
local EditableImage = Roact.PureComponent:extend("EditableImage")
function EditableImage:init()
self.ref = Roact.createRef()
end
function EditableImage:writePixels()
local image = self.ref.current
if not image then
return
end
if not self.props.pixels then
return
end
image:WritePixels(Vector2.zero, self.props.size, self.props.pixels)
end
function EditableImage:render()
return e("EditableImage", {
Size = self.props.size,
[Roact.Ref] = self.ref,
})
end
function EditableImage:didMount()
self:writePixels()
end
function EditableImage:didUpdate()
self:writePixels()
end
return EditableImage

View File

@@ -31,13 +31,13 @@ local function Header(props)
Version = e("TextLabel", { Version = e("TextLabel", {
Text = Version.display(Config.version), Text = Version.display(Config.version),
FontFace = theme.Font.Thin, Font = Enum.Font.Gotham,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = theme.Header.VersionColor, TextColor3 = theme.Header.VersionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
Size = UDim2.new(1, 0, 0, theme.TextSize.Body), Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = 2, LayoutOrder = 2,
BackgroundTransparency = 1, BackgroundTransparency = 1,

View File

@@ -14,123 +14,6 @@ local EMPTY_TABLE = {}
local e = Roact.createElement local e = Roact.createElement
local function ViewDiffButton(props)
return Theme.with(function(theme)
return e("TextButton", {
Text = "",
Size = UDim2.new(0.7, 0, 1, -4),
LayoutOrder = 2,
BackgroundTransparency = 1,
[Roact.Event.Activated] = props.onClick,
}, {
e(BorderedContainer, {
size = UDim2.new(1, 0, 1, 0),
transparency = props.transparency:map(function(t)
return 0.5 + (0.5 * t)
end),
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 5),
}),
Label = e("TextLabel", {
Text = "View Diff",
BackgroundTransparency = 1,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0, 65, 1, 0),
LayoutOrder = 1,
}),
Icon = e("ImageLabel", {
Image = Assets.Images.Icons.Expand,
ImageColor3 = theme.Settings.Setting.DescriptionColor,
ImageTransparency = 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,
LayoutOrder = 2,
}),
}),
})
end)
end
local function RowContent(props)
local values = props.values
local metadata = props.metadata
if props.showStringDiff and values[1] == "Source" then
-- Special case for .Source updates
return e(ViewDiffButton, {
transparency = props.transparency,
onClick = function()
if not props.showStringDiff then
return
end
props.showStringDiff(tostring(values[2]), tostring(values[3]))
end,
})
end
if props.showTableDiff and (type(values[2]) == "table" or type(values[3]) == "table") then
-- Special case for table properties (like Attributes/Tags)
return e(ViewDiffButton, {
transparency = props.transparency,
onClick = function()
if not props.showTableDiff then
return
end
props.showTableDiff(values[2], values[3])
end,
})
end
return Theme.with(function(theme)
return Roact.createFragment({
ColumnB = e(
"Frame",
{
BackgroundTransparency = 1,
Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 2,
},
e(DisplayValue, {
value = values[2],
transparency = props.transparency,
textColor = if metadata.isWarning
then theme.Diff.Warning
else theme.Settings.Setting.DescriptionColor,
})
),
ColumnC = e(
"Frame",
{
BackgroundTransparency = 1,
Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 3,
},
e(DisplayValue, {
value = values[3],
transparency = props.transparency,
textColor = if metadata.isWarning
then theme.Diff.Warning
else theme.Settings.Setting.DescriptionColor,
})
),
})
end)
end
local ChangeList = Roact.Component:extend("ChangeList") local ChangeList = Roact.Component:extend("ChangeList")
function ChangeList:init() function ChangeList:init()
@@ -153,9 +36,8 @@ function ChangeList:render()
PaddingRight = UDim.new(0, 5), PaddingRight = UDim.new(0, 5),
} }
local headerRow = changes[1]
local headers = e("Frame", { local headers = e("Frame", {
Size = UDim2.new(1, 0, 0, 24), Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = rowTransparency, BackgroundTransparency = rowTransparency,
BackgroundColor3 = theme.Diff.Row, BackgroundColor3 = theme.Diff.Row,
LayoutOrder = 0, LayoutOrder = 0,
@@ -167,36 +49,36 @@ function ChangeList:render()
HorizontalAlignment = Enum.HorizontalAlignment.Left, HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center, VerticalAlignment = Enum.VerticalAlignment.Center,
}), }),
ColumnA = e("TextLabel", { A = e("TextLabel", {
Text = tostring(headerRow[1]), Text = tostring(changes[1][1]),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Bold, Font = Enum.Font.GothamBold,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = theme.TextColor, TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.3, 0, 1, 0), Size = UDim2.new(0.3, 0, 1, 0),
LayoutOrder = 1, LayoutOrder = 1,
}), }),
ColumnB = e("TextLabel", { B = e("TextLabel", {
Text = tostring(headerRow[2]), Text = tostring(changes[1][2]),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Bold, Font = Enum.Font.GothamBold,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = theme.TextColor, TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.35, 0, 1, 0), Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 2, LayoutOrder = 2,
}), }),
ColumnC = e("TextLabel", { C = e("TextLabel", {
Text = tostring(headerRow[3]), Text = tostring(changes[1][3]),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Bold, Font = Enum.Font.GothamBold,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = theme.TextColor, TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
@@ -213,8 +95,91 @@ function ChangeList:render()
local metadata = values[4] or EMPTY_TABLE local metadata = values[4] or EMPTY_TABLE
local isWarning = metadata.isWarning local isWarning = metadata.isWarning
-- Special case for .Source updates
-- because we want to display a syntax highlighted diff for better UX
if self.props.showSourceDiff and tostring(values[1]) == "Source" then
rows[row] = e("Frame", {
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
BackgroundColor3 = theme.Diff.Row,
BorderSizePixel = 0,
LayoutOrder = row,
}, {
Padding = e("UIPadding", pad),
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
A = e("TextLabel", {
Text = (if isWarning then "" else "") .. tostring(values[1]),
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.3, 0, 1, 0),
LayoutOrder = 1,
}),
Button = e("TextButton", {
Text = "",
Size = UDim2.new(0.7, 0, 1, -4),
LayoutOrder = 2,
BackgroundTransparency = 1,
[Roact.Event.Activated] = function()
if props.showSourceDiff then
props.showSourceDiff(tostring(values[2]), tostring(values[3]))
end
end,
}, {
e(BorderedContainer, {
size = UDim2.new(1, 0, 1, 0),
transparency = self.props.transparency:map(function(t)
return 0.5 + (0.5 * t)
end),
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 5),
}),
Label = e("TextLabel", {
Text = "View Diff",
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0, 65, 1, 0),
LayoutOrder = 1,
}),
Icon = e("ImageLabel", {
Image = Assets.Images.Icons.Expand,
ImageColor3 = theme.Settings.Setting.DescriptionColor,
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,
LayoutOrder = 2,
}),
}),
}),
})
continue
end
rows[row] = e("Frame", { rows[row] = e("Frame", {
Size = UDim2.new(1, 0, 0, 24), Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1, BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
BackgroundColor3 = theme.Diff.Row, BackgroundColor3 = theme.Diff.Row,
BorderSizePixel = 0, BorderSizePixel = 0,
@@ -227,25 +192,44 @@ function ChangeList:render()
HorizontalAlignment = Enum.HorizontalAlignment.Left, HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center, VerticalAlignment = Enum.VerticalAlignment.Center,
}), }),
ColumnA = e("TextLabel", { A = e("TextLabel", {
Text = (if isWarning then "" else "") .. tostring(values[1]), Text = (if isWarning then "" else "") .. tostring(values[1]),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor, TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.3, 0, 1, 0), Size = UDim2.new(0.3, 0, 1, 0),
LayoutOrder = 1, LayoutOrder = 1,
}), }),
Content = e(RowContent, { B = e(
values = values, "Frame",
metadata = metadata, {
transparency = props.transparency, BackgroundTransparency = 1,
showStringDiff = props.showStringDiff, Size = UDim2.new(0.35, 0, 1, 0),
showTableDiff = props.showTableDiff, LayoutOrder = 2,
}), },
e(DisplayValue, {
value = values[2],
transparency = props.transparency,
textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
})
),
C = e(
"Frame",
{
BackgroundTransparency = 1,
Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 3,
},
e(DisplayValue, {
value = values[3],
transparency = props.transparency,
textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
})
),
}) })
end end
@@ -269,8 +253,8 @@ function ChangeList:render()
}, { }, {
Headers = headers, Headers = headers,
Values = e(ScrollingFrame, { Values = e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, -24), size = UDim2.new(1, 0, 1, -30),
position = UDim2.new(0, 0, 0, 24), position = UDim2.new(0, 0, 0, 30),
contentSize = self.contentSize, contentSize = self.contentSize,
transparency = props.transparency, transparency = props.transparency,
}, rows), }, rows),

View File

@@ -30,10 +30,10 @@ local function DisplayValue(props)
}), }),
}), }),
Label = e("TextLabel", { Label = e("TextLabel", {
Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255), Text = string.format("%d,%d,%d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = props.textColor, TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
@@ -90,8 +90,8 @@ local function DisplayValue(props)
return e("TextLabel", { return e("TextLabel", {
Text = textRepresentation, Text = textRepresentation,
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = props.textColor, TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
@@ -104,16 +104,11 @@ local function DisplayValue(props)
-- Or special text handling tostring for some? -- Or special text handling tostring for some?
-- Will add as needed, let's see what cases arise. -- Will add as needed, let's see what cases arise.
local textRepresentation = string.gsub(tostring(props.value), "%s", " ")
if t == "string" then
textRepresentation = '"' .. textRepresentation .. '"'
end
return e("TextLabel", { return e("TextLabel", {
Text = textRepresentation, Text = string.gsub(tostring(props.value), "%s", " "),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = props.textColor, TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,

View File

@@ -1,4 +1,5 @@
local SelectionService = game:GetService("Selection") local SelectionService = game:GetService("Selection")
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
@@ -14,8 +15,7 @@ local bindingUtil = require(Plugin.App.bindingUtil)
local e = Roact.createElement local e = Roact.createElement
local ChangeList = require(script.Parent.ChangeList) local ChangeList = require(script.Parent.ChangeList)
local Tooltip = require(Plugin.App.Components.Tooltip) local Tooltip = require(script.Parent.Parent.Tooltip)
local ClassIcon = require(Plugin.App.Components.ClassIcon)
local Expansion = Roact.Component:extend("Expansion") local Expansion = Roact.Component:extend("Expansion")
@@ -28,14 +28,13 @@ function Expansion:render()
return e("Frame", { return e("Frame", {
BackgroundTransparency = 1, BackgroundTransparency = 1,
Size = UDim2.new(1, -props.indent, 1, -24), Size = UDim2.new(1, -props.indent, 1, -30),
Position = UDim2.new(0, props.indent, 0, 24), Position = UDim2.new(0, props.indent, 0, 30),
}, { }, {
ChangeList = e(ChangeList, { ChangeList = e(ChangeList, {
changes = props.changeList, changes = props.changeList,
transparency = props.transparency, transparency = props.transparency,
showStringDiff = props.showStringDiff, showSourceDiff = props.showSourceDiff,
showTableDiff = props.showTableDiff,
}), }),
}) })
end end
@@ -44,7 +43,7 @@ local DomLabel = Roact.Component:extend("DomLabel")
function DomLabel:init() function DomLabel:init()
local initHeight = self.props.elementHeight:getValue() local initHeight = self.props.elementHeight:getValue()
self.expanded = initHeight > 24 self.expanded = initHeight > 30
self.motor = Flipper.SingleMotor.new(initHeight) self.motor = Flipper.SingleMotor.new(initHeight)
self.binding = bindingUtil.fromMotor(self.motor) self.binding = bindingUtil.fromMotor(self.motor)
@@ -53,7 +52,7 @@ function DomLabel:init()
renderExpansion = self.expanded, renderExpansion = self.expanded,
}) })
self.motor:onStep(function(value) self.motor:onStep(function(value)
local renderExpansion = value > 24 local renderExpansion = value > 30
self.props.setElementHeight(value) self.props.setElementHeight(value)
if self.props.updateEvent then if self.props.updateEvent then
@@ -81,7 +80,7 @@ function DomLabel:didUpdate(prevProps)
then then
-- Close the expansion when the domlabel is changed to a different thing -- Close the expansion when the domlabel is changed to a different thing
self.expanded = false self.expanded = false
self.motor:setGoal(Flipper.Spring.new(24, { self.motor:setGoal(Flipper.Spring.new(30, {
frequency = 5, frequency = 5,
dampingRatio = 1, dampingRatio = 1,
})) }))
@@ -90,49 +89,17 @@ end
function DomLabel:render() function DomLabel:render()
local props = self.props local props = self.props
local depth = props.depth or 1
return Theme.with(function(theme) return Theme.with(function(theme)
local color = if props.isWarning local iconProps = StudioService:GetClassIcon(props.className)
then theme.Diff.Warning local indent = (props.depth or 0) * 20 + 25
elseif props.patchType then theme.Diff[props.patchType]
else theme.TextColor
local indent = (depth - 1) * 12 + 15
-- Line guides help indent depth remain readable -- Line guides help indent depth remain readable
local lineGuides = {} local lineGuides = {}
for i = 2, depth do for i = 1, props.depth or 0 do
if props.depthsComplete[i] then lineGuides["Line_" .. i] = e("Frame", {
continue Size = UDim2.new(0, 2, 1, 2),
end Position = UDim2.new(0, (20 * i) + 15, 0, -1),
if props.isFinalChild and i == depth then
-- This line stops halfway down to merge with our connector for the right angle
lineGuides["Line_" .. i] = e("Frame", {
Size = UDim2.new(0, 2, 0, 15),
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
BorderSizePixel = 0,
BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
})
else
-- All other lines go all the way
-- with the exception of the final element, which stops halfway down
lineGuides["Line_" .. i] = e("Frame", {
Size = UDim2.new(0, 2, 1, if props.isFinalElement then -9 else 2),
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
BorderSizePixel = 0,
BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
})
end
end
if depth ~= 1 then
lineGuides["Connector"] = e("Frame", {
Size = UDim2.new(0, 8, 0, 2),
Position = UDim2.new(0, 2 + (12 * props.depth), 0, 12),
AnchorPoint = Vector2.xAxis,
BorderSizePixel = 0, BorderSizePixel = 0,
BackgroundTransparency = props.transparency, BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor, BackgroundColor3 = theme.BorderedContainer.BorderColor,
@@ -141,8 +108,9 @@ function DomLabel:render()
return e("Frame", { return e("Frame", {
ClipsDescendants = true, ClipsDescendants = true,
BackgroundTransparency = if props.elementIndex % 2 == 0 then 0.985 else 1, BackgroundColor3 = if props.patchType then theme.Diff[props.patchType] else nil,
BackgroundColor3 = theme.Diff.Row, BorderSizePixel = 0,
BackgroundTransparency = props.patchType and props.transparency or 1,
Size = self.binding:map(function(expand) Size = self.binding:map(function(expand)
return UDim2.new(1, 0, 0, expand) return UDim2.new(1, 0, 0, expand)
end), end),
@@ -172,8 +140,8 @@ function DomLabel:render()
if props.changeList then if props.changeList then
self.expanded = not self.expanded self.expanded = not self.expanded
local goalHeight = 24 local goalHeight = 30
+ (if self.expanded then math.clamp(#props.changeList * 24, 24, 24 * 6) else 0) + (if self.expanded then math.clamp(#props.changeList * 30, 30, 30 * 6) else 0)
self.motor:setGoal(Flipper.Spring.new(goalHeight, { self.motor:setGoal(Flipper.Spring.new(goalHeight, {
frequency = 5, frequency = 5,
dampingRatio = 1, dampingRatio = 1,
@@ -198,81 +166,46 @@ function DomLabel:render()
indent = indent, indent = indent,
transparency = props.transparency, transparency = props.transparency,
changeList = props.changeList, changeList = props.changeList,
showStringDiff = props.showStringDiff, showSourceDiff = props.showSourceDiff,
showTableDiff = props.showTableDiff,
}) })
else nil, else nil,
DiffIcon = if props.patchType DiffIcon = if props.patchType
then e("ImageLabel", { then e("ImageLabel", {
Image = Assets.Images.Diff[props.patchType], Image = Assets.Images.Diff[props.patchType],
ImageColor3 = color, ImageColor3 = theme.AddressEntry.PlaceholderColor,
ImageTransparency = props.transparency, ImageTransparency = props.transparency,
BackgroundTransparency = 1, BackgroundTransparency = 1,
Size = UDim2.new(0, 14, 0, 14), Size = UDim2.new(0, 20, 0, 20),
Position = UDim2.new(0, 0, 0, 12), Position = UDim2.new(0, 0, 0, 15),
AnchorPoint = Vector2.new(0, 0.5), AnchorPoint = Vector2.new(0, 0.5),
}) })
else nil, else nil,
ClassIcon = e(ClassIcon, { ClassIcon = e("ImageLabel", {
className = props.className, Image = iconProps.Image,
color = color, ImageTransparency = props.transparency,
transparency = props.transparency, ImageRectOffset = iconProps.ImageRectOffset,
size = UDim2.new(0, 16, 0, 16), ImageRectSize = iconProps.ImageRectSize,
position = UDim2.new(0, indent + 2, 0, 12), BackgroundTransparency = 1,
anchorPoint = Vector2.new(0, 0.5), Size = UDim2.new(0, 20, 0, 20),
Position = UDim2.new(0, indent, 0, 15),
AnchorPoint = Vector2.new(0, 0.5),
}), }),
InstanceName = e("TextLabel", { InstanceName = e("TextLabel", {
Text = (if props.isWarning then "" else "") .. props.name, Text = (if props.isWarning then "" else "") .. props.name .. (props.hint and string.format(
' <font color="#%s">%s</font>',
theme.AddressEntry.PlaceholderColor:ToHex(),
props.hint
) or ""),
RichText = true, RichText = true,
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = if props.patchType then theme.Font.Bold else theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = color, TextColor3 = if props.isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(1, -indent - 50, 0, 24), Size = UDim2.new(1, -indent - 50, 0, 30),
Position = UDim2.new(0, indent + 22, 0, 0), Position = UDim2.new(0, indent + 30, 0, 0),
}),
ChangeInfo = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -indent - 80, 0, 24),
Position = UDim2.new(1, -2, 0, 0),
AnchorPoint = Vector2.new(1, 0),
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 4),
}),
Edits = if props.changeInfo and props.changeInfo.edits
then e("TextLabel", {
Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
BackgroundTransparency = 1,
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Body,
TextColor3 = theme.SubTextColor,
TextTransparency = props.transparency,
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 2,
})
else nil,
Failed = if props.changeInfo and props.changeInfo.failed
then e("TextLabel", {
Text = props.changeInfo.failed,
BackgroundTransparency = 1,
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Diff.Warning,
TextTransparency = props.transparency,
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 6,
})
else nil,
}), }),
LineGuides = e("Folder", nil, lineGuides), LineGuides = e("Folder", nil, lineGuides),
}) })

View File

@@ -8,8 +8,8 @@ local PatchTree = require(Plugin.PatchTree)
local PatchSet = require(Plugin.PatchSet) local PatchSet = require(Plugin.PatchSet)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
local e = Roact.createElement local e = Roact.createElement
@@ -55,60 +55,33 @@ function PatchVisualizer:render()
end end
-- Recusively draw tree -- Recusively draw tree
local scrollElements, elementHeights, elementIndex = {}, {}, 0 local scrollElements, elementHeights = {}, {}
if patchTree then if patchTree then
local elementTotal = patchTree:getCount()
local depthsComplete = {}
local function drawNode(node, depth) local function drawNode(node, depth)
elementIndex += 1 local elementHeight, setElementHeight = Roact.createBinding(30)
table.insert(elementHeights, elementHeight)
local parentNode = patchTree:getNode(node.parentId) table.insert(
local isFinalChild = true scrollElements,
if parentNode then e(DomLabel, {
for _id, sibling in parentNode.children do updateEvent = self.updateEvent,
if type(sibling) == "table" and sibling.name and sibling.name > node.name then elementHeight = elementHeight,
isFinalChild = false setElementHeight = setElementHeight,
break patchType = node.patchType,
end className = node.className,
end isWarning = node.isWarning,
end instance = node.instance,
name = node.name,
local elementHeight, setElementHeight = Roact.createBinding(24) hint = node.hint,
elementHeights[elementIndex] = elementHeight changeList = node.changeList,
scrollElements[elementIndex] = e(DomLabel, { depth = depth,
transparency = self.props.transparency, transparency = self.props.transparency,
showStringDiff = self.props.showStringDiff, showSourceDiff = self.props.showSourceDiff,
showTableDiff = self.props.showTableDiff, })
updateEvent = self.updateEvent, )
elementHeight = elementHeight,
setElementHeight = setElementHeight,
elementIndex = elementIndex,
isFinalElement = elementIndex == elementTotal,
depth = depth,
depthsComplete = table.clone(depthsComplete),
hasChildren = (node.children ~= nil and next(node.children) ~= nil),
isFinalChild = isFinalChild,
patchType = node.patchType,
className = node.className,
isWarning = node.isWarning,
instance = node.instance,
name = node.name,
changeInfo = node.changeInfo,
changeList = node.changeList,
})
if isFinalChild then
depthsComplete[depth] = true
end
end end
patchTree:forEach(function(node, depth) patchTree:forEach(function(node, depth)
depthsComplete[depth] = false
for i = depth + 1, #depthsComplete do
depthsComplete[i] = nil
end
drawNode(node, depth) drawNode(node, depth)
end) end)
end end
@@ -118,23 +91,21 @@ function PatchVisualizer:render()
transparency = self.props.transparency, transparency = self.props.transparency,
size = self.props.size, size = self.props.size,
position = self.props.position, position = self.props.position,
anchorPoint = self.props.anchorPoint,
layoutOrder = self.props.layoutOrder, layoutOrder = self.props.layoutOrder,
}, { }, {
CleanMerge = e("TextLabel", { CleanMerge = e("TextLabel", {
Visible = #scrollElements == 0, Visible = #scrollElements == 0,
Text = "No changes to sync, project is up to date.", Text = "No changes to sync, project is up to date.",
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Medium, TextSize = 15,
TextColor3 = theme.TextColor, TextColor3 = theme.Settings.Setting.DescriptionColor,
TextWrapped = true, TextWrapped = true,
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
VirtualScroller = e(VirtualScroller, { VirtualScroller = e(VirtualScroller, {
size = UDim2.new(1, 0, 1, -2), size = UDim2.new(1, 0, 1, 0),
position = UDim2.new(0, 0, 0, 2),
transparency = self.props.transparency, transparency = self.props.transparency,
count = #scrollElements, count = #scrollElements,
updateEvent = self.updateEvent.Event, updateEvent = self.updateEvent.Event,

View File

@@ -10,12 +10,6 @@ local bindingUtil = require(Plugin.App.bindingUtil)
local e = Roact.createElement local e = Roact.createElement
local scrollDirToAutoSize = {
[Enum.ScrollingDirection.X] = Enum.AutomaticSize.X,
[Enum.ScrollingDirection.Y] = Enum.AutomaticSize.Y,
[Enum.ScrollingDirection.XY] = Enum.AutomaticSize.XY,
}
local function ScrollingFrame(props) local function ScrollingFrame(props)
return Theme.with(function(theme) return Theme.with(function(theme)
return e("ScrollingFrame", { return e("ScrollingFrame", {
@@ -34,21 +28,16 @@ local function ScrollingFrame(props)
Size = props.size, Size = props.size,
Position = props.position, Position = props.position,
AnchorPoint = props.anchorPoint, AnchorPoint = props.anchorPoint,
CanvasSize = if props.contentSize CanvasSize = props.contentSize:map(function(value)
then props.contentSize:map(function(value) return UDim2.new(
return UDim2.new( 0,
0, if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y)
if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y) then value.X
then value.X else 0,
else 0, 0,
0, value.Y
value.Y )
) end),
end)
else UDim2.new(),
AutomaticCanvasSize = if props.contentSize == nil
then scrollDirToAutoSize[props.scrollingDirection or Enum.ScrollingDirection.XY]
else nil,
BorderSizePixel = 0, BorderSizePixel = 0,
BackgroundTransparency = 1, BackgroundTransparency = 1,

View File

@@ -20,7 +20,6 @@ local function SlicedImage(props)
Size = props.size, Size = props.size,
Position = props.position, Position = props.position,
AnchorPoint = props.anchorPoint, AnchorPoint = props.anchorPoint,
AutomaticSize = props.automaticSize,
ZIndex = props.zIndex, ZIndex = props.zIndex,
LayoutOrder = props.layoutOrder, LayoutOrder = props.layoutOrder,

View File

@@ -1,3 +1,5 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages local Packages = Rojo.Packages
@@ -7,9 +9,7 @@ local Log = require(Packages.Log)
local Highlighter = require(Packages.Highlighter) local Highlighter = require(Packages.Highlighter)
local StringDiff = require(script:FindFirstChild("StringDiff")) local StringDiff = require(script:FindFirstChild("StringDiff"))
local Timer = require(Plugin.Timer)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local CodeLabel = require(Plugin.App.Components.CodeLabel) local CodeLabel = require(Plugin.App.Components.CodeLabel)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
@@ -31,6 +31,7 @@ function StringDiffVisualizer:init()
end) end)
end) end)
self:calculateContentSize()
self:updateScriptBackground() self:updateScriptBackground()
self:setState({ self:setState({
@@ -51,7 +52,8 @@ function StringDiffVisualizer:updateScriptBackground()
end end
function StringDiffVisualizer:didUpdate(previousProps) function StringDiffVisualizer:didUpdate(previousProps)
if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then if previousProps.oldText ~= self.props.oldText or previousProps.newText ~= self.props.newText then
self:calculateContentSize()
local add, remove = self:calculateDiffLines() local add, remove = self:calculateDiffLines()
self:setState({ self:setState({
add = add, add = add,
@@ -60,30 +62,29 @@ function StringDiffVisualizer:didUpdate(previousProps)
end end
end end
function StringDiffVisualizer:calculateContentSize(theme) function StringDiffVisualizer:calculateContentSize()
local oldString, newString = self.props.oldString, self.props.newString local oldText, newText = self.props.oldText, self.props.newText
local oldStringBounds = getTextBoundsAsync(oldString, theme.Font.Code, theme.TextSize.Code, math.huge) local oldTextBounds = TextService:GetTextSize(oldText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
local newStringBounds = getTextBoundsAsync(newString, theme.Font.Code, theme.TextSize.Code, math.huge) local newTextBounds = TextService:GetTextSize(newText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
self.setContentSize( self.setContentSize(
Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y)) Vector2.new(math.max(oldTextBounds.X, newTextBounds.X), math.max(oldTextBounds.Y, newTextBounds.Y))
) )
end end
function StringDiffVisualizer:calculateDiffLines() function StringDiffVisualizer:calculateDiffLines()
Timer.start("StringDiffVisualizer:calculateDiffLines") local oldText, newText = self.props.oldText, self.props.newText
local oldString, newString = self.props.oldString, self.props.newString
-- Diff the two texts -- Diff the two texts
local startClock = os.clock() local startClock = os.clock()
local diffs = StringDiff.findDiffs(oldString, newString) local diffs = StringDiff.findDiffs(oldText, newText)
local stopClock = os.clock() local stopClock = os.clock()
Log.trace( Log.trace(
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections", "Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
#oldString, #oldText,
#newString, #newText,
math.round((stopClock - startClock) * 1000 * 1000), math.round((stopClock - startClock) * 1000 * 1000),
#diffs #diffs
) )
@@ -132,16 +133,13 @@ function StringDiffVisualizer:calculateDiffLines()
end end
end end
Timer.stop()
return add, remove return add, remove
end end
function StringDiffVisualizer:render() function StringDiffVisualizer:render()
local oldString, newString = self.props.oldString, self.props.newString local oldText, newText = self.props.oldText, self.props.newText
return Theme.with(function(theme) return Theme.with(function(theme)
self:calculateContentSize(theme)
return e(BorderedContainer, { return e(BorderedContainer, {
size = self.props.size, size = self.props.size,
position = self.props.position, position = self.props.position,
@@ -177,7 +175,7 @@ function StringDiffVisualizer:render()
Source = e(CodeLabel, { Source = e(CodeLabel, {
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
position = UDim2.new(0, 0, 0, 0), position = UDim2.new(0, 0, 0, 0),
text = oldString, text = oldText,
lineBackground = theme.Diff.Remove, lineBackground = theme.Diff.Remove,
markedLines = self.state.remove, markedLines = self.state.remove,
}), }),
@@ -192,7 +190,7 @@ function StringDiffVisualizer:render()
Source = e(CodeLabel, { Source = e(CodeLabel, {
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
position = UDim2.new(0, 0, 0, 0), position = UDim2.new(0, 0, 0, 0),
text = newString, text = newText,
lineBackground = theme.Diff.Add, lineBackground = theme.Diff.Add,
markedLines = self.state.add, markedLines = self.state.add,
}), }),

View File

@@ -1,195 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Timer = require(Plugin.Timer)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local DisplayValue = require(Plugin.App.Components.PatchVisualizer.DisplayValue)
local e = Roact.createElement
local Array = Roact.Component:extend("Array")
function Array:init()
self:setState({
diff = self:calculateDiff(),
})
end
function Array:calculateDiff()
Timer.start("Array:calculateDiff")
--[[
Find the indexes that are added or removed from the array,
and display them side by side with gaps for the indexes that
dont exist in the opposite array.
]]
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
local i, j = 1, 1
local diff = {}
while i <= #oldTable and j <= #newTable do
if oldTable[i] == newTable[j] then
table.insert(diff, { oldTable[i], newTable[j] }) -- Unchanged
i += 1
j += 1
elseif not table.find(newTable, oldTable[i], j) then
table.insert(diff, { oldTable[i], nil }) -- Removal
i += 1
elseif not table.find(oldTable, newTable[j], i) then
table.insert(diff, { nil, newTable[j] }) -- Addition
j += 1
else
if table.find(newTable, oldTable[i], j) then
table.insert(diff, { nil, newTable[j] }) -- Addition
j += 1
else
table.insert(diff, { oldTable[i], nil }) -- Removal
i += 1
end
end
end
-- Handle remaining elements
while i <= #oldTable do
table.insert(diff, { oldTable[i], nil }) -- Remaining Removals
i += 1
end
while j <= #newTable do
table.insert(diff, { nil, newTable[j] }) -- Remaining Additions
j += 1
end
Timer.stop()
return diff
end
function Array:didUpdate(previousProps)
if previousProps.oldTable ~= self.props.oldTable or previousProps.newTable ~= self.props.newTable then
self:setState({
diff = self:calculateDiff(),
})
end
end
function Array:render()
return Theme.with(function(theme)
local diff = self.state.diff
local lines = table.create(#diff)
for i, element in diff do
local oldValue = element[1]
local newValue = element[2]
local patchType = if oldValue == nil then "Add" elseif newValue == nil then "Remove" else "Remain"
table.insert(
lines,
e("Frame", {
Size = UDim2.new(1, 0, 0, 25),
BackgroundTransparency = if patchType == "Remain" then 1 else self.props.transparency,
BackgroundColor3 = if patchType == "Remain" then theme.Diff.Row else theme.Diff[patchType],
BorderSizePixel = 0,
LayoutOrder = i,
}, {
DiffIcon = if patchType ~= "Remain"
then e("ImageLabel", {
Image = Assets.Images.Diff[patchType],
ImageColor3 = theme.AddressEntry.PlaceholderColor,
ImageTransparency = self.props.transparency,
BackgroundTransparency = 1,
Size = UDim2.new(0, 15, 0, 15),
Position = UDim2.new(0, 7, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
})
else nil,
Old = e("Frame", {
Size = UDim2.new(0.5, -30, 1, 0),
Position = UDim2.new(0, 30, 0, 0),
BackgroundTransparency = 1,
}, {
Display = if oldValue ~= nil
then e(DisplayValue, {
value = oldValue,
transparency = self.props.transparency,
textColor = theme.Settings.Setting.DescriptionColor,
})
else nil,
}),
New = e("Frame", {
Size = UDim2.new(0.5, -10, 1, 0),
Position = UDim2.new(0.5, 5, 0, 0),
BackgroundTransparency = 1,
}, {
Display = if newValue ~= nil
then e(DisplayValue, {
value = newValue,
transparency = self.props.transparency,
textColor = theme.Settings.Setting.DescriptionColor,
})
else nil,
}),
})
)
end
return Roact.createFragment({
Headers = e("Frame", {
Size = UDim2.new(1, 0, 0, 25),
BackgroundTransparency = self.props.transparency:map(function(t)
return 0.95 + (0.05 * t)
end),
BackgroundColor3 = theme.Diff.Row,
}, {
ColumnA = e("TextLabel", {
Size = UDim2.new(0.5, -30, 1, 0),
Position = UDim2.new(0, 30, 0, 0),
BackgroundTransparency = 1,
Text = "Old",
TextXAlignment = Enum.TextXAlignment.Left,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextTruncate = Enum.TextTruncate.AtEnd,
}),
ColumnB = e("TextLabel", {
Size = UDim2.new(0.5, -10, 1, 0),
Position = UDim2.new(0.5, 5, 0, 0),
BackgroundTransparency = 1,
Text = "New",
TextXAlignment = Enum.TextXAlignment.Left,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextTruncate = Enum.TextTruncate.AtEnd,
}),
Separator = e("Frame", {
Size = UDim2.new(1, 0, 0, 1),
Position = UDim2.new(0, 0, 1, 0),
BackgroundTransparency = 0,
BorderSizePixel = 0,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
}),
}),
KeyValues = e(ScrollingFrame, {
position = UDim2.new(0, 1, 0, 25),
size = UDim2.new(1, -2, 1, -27),
scrollingDirection = Enum.ScrollingDirection.Y,
transparency = self.props.transparency,
}, {
Layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
}),
Lines = Roact.createFragment(lines),
}),
})
end)
end
return Array

View File

@@ -1,211 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Timer = require(Plugin.Timer)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local DisplayValue = require(Plugin.App.Components.PatchVisualizer.DisplayValue)
local e = Roact.createElement
local Dictionary = Roact.Component:extend("Dictionary")
function Dictionary:init()
self:setState({
diff = self:calculateDiff(),
})
end
function Dictionary:calculateDiff()
Timer.start("Dictionary:calculateDiff")
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
-- Diff the two tables and find the added keys, removed keys, and changed keys
local diff = {}
for key, oldValue in oldTable do
local newValue = newTable[key]
if newValue == nil then
table.insert(diff, {
key = key,
patchType = "Remove",
})
elseif newValue ~= oldValue then
-- Note: should this do some sort of deep comparison for various types?
table.insert(diff, {
key = key,
patchType = "Edit",
})
else
table.insert(diff, {
key = key,
patchType = "Remain",
})
end
end
for key in newTable do
if oldTable[key] == nil then
table.insert(diff, {
key = key,
patchType = "Add",
})
end
end
table.sort(diff, function(a, b)
return a.key < b.key
end)
Timer.stop()
return diff
end
function Dictionary:didUpdate(previousProps)
if previousProps.oldTable ~= self.props.oldTable or previousProps.newTable ~= self.props.newTable then
self:setState({
diff = self:calculateDiff(),
})
end
end
function Dictionary:render()
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
local diff = self.state.diff
return Theme.with(function(theme)
local lines = table.create(#diff)
for order, line in diff do
local key = line.key
local oldValue = oldTable[key]
local newValue = newTable[key]
table.insert(
lines,
e("Frame", {
Size = UDim2.new(1, 0, 0, 25),
LayoutOrder = order,
BorderSizePixel = 0,
BackgroundTransparency = if line.patchType == "Remain" then 1 else self.props.transparency,
BackgroundColor3 = if line.patchType == "Remain"
then theme.Diff.Row
else theme.Diff[line.patchType],
}, {
DiffIcon = if line.patchType ~= "Remain"
then e("ImageLabel", {
Image = Assets.Images.Diff[line.patchType],
ImageColor3 = theme.AddressEntry.PlaceholderColor,
ImageTransparency = self.props.transparency,
BackgroundTransparency = 1,
Size = UDim2.new(0, 15, 0, 15),
Position = UDim2.new(0, 7, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
})
else nil,
KeyName = e("TextLabel", {
Size = UDim2.new(0.3, -15, 1, 0),
Position = UDim2.new(0, 30, 0, 0),
BackgroundTransparency = 1,
Text = key,
TextXAlignment = Enum.TextXAlignment.Left,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextTruncate = Enum.TextTruncate.AtEnd,
}),
OldValue = e("Frame", {
Size = UDim2.new(0.35, -7, 1, 0),
Position = UDim2.new(0.3, 15, 0, 0),
BackgroundTransparency = 1,
}, {
e(DisplayValue, {
value = oldValue,
transparency = self.props.transparency,
textColor = theme.Settings.Setting.DescriptionColor,
}),
}),
NewValue = e("Frame", {
Size = UDim2.new(0.35, -8, 1, 0),
Position = UDim2.new(0.65, 8, 0, 0),
BackgroundTransparency = 1,
}, {
e(DisplayValue, {
value = newValue,
transparency = self.props.transparency,
textColor = theme.Settings.Setting.DescriptionColor,
}),
}),
})
)
end
return Roact.createFragment({
Headers = e("Frame", {
Size = UDim2.new(1, 0, 0, 25),
BackgroundTransparency = self.props.transparency:map(function(t)
return 0.95 + (0.05 * t)
end),
BackgroundColor3 = theme.Diff.Row,
}, {
ColumnA = e("TextLabel", {
Size = UDim2.new(0.3, -15, 1, 0),
Position = UDim2.new(0, 30, 0, 0),
BackgroundTransparency = 1,
Text = "Key",
TextXAlignment = Enum.TextXAlignment.Left,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextTruncate = Enum.TextTruncate.AtEnd,
}),
ColumnB = e("TextLabel", {
Size = UDim2.new(0.35, -7, 1, 0),
Position = UDim2.new(0.3, 15, 0, 0),
BackgroundTransparency = 1,
Text = "Old",
TextXAlignment = Enum.TextXAlignment.Left,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextTruncate = Enum.TextTruncate.AtEnd,
}),
ColumnC = e("TextLabel", {
Size = UDim2.new(0.35, -8, 1, 0),
Position = UDim2.new(0.65, 8, 0, 0),
BackgroundTransparency = 1,
Text = "New",
TextXAlignment = Enum.TextXAlignment.Left,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextTruncate = Enum.TextTruncate.AtEnd,
}),
Separator = e("Frame", {
Size = UDim2.new(1, 0, 0, 1),
Position = UDim2.new(0, 0, 1, 0),
BackgroundTransparency = 0,
BorderSizePixel = 0,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
}),
}),
KeyValues = e(ScrollingFrame, {
position = UDim2.new(0, 1, 0, 25),
size = UDim2.new(1, -2, 1, -27),
scrollingDirection = Enum.ScrollingDirection.Y,
transparency = self.props.transparency,
}, {
Layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
VerticalAlignment = Enum.VerticalAlignment.Top,
}),
Lines = Roact.createFragment(lines),
}),
})
end)
end
return Dictionary

View File

@@ -1,48 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local Array = require(script:FindFirstChild("Array"))
local Dictionary = require(script:FindFirstChild("Dictionary"))
local e = Roact.createElement
local TableDiffVisualizer = Roact.Component:extend("TableDiffVisualizer")
function TableDiffVisualizer:render()
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
-- Ensure we're diffing tables, not mixing types
if type(oldTable) ~= "table" then
oldTable = {}
end
if type(newTable) ~= "table" then
newTable = {}
end
local isArray = next(newTable) == 1 or next(oldTable) == 1
return e(BorderedContainer, {
size = self.props.size,
position = self.props.position,
anchorPoint = self.props.anchorPoint,
transparency = self.props.transparency,
}, {
Content = if isArray
then e(Array, {
oldTable = oldTable,
newTable = newTable,
transparency = self.props.transparency,
})
else e(Dictionary, {
oldTable = oldTable,
newTable = newTable,
transparency = self.props.transparency,
}),
})
end
return TableDiffVisualizer

View File

@@ -1,59 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local SlicedImage = require(Plugin.App.Components.SlicedImage)
local e = Roact.createElement
return function(props)
return Theme.with(function(theme)
return e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = props.color,
transparency = props.transparency:map(function(transparency)
return 0.9 + (0.1 * transparency)
end),
layoutOrder = props.layoutOrder,
position = props.position,
anchorPoint = props.anchorPoint,
size = UDim2.new(0, 0, 0, theme.TextSize.Medium),
automaticSize = Enum.AutomaticSize.X,
}, {
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 4),
PaddingRight = UDim.new(0, 4),
PaddingTop = UDim.new(0, 2),
PaddingBottom = UDim.new(0, 2),
}),
Icon = if props.icon
then e("ImageLabel", {
Size = UDim2.new(0, 12, 0, 12),
Position = UDim2.new(0, 0, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
Image = props.icon,
BackgroundTransparency = 1,
ImageColor3 = props.color,
ImageTransparency = props.transparency,
})
else nil,
Text = e("TextLabel", {
Text = props.text,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Small,
TextColor3 = props.color,
TextXAlignment = Enum.TextXAlignment.Center,
TextTransparency = props.transparency,
Size = UDim2.new(0, 0, 1, 0),
Position = UDim2.new(0, if props.icon then 15 else 0, 0, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
}),
})
end)
end

View File

@@ -1,3 +1,5 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages local Packages = Rojo.Packages
@@ -8,7 +10,6 @@ local Flipper = require(Packages.Flipper)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil) local bindingUtil = require(Plugin.App.bindingUtil)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local SlicedImage = require(script.Parent.SlicedImage) local SlicedImage = require(script.Parent.SlicedImage)
local TouchRipple = require(script.Parent.TouchRipple) local TouchRipple = require(script.Parent.TouchRipple)
@@ -40,17 +41,18 @@ end
function TextButton:render() function TextButton:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Large, math.huge) local textSize =
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge))
local style = self.props.style local style = self.props.style
local buttonTheme = theme.Button[style] theme = theme.Button[style]
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover") local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled") local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
return e("ImageButton", { return e("ImageButton", {
Size = UDim2.new(0, (theme.TextSize.Body * 2) + textBounds.X, 0, 34), Size = UDim2.new(0, 15 + textSize.X + 15, 0, 34),
Position = self.props.position, Position = self.props.position,
AnchorPoint = self.props.anchorPoint, AnchorPoint = self.props.anchorPoint,
@@ -72,22 +74,18 @@ function TextButton:render()
end, end,
}, { }, {
TouchRipple = e(TouchRipple, { TouchRipple = e(TouchRipple, {
color = buttonTheme.ActionFillColor, color = theme.ActionFillColor,
transparency = self.props.transparency:map(function(value) transparency = self.props.transparency:map(function(value)
return bindingUtil.blendAlpha({ buttonTheme.ActionFillTransparency, value }) return bindingUtil.blendAlpha({ theme.ActionFillTransparency, value })
end), end),
zIndex = 2, zIndex = 2,
}), }),
Text = e("TextLabel", { Text = e("TextLabel", {
Text = self.props.text, Text = self.props.text,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Large, TextSize = 18,
TextColor3 = bindingUtil.mapLerp( TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
bindingEnabled,
buttonTheme.Enabled.TextColor,
buttonTheme.Disabled.TextColor
),
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
@@ -97,11 +95,7 @@ function TextButton:render()
Border = style == "Bordered" and e(SlicedImage, { Border = style == "Bordered" and e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = bindingUtil.mapLerp( color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
bindingEnabled,
buttonTheme.Enabled.BorderColor,
buttonTheme.Disabled.BorderColor
),
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
@@ -111,18 +105,14 @@ function TextButton:render()
HoverOverlay = e(SlicedImage, { HoverOverlay = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = buttonTheme.ActionFillColor, color = theme.ActionFillColor,
transparency = Roact.joinBindings({ transparency = Roact.joinBindings({
hover = bindingHover:map(function(value) hover = bindingHover:map(function(value)
return 1 - value return 1 - value
end), end),
transparency = self.props.transparency, transparency = self.props.transparency,
}):map(function(values) }):map(function(values)
return bindingUtil.blendAlpha({ return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
buttonTheme.ActionFillTransparency,
values.hover,
values.transparency,
})
end), end),
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
@@ -134,8 +124,8 @@ function TextButton:render()
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = bindingUtil.mapLerp( color = bindingUtil.mapLerp(
bindingEnabled, bindingEnabled,
buttonTheme.Enabled.BackgroundColor, theme.Enabled.BackgroundColor,
buttonTheme.Disabled.BackgroundColor theme.Disabled.BackgroundColor
), ),
transparency = self.props.transparency, transparency = self.props.transparency,

View File

@@ -38,18 +38,14 @@ end
function TextInput:render() function TextInput:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local textInputTheme = theme.TextInput theme = theme.TextInput
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover") local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled") local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
return e(SlicedImage, { return e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = bindingUtil.mapLerp( color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
bindingEnabled,
textInputTheme.Enabled.BorderColor,
textInputTheme.Disabled.BorderColor
),
transparency = self.props.transparency, transparency = self.props.transparency,
size = self.props.size or UDim2.new(1, 0, 1, 0), size = self.props.size or UDim2.new(1, 0, 1, 0),
@@ -59,18 +55,14 @@ function TextInput:render()
}, { }, {
HoverOverlay = e(SlicedImage, { HoverOverlay = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = textInputTheme.ActionFillColor, color = theme.ActionFillColor,
transparency = Roact.joinBindings({ transparency = Roact.joinBindings({
hover = bindingHover:map(function(value) hover = bindingHover:map(function(value)
return 1 - value return 1 - value
end), end),
transparency = self.props.transparency, transparency = self.props.transparency,
}):map(function(values) }):map(function(values)
return bindingUtil.blendAlpha({ return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
textInputTheme.ActionFillTransparency,
values.hover,
values.transparency,
})
end), end),
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
zIndex = -1, zIndex = -1,
@@ -80,18 +72,14 @@ function TextInput:render()
Size = UDim2.fromScale(1, 1), Size = UDim2.fromScale(1, 1),
Text = self.props.text, Text = self.props.text,
PlaceholderText = self.props.placeholder, PlaceholderText = self.props.placeholder,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextColor3 = bindingUtil.mapLerp( TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
bindingEnabled,
textInputTheme.Disabled.TextColor,
textInputTheme.Enabled.TextColor
),
PlaceholderColor3 = bindingUtil.mapLerp( PlaceholderColor3 = bindingUtil.mapLerp(
bindingEnabled, bindingEnabled,
textInputTheme.Disabled.PlaceholderColor, theme.Disabled.PlaceholderColor,
textInputTheme.Enabled.PlaceholderColor theme.Enabled.PlaceholderColor
), ),
TextSize = theme.TextSize.Large, TextSize = 18,
TextEditable = self.props.enabled, TextEditable = self.props.enabled,
ClearTextOnFocus = self.props.clearTextOnFocus, ClearTextOnFocus = self.props.clearTextOnFocus,

View File

@@ -1,3 +1,4 @@
local TextService = game:GetService("TextService")
local HttpService = game:GetService("HttpService") local HttpService = game:GetService("HttpService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
@@ -7,8 +8,6 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local e = Roact.createElement local e = Roact.createElement
@@ -22,48 +21,50 @@ local Y_OVERLAP = 10 -- Let the triangle tail piece overlap the target a bit to
local TooltipContext = Roact.createContext({}) local TooltipContext = Roact.createContext({})
local function Popup(props) local function Popup(props)
local textSize = TextService:GetTextSize(
props.Text,
16,
Enum.Font.GothamMedium,
Vector2.new(math.min(props.parentSize.X, 160), math.huge)
) + TEXT_PADDING + (Vector2.one * 2)
local trigger = props.Trigger:getValue()
local spaceBelow = props.parentSize.Y
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
local displayAbove = spaceBelow < textSize.Y and spaceAbove > spaceBelow
local X = math.clamp(props.Position.X - X_OFFSET, 0, props.parentSize.X - textSize.X)
local Y = 0
if displayAbove then
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
else
Y = math.min(
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
props.parentSize.Y - textSize.Y
)
end
return Theme.with(function(theme) return Theme.with(function(theme)
local textXSpace = math.min(props.parentSize.X, 250) - TEXT_PADDING.X
local textBounds = getTextBoundsAsync(props.Text, theme.Font.Main, theme.TextSize.Medium, textXSpace)
local contentSize = textBounds + TEXT_PADDING + (Vector2.one * 2)
local trigger = props.Trigger:getValue()
local spaceBelow = props.parentSize.Y
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
local displayAbove = spaceBelow < contentSize.Y and spaceAbove > spaceBelow
local X = math.clamp(props.Position.X - X_OFFSET, 0, math.max(props.parentSize.X - contentSize.X, 1))
local Y = 0
if displayAbove then
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - contentSize.Y + Y_OVERLAP, 0)
else
Y = math.min(
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
props.parentSize.Y - contentSize.Y
)
end
return e(BorderedContainer, { return e(BorderedContainer, {
position = UDim2.fromOffset(X, Y), position = UDim2.fromOffset(X, Y),
size = UDim2.fromOffset(contentSize.X, contentSize.Y), size = UDim2.fromOffset(textSize.X, textSize.Y),
transparency = props.transparency, transparency = props.transparency,
}, { }, {
Label = e("TextLabel", { Label = e("TextLabel", {
BackgroundTransparency = 1, BackgroundTransparency = 1,
Position = UDim2.fromScale(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(1, -TEXT_PADDING.X, 1, -TEXT_PADDING.Y),
AnchorPoint = Vector2.new(0.5, 0.5), AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.fromOffset(textBounds.X, textBounds.Y),
Text = props.Text, Text = props.Text,
TextSize = theme.TextSize.Medium, TextSize = 16,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextWrapped = true, TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
TextColor3 = theme.Button.Bordered.Enabled.TextColor, TextColor3 = theme.Button.Bordered.Enabled.TextColor,
TextTransparency = props.transparency, TextTransparency = props.transparency,
}), }),
@@ -71,8 +72,8 @@ local function Popup(props)
Tail = e("ImageLabel", { Tail = e("ImageLabel", {
ZIndex = 100, ZIndex = 100,
Position = if displayAbove Position = if displayAbove
then UDim2.new(0, math.clamp(props.Position.X - X, 6, contentSize.X - 6), 1, -1) then UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 1, -1)
else UDim2.new(0, math.clamp(props.Position.X - X, 6, contentSize.X - 6), 0, -TAIL_SIZE + 1), else UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 0, -TAIL_SIZE + 1),
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE), Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
AnchorPoint = Vector2.new(0.5, 0), AnchorPoint = Vector2.new(0.5, 0),
Rotation = if displayAbove then 180 else 0, Rotation = if displayAbove then 180 else 0,
@@ -162,6 +163,7 @@ local Trigger = Roact.Component:extend("TooltipTrigger")
function Trigger:init() function Trigger:init()
self.id = HttpService:GenerateGUID(false) self.id = HttpService:GenerateGUID(false)
self.ref = Roact.createRef() self.ref = Roact.createRef()
self.mousePos = Vector2.zero
self.showingPopup = false self.showingPopup = false
self.destroy = function() self.destroy = function()
@@ -193,22 +195,18 @@ end
function Trigger:isHovering() function Trigger:isHovering()
local rbx = self.ref.current local rbx = self.ref.current
if rbx then if rbx then
return rbx.GuiState == Enum.GuiState.Hover local pos = rbx.AbsolutePosition
local size = rbx.AbsoluteSize
local mousePos = self.mousePos
return mousePos.X >= pos.X
and mousePos.X <= pos.X + size.X
and mousePos.Y >= pos.Y
and mousePos.Y <= pos.Y + size.Y
end end
return false return false
end end
function Trigger:getMousePos()
local rbx = self.ref.current
if rbx then
local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui")
if widget then
return widget:GetRelativeMousePosition()
end
end
return Vector2.zero
end
function Trigger:managePopup() function Trigger:managePopup()
if self:isHovering() then if self:isHovering() then
if self.showingPopup or self.showDelayThread then if self.showingPopup or self.showDelayThread then
@@ -219,7 +217,7 @@ function Trigger:managePopup()
self.showDelayThread = task.delay(DELAY, function() self.showDelayThread = task.delay(DELAY, function()
self.props.context.addTip(self.id, { self.props.context.addTip(self.id, {
Text = self.props.text, Text = self.props.text,
Position = self:getMousePos(), Position = self.mousePos,
Trigger = self.ref, Trigger = self.ref,
}) })
self.showDelayThread = nil self.showDelayThread = nil
@@ -236,7 +234,13 @@ function Trigger:managePopup()
end end
function Trigger:render() function Trigger:render()
local function recalculate() local function recalculate(rbx)
local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui")
if not widget then
return
end
self.mousePos = widget:GetRelativeMousePosition()
self:managePopup() self:managePopup()
end end
@@ -246,9 +250,11 @@ function Trigger:render()
ZIndex = self.props.zIndex or 100, ZIndex = self.props.zIndex or 100,
[Roact.Ref] = self.ref, [Roact.Ref] = self.ref,
[Roact.Change.GuiState] = recalculate,
[Roact.Change.AbsolutePosition] = recalculate, [Roact.Change.AbsolutePosition] = recalculate,
[Roact.Change.AbsoluteSize] = recalculate, [Roact.Change.AbsoluteSize] = recalculate,
[Roact.Event.MouseMoved] = recalculate,
[Roact.Event.MouseLeave] = recalculate,
[Roact.Event.MouseEnter] = recalculate,
}) })
end end

View File

@@ -131,8 +131,8 @@ function VirtualScroller:render()
Position = props.position, Position = props.position,
AnchorPoint = props.anchorPoint, AnchorPoint = props.anchorPoint,
BackgroundTransparency = props.backgroundTransparency or 1, BackgroundTransparency = props.backgroundTransparency or 1,
BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor, BackgroundColor3 = props.backgroundColor3,
BorderColor3 = props.borderColor3 or theme.BorderedContainer.BorderColor, BorderColor3 = props.borderColor3,
CanvasSize = self.totalCanvas:map(function(s) CanvasSize = self.totalCanvas:map(function(s)
return UDim2.fromOffset(0, s) return UDim2.fromOffset(0, s)
end), end),

View File

@@ -1,3 +1,4 @@
local TextService = game:GetService("TextService")
local StudioService = game:GetService("StudioService") local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
@@ -8,10 +9,10 @@ local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper) local Flipper = require(Packages.Flipper)
local Log = require(Packages.Log) local Log = require(Packages.Log)
local bindingUtil = require(script.Parent.bindingUtil)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local TextButton = require(Plugin.App.Components.TextButton) local TextButton = require(Plugin.App.Components.TextButton)
@@ -85,46 +86,51 @@ function Notification:render()
return 1 - value return 1 - value
end) end)
return Theme.with(function(theme) local textBounds = TextService:GetTextSize(self.props.text, 15, Enum.Font.GothamMedium, Vector2.new(350, 700))
local actionButtons = {}
local buttonsX = 0
if self.props.actions then
local count = 0
for key, action in self.props.actions do
actionButtons[key] = e(TextButton, {
text = action.text,
style = action.style,
onClick = function()
local success, err = pcall(action.onClick, self)
if not success then
Log.warn("Error in notification action: " .. tostring(err))
end
end,
layoutOrder = -action.layoutOrder,
transparency = transparency,
})
buttonsX += getTextBoundsAsync(action.text, theme.Font.Main, theme.TextSize.Large, math.huge).X + (theme.TextSize.Body * 2) local actionButtons = {}
local buttonsX = 0
if self.props.actions then
local count = 0
for key, action in self.props.actions do
actionButtons[key] = e(TextButton, {
text = action.text,
style = action.style,
onClick = function()
local success, err = pcall(action.onClick, self)
if not success then
Log.warn("Error in notification action: " .. tostring(err))
end
end,
layoutOrder = -action.layoutOrder,
transparency = transparency,
})
count += 1 buttonsX += TextService:GetTextSize(
end action.text,
18,
Enum.Font.GothamMedium,
Vector2.new(math.huge, math.huge)
).X + 30
buttonsX += (count - 1) * 5 count += 1
end end
local paddingY, logoSize = 20, 32 buttonsX += (count - 1) * 5
local actionsY = if self.props.actions then 37 else 0 end
local textXSpace = math.max(250, buttonsX) + 35
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Body, textXSpace)
local contentX = math.max(textBounds.X, buttonsX)
local size = self.binding:map(function(value) local paddingY, logoSize = 20, 32
return UDim2.fromOffset( local actionsY = if self.props.actions then 35 else 0
(35 + 40 + contentX) * value, local contentX = math.max(textBounds.X, buttonsX)
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
)
end)
local size = self.binding:map(function(value)
return UDim2.fromOffset(
(35 + 40 + contentX) * value,
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
)
end)
return Theme.with(function(theme)
return e("TextButton", { return e("TextButton", {
BackgroundTransparency = 1, BackgroundTransparency = 1,
Size = size, Size = size,
@@ -141,7 +147,8 @@ function Notification:render()
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
}, { }, {
Contents = e("Frame", { Contents = e("Frame", {
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
Position = UDim2.new(0, 0, 0, paddingY / 2),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, { }, {
Logo = e("ImageLabel", { Logo = e("ImageLabel", {
@@ -154,15 +161,14 @@ function Notification:render()
}), }),
Info = e("TextLabel", { Info = e("TextLabel", {
Text = self.props.text, Text = self.props.text,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 15,
TextColor3 = theme.Notification.InfoColor, TextColor3 = theme.Notification.InfoColor,
TextTransparency = transparency, TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Center,
TextWrapped = true, TextWrapped = true,
Size = UDim2.new(0, textBounds.X, 1, -actionsY), Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
Position = UDim2.fromOffset(35, 0), Position = UDim2.fromOffset(35, 0),
LayoutOrder = 1, LayoutOrder = 1,
@@ -170,7 +176,7 @@ function Notification:render()
}), }),
Actions = if self.props.actions Actions = if self.props.actions
then e("Frame", { then e("Frame", {
Size = UDim2.new(1, -40, 0, actionsY), Size = UDim2.new(1, -40, 0, 35),
Position = UDim2.new(1, 0, 1, 0), Position = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(1, 1), AnchorPoint = Vector2.new(1, 1),
BackgroundTransparency = 1, BackgroundTransparency = 1,
@@ -190,8 +196,6 @@ function Notification:render()
Padding = e("UIPadding", { Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17), PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15), PaddingRight = UDim.new(0, 15),
PaddingTop = UDim.new(0, paddingY / 2),
PaddingBottom = UDim.new(0, paddingY / 2),
}), }),
}), }),
}) })

View File

@@ -4,16 +4,14 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
local Timer = require(Plugin.Timer)
local PatchTree = require(Plugin.PatchTree)
local Settings = require(Plugin.Settings) local Settings = require(Plugin.Settings)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local TextButton = require(Plugin.App.Components.TextButton) local TextButton = require(Plugin.App.Components.TextButton)
local Header = require(Plugin.App.Components.Header)
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui) local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
local Tooltip = require(Plugin.App.Components.Tooltip) local Tooltip = require(Plugin.App.Components.Tooltip)
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer) local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer) local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer)
local e = Roact.createElement local e = Roact.createElement
@@ -24,75 +22,50 @@ function ConfirmingPage:init()
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
self:setState({ self:setState({
patchTree = nil, showingSourceDiff = false,
showingStringDiff = false, oldSource = "",
oldString = "", newSource = "",
newString = "",
showingTableDiff = false,
oldTable = {},
newTable = {},
}) })
if self.props.confirmData and self.props.confirmData.patch and self.props.confirmData.instanceMap then
self:buildPatchTree()
end
end
function ConfirmingPage:didUpdate(prevProps)
if prevProps.confirmData ~= self.props.confirmData then
self:buildPatchTree()
end
end
function ConfirmingPage:buildPatchTree()
Timer.start("ConfirmingPage:buildPatchTree")
self:setState({
patchTree = PatchTree.build(
self.props.confirmData.patch,
self.props.confirmData.instanceMap,
{ "Property", "Current", "Incoming" }
),
})
Timer.stop()
end end
function ConfirmingPage:render() function ConfirmingPage:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local pageContent = Roact.createFragment({ local pageContent = Roact.createFragment({
Header = e(Header, {
transparency = self.props.transparency,
layoutOrder = 1,
}),
Title = e("TextLabel", { Title = e("TextLabel", {
Text = string.format( Text = string.format(
"Sync changes for project '%s':", "Sync changes for project '%s':",
self.props.confirmData.serverInfo.projectName or "UNKNOWN" self.props.confirmData.serverInfo.projectName or "UNKNOWN"
), ),
FontFace = theme.Font.Thin, LayoutOrder = 2,
Font = Enum.Font.Gotham,
LineHeight = 1.2, LineHeight = 1.2,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = theme.TextColor, TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, theme.TextSize.Large + 2), Size = UDim2.new(1, 0, 0, 20),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
PatchVisualizer = e(PatchVisualizer, { PatchVisualizer = e(PatchVisualizer, {
size = UDim2.new(1, 0, 1, -100), size = UDim2.new(1, 0, 1, -150),
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 3, layoutOrder = 3,
patchTree = self.state.patchTree, changeListHeaders = { "Property", "Current", "Incoming" },
patch = self.props.confirmData.patch,
instanceMap = self.props.confirmData.instanceMap,
showStringDiff = function(oldString: string, newString: string) showSourceDiff = function(oldSource: string, newSource: string)
self:setState({ self:setState({
showingStringDiff = true, showingSourceDiff = true,
oldString = oldString, oldSource = oldSource,
newString = newString, newSource = newSource,
})
end,
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
self:setState({
showingTableDiff = true,
oldTable = oldTable,
newTable = newTable,
}) })
end, end,
}), }),
@@ -148,11 +121,6 @@ function ConfirmingPage:render()
}), }),
}), }),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 8),
PaddingRight = UDim.new(0, 8),
}),
Layout = e("UIListLayout", { Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center, HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center, VerticalAlignment = Enum.VerticalAlignment.Center,
@@ -161,10 +129,15 @@ function ConfirmingPage:render()
Padding = UDim.new(0, 10), Padding = UDim.new(0, 10),
}), }),
StringDiff = e(StudioPluginGui, { Padding = e("UIPadding", {
id = "Rojo_ConfirmingStringDiff", PaddingLeft = UDim.new(0, 20),
title = "String diff", PaddingRight = UDim.new(0, 20),
active = self.state.showingStringDiff, }),
SourceDiff = e(StudioPluginGui, {
id = "Rojo_ConfirmingSourceDiff",
title = "Source diff",
active = self.state.showingSourceDiff,
isEphemeral = true, isEphemeral = true,
initDockState = Enum.InitialDockState.Float, initDockState = Enum.InitialDockState.Float,
@@ -176,7 +149,7 @@ function ConfirmingPage:render()
onClose = function() onClose = function()
self:setState({ self:setState({
showingStringDiff = false, showingSourceDiff = false,
}) })
end, end,
}, { }, {
@@ -192,46 +165,8 @@ function ConfirmingPage:render()
anchorPoint = Vector2.new(0, 0), anchorPoint = Vector2.new(0, 0),
transparency = self.props.transparency, transparency = self.props.transparency,
oldString = self.state.oldString, oldText = self.state.oldSource,
newString = self.state.newString, newText = self.state.newSource,
}),
}),
}),
}),
TableDiff = e(StudioPluginGui, {
id = "Rojo_ConfirmingTableDiff",
title = "Table diff",
active = self.state.showingTableDiff,
isEphemeral = true,
initDockState = Enum.InitialDockState.Float,
overridePreviousState = true,
floatingSize = Vector2.new(500, 350),
minimumSize = Vector2.new(400, 250),
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
onClose = function()
self:setState({
showingTableDiff = false,
})
end,
}, {
TooltipsProvider = e(Tooltip.Provider, nil, {
Tooltips = e(Tooltip.Container, nil),
Content = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
e(TableDiffVisualizer, {
size = UDim2.new(1, -10, 1, -10),
position = UDim2.new(0, 5, 0, 5),
anchorPoint = Vector2.new(0, 0),
transparency = self.props.transparency,
oldTable = self.state.oldTable,
newTable = self.state.newTable,
}), }),
}), }),
}), }),

View File

@@ -3,8 +3,9 @@ local Plugin = Rojo.Plugin
local Packages = Rojo.Packages local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)
local timeUtil = require(Plugin.timeUtil) local bindingUtil = require(Plugin.App.bindingUtil)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local PatchSet = require(Plugin.PatchSet) local PatchSet = require(Plugin.PatchSet)
@@ -17,188 +18,86 @@ local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local Tooltip = require(Plugin.App.Components.Tooltip) local Tooltip = require(Plugin.App.Components.Tooltip)
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer) local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer) local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer)
local e = Roact.createElement local e = Roact.createElement
local ChangesViewer = Roact.Component:extend("ChangesViewer") local AGE_UNITS = {
{ 31556909, "year" },
{ 2629743, "month" },
{ 604800, "week" },
{ 86400, "day" },
{ 3600, "hour" },
{
60,
"minute",
},
}
function timeSinceText(elapsed: number): string
if elapsed < 3 then
return "just now"
end
function ChangesViewer:init() local ageText = string.format("%d seconds ago", elapsed)
for _, UnitData in ipairs(AGE_UNITS) do
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
if elapsed > UnitSeconds then
local c = math.floor(elapsed / UnitSeconds)
ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "")
break
end
end
return ageText
end
local ChangesDrawer = Roact.Component:extend("ChangesDrawer")
function ChangesDrawer:init()
-- Hold onto the serve session during the lifecycle of this component -- Hold onto the serve session during the lifecycle of this component
-- so that it can still render during the fade out after disconnecting -- so that it can still render during the fade out after disconnecting
self.serveSession = self.props.serveSession self.serveSession = self.props.serveSession
end end
function ChangesViewer:render() function ChangesDrawer:render()
if self.props.rendered == false or self.serveSession == nil or self.props.patchData == nil then if self.props.rendered == false or self.serveSession == nil then
return nil return nil
end end
local unapplied = PatchSet.countChanges(self.props.patchData.unapplied)
local applied = PatchSet.countChanges(self.props.patchData.patch) - unapplied
return Theme.with(function(theme) return Theme.with(function(theme)
return Roact.createFragment({ return e(BorderedContainer, {
Navbar = e("Frame", { transparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 40), size = self.props.height:map(function(y)
BackgroundTransparency = 1, return UDim2.new(1, 0, y, -220 * y)
end),
position = UDim2.new(0, 0, 1, 0),
anchorPoint = Vector2.new(0, 1),
layoutOrder = self.props.layoutOrder,
}, {
Close = e(IconButton, {
icon = Assets.Images.Icons.Close,
iconSize = 24,
color = theme.ConnectionDetails.DisconnectColor,
transparency = self.props.transparency,
position = UDim2.new(1, 0, 0, 0),
anchorPoint = Vector2.new(1, 0),
onClick = self.props.onClose,
}, { }, {
Close = e(IconButton, { Tip = e(Tooltip.Trigger, {
icon = Assets.Images.Icons.Close, text = "Close the patch visualizer",
iconSize = 24,
color = theme.Settings.Navbar.BackButtonColor,
transparency = self.props.transparency,
position = UDim2.new(0, 0, 0.5, 0),
anchorPoint = Vector2.new(0, 0.5),
onClick = self.props.onBack,
}, {
Tip = e(Tooltip.Trigger, {
text = "Close",
}),
}),
Title = e("TextLabel", {
Text = "Sync",
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Large,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = theme.TextColor,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, -40, 0, theme.TextSize.Large + 2),
Position = UDim2.new(0, 40, 0, 0),
BackgroundTransparency = 1,
}),
Subtitle = e("TextLabel", {
Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
TextXAlignment = Enum.TextXAlignment.Left,
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Medium,
TextColor3 = theme.SubTextColor,
TextTruncate = Enum.TextTruncate.AtEnd,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, -40, 0, theme.TextSize.Medium),
Position = UDim2.new(0, 40, 0, theme.TextSize.Large + 2),
BackgroundTransparency = 1,
}),
Info = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 10, 0, 24),
AutomaticSize = Enum.AutomaticSize.X,
Position = UDim2.new(1, -5, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
}, {
Tooltip = e(Tooltip.Trigger, {
text = `{applied} changes applied`
.. (if unapplied > 0 then `, {unapplied} changes failed` else ""),
}),
Content = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 4),
}),
StatusIcon = e("ImageLabel", {
BackgroundTransparency = 1,
Image = if unapplied > 0
then Assets.Images.Icons.SyncWarning
else Assets.Images.Icons.SyncSuccess,
ImageColor3 = if unapplied > 0 then theme.Diff.Warning else theme.TextColor,
Size = UDim2.new(0, 24, 0, 24),
LayoutOrder = 10,
}),
StatusSpacer = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 6, 0, 4),
LayoutOrder = 9,
}),
AppliedIcon = e("ImageLabel", {
BackgroundTransparency = 1,
Image = Assets.Images.Icons.Checkmark,
ImageColor3 = theme.TextColor,
Size = UDim2.new(0, 16, 0, 16),
LayoutOrder = 1,
}),
AppliedText = e("TextLabel", {
Text = applied,
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Body,
TextColor3 = theme.TextColor,
TextTransparency = self.props.transparency,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
LayoutOrder = 2,
}),
Warnings = if unapplied > 0
then Roact.createFragment({
WarningsSpacer = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 4, 0, 4),
LayoutOrder = 3,
}),
UnappliedIcon = e("ImageLabel", {
BackgroundTransparency = 1,
Image = Assets.Images.Icons.Exclamation,
ImageColor3 = theme.Diff.Warning,
Size = UDim2.new(0, 4, 0, 16),
LayoutOrder = 4,
}),
UnappliedText = e("TextLabel", {
Text = unapplied,
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Diff.Warning,
TextTransparency = self.props.transparency,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1,
LayoutOrder = 5,
}),
})
else nil,
}),
}),
Divider = e("Frame", {
BackgroundColor3 = theme.Settings.DividerColor,
BackgroundTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 1),
Position = UDim2.new(0, 0, 1, 0),
BorderSizePixel = 0,
}, {
Gradient = e("UIGradient", {
Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 1),
NumberSequenceKeypoint.new(0.1, 0),
NumberSequenceKeypoint.new(0.9, 0),
NumberSequenceKeypoint.new(1, 1),
}),
}),
}), }),
}), }),
Patch = e(PatchVisualizer, { PatchVisualizer = e(PatchVisualizer, {
size = UDim2.new(1, -10, 1, -65), size = UDim2.new(1, 0, 1, 0),
position = UDim2.new(0, 5, 1, -5),
anchorPoint = Vector2.new(0, 1),
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = self.props.layoutOrder, layoutOrder = 3,
patchTree = self.props.patchTree, patchTree = self.props.patchTree,
showStringDiff = self.props.showStringDiff, showSourceDiff = self.props.showSourceDiff,
showTableDiff = self.props.showTableDiff,
}), }),
}) })
end) end)
@@ -217,13 +116,13 @@ local function ConnectionDetails(props)
}, { }, {
ProjectName = e("TextLabel", { ProjectName = e("TextLabel", {
Text = props.projectName, Text = props.projectName,
FontFace = theme.Font.Bold, Font = Enum.Font.GothamBold,
TextSize = theme.TextSize.Large, TextSize = 20,
TextColor3 = theme.ConnectionDetails.ProjectNameColor, TextColor3 = theme.ConnectionDetails.ProjectNameColor,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, theme.TextSize.Large), Size = UDim2.new(1, 0, 0, 20),
LayoutOrder = 1, LayoutOrder = 1,
BackgroundTransparency = 1, BackgroundTransparency = 1,
@@ -231,13 +130,13 @@ local function ConnectionDetails(props)
Address = e("TextLabel", { Address = e("TextLabel", {
Text = props.address, Text = props.address,
FontFace = theme.Font.Code, Font = Enum.Font.Code,
TextSize = theme.TextSize.Medium, TextSize = 15,
TextColor3 = theme.ConnectionDetails.AddressColor, TextColor3 = theme.ConnectionDetails.AddressColor,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium), Size = UDim2.new(1, 0, 0, 15),
LayoutOrder = 2, LayoutOrder = 2,
BackgroundTransparency = 1, BackgroundTransparency = 1,
@@ -266,7 +165,20 @@ function ConnectedPage:getChangeInfoText()
if patchData == nil then if patchData == nil then
return "" return ""
end end
return timeUtil.elapsedToText(DateTime.now().UnixTimestamp - patchData.timestamp)
local elapsed = os.time() - patchData.timestamp
local unapplied = PatchSet.countChanges(patchData.unapplied)
return "<i>Synced "
.. timeSinceText(elapsed)
.. (if unapplied > 0
then string.format(
', <font color="#FF8E3C">but %d change%s failed to apply</font>',
unapplied,
unapplied == 1 and "" or "s"
)
else "")
.. "</i>"
end end
function ConnectedPage:startChangeInfoTextUpdater() function ConnectedPage:startChangeInfoTextUpdater()
@@ -276,13 +188,17 @@ function ConnectedPage:startChangeInfoTextUpdater()
-- Start a new updater -- Start a new updater
self.changeInfoTextUpdater = task.defer(function() self.changeInfoTextUpdater = task.defer(function()
while true do while true do
self.setChangeInfoText(self:getChangeInfoText()) if self.state.hoveringChangeInfo then
self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
else
self.setChangeInfoText(self:getChangeInfoText())
end
local elapsed = DateTime.now().UnixTimestamp - self.props.patchData.timestamp local elapsed = os.time() - self.props.patchData.timestamp
local updateInterval = 1 local updateInterval = 1
-- Update timestamp text as frequently as currently needed -- Update timestamp text as frequently as currently needed
for _, UnitData in ipairs(timeUtil.AGE_UNITS) do for _, UnitData in ipairs(AGE_UNITS) do
local UnitSeconds = UnitData[1] local UnitSeconds = UnitData[1]
if elapsed > UnitSeconds then if elapsed > UnitSeconds then
updateInterval = UnitSeconds updateInterval = UnitSeconds
@@ -303,12 +219,29 @@ function ConnectedPage:stopChangeInfoTextUpdater()
end end
function ConnectedPage:init() function ConnectedPage:init()
self.changeDrawerMotor = Flipper.SingleMotor.new(0)
self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
self.changeDrawerMotor:onStep(function(value)
local renderChanges = value > 0.05
self:setState(function(state)
if state.renderChanges == renderChanges then
return nil
end
return {
renderChanges = renderChanges,
}
end)
end)
self:setState({ self:setState({
renderChanges = false, renderChanges = false,
hoveringChangeInfo = false, hoveringChangeInfo = false,
showingStringDiff = false, showingSourceDiff = false,
oldString = "", oldSource = "",
newString = "", newSource = "",
}) })
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("") self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
@@ -325,16 +258,12 @@ function ConnectedPage:didUpdate(previousProps)
-- New patch recieved -- New patch recieved
self:startChangeInfoTextUpdater() self:startChangeInfoTextUpdater()
self:setState({ self:setState({
showingStringDiff = false, showingSourceDiff = false,
}) })
end end
end end
function ConnectedPage:render() function ConnectedPage:render()
local syncWarning = self.props.patchData
and self.props.patchData.unapplied
and PatchSet.countChanges(self.props.patchData.unapplied) > 0
return Theme.with(function(theme) return Theme.with(function(theme)
return Roact.createFragment({ return Roact.createFragment({
Padding = e("UIPadding", { Padding = e("UIPadding", {
@@ -349,88 +278,9 @@ function ConnectedPage:render()
Padding = UDim.new(0, 10), Padding = UDim.new(0, 10),
}), }),
Heading = e("Frame", { Header = e(Header, {
BackgroundTransparency = 1, transparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 32), layoutOrder = 1,
}, {
Header = e(Header, {
transparency = self.props.transparency,
}),
ChangeInfo = e("TextButton", {
Text = "",
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = theme.BorderedContainer.BorderedColor,
BackgroundTransparency = if self.state.hoveringChangeInfo then 0.7 else 1,
BorderSizePixel = 0,
Position = UDim2.new(1, -5, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
[Roact.Event.MouseEnter] = function()
self:setState({
hoveringChangeInfo = true,
})
end,
[Roact.Event.MouseLeave] = function()
self:setState({
hoveringChangeInfo = false,
})
end,
[Roact.Event.Activated] = function()
self:setState(function(prevState)
prevState = prevState or {}
return {
renderChanges = not prevState.renderChanges,
}
end)
end,
}, {
Corner = e("UICorner", {
CornerRadius = UDim.new(0, 5),
}),
Tooltip = e(Tooltip.Trigger, {
text = if self.state.renderChanges then "Hide changes" else "View changes",
}),
Content = e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 5),
PaddingRight = UDim.new(0, 5),
}),
Text = e("TextLabel", {
BackgroundTransparency = 1,
Text = self.changeInfoText,
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Body,
TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
TextTransparency = self.props.transparency,
TextXAlignment = Enum.TextXAlignment.Right,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 1,
}),
Icon = e("ImageLabel", {
BackgroundTransparency = 1,
Image = if syncWarning
then Assets.Images.Icons.SyncWarning
else Assets.Images.Icons.SyncSuccess,
ImageColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 24, 0, 24),
LayoutOrder = 2,
}),
}),
}),
}), }),
ConnectionDetails = e(ConnectionDetails, { ConnectionDetails = e(ConnectionDetails, {
@@ -480,65 +330,83 @@ function ConnectedPage:render()
}), }),
}), }),
ChangesViewer = e(StudioPluginGui, { ChangeInfo = e("TextButton", {
id = "Rojo_ChangesViewer", Text = self.changeInfoText,
title = "View changes", Font = Enum.Font.Gotham,
active = self.state.renderChanges, TextSize = 14,
isEphemeral = true, TextWrapped = true,
RichText = true,
TextColor3 = theme.Header.VersionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextTransparency = self.props.transparency,
initDockState = Enum.InitialDockState.Float, Size = UDim2.new(1, 0, 0, 28),
overridePreviousState = true,
floatingSize = Vector2.new(400, 500),
minimumSize = Vector2.new(300, 300),
zIndexBehavior = Enum.ZIndexBehavior.Sibling, LayoutOrder = 4,
BackgroundTransparency = 1,
onClose = function() [Roact.Event.MouseEnter] = function()
self:setState({ self:setState({
renderChanges = false, hoveringChangeInfo = true,
}) })
self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
end,
[Roact.Event.MouseLeave] = function()
self:setState({
hoveringChangeInfo = false,
})
self.setChangeInfoText(self:getChangeInfoText())
end,
[Roact.Event.Activated] = function()
if self.state.renderChanges then
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
frequency = 4,
dampingRatio = 1,
}))
else
self.changeDrawerMotor:setGoal(Flipper.Spring.new(1, {
frequency = 3,
dampingRatio = 1,
}))
end
end, end,
}, { }, {
TooltipsProvider = e(Tooltip.Provider, nil, { Tooltip = e(Tooltip.Trigger, {
Tooltips = e(Tooltip.Container, nil), text = if self.state.renderChanges then "Hide the changes" else "View the changes",
Content = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
Changes = e(ChangesViewer, {
transparency = self.props.transparency,
rendered = self.state.renderChanges,
patchData = self.props.patchData,
patchTree = self.props.patchTree,
serveSession = self.props.serveSession,
showStringDiff = function(oldString: string, newString: string)
self:setState({
showingStringDiff = true,
oldString = oldString,
newString = newString,
})
end,
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
self:setState({
showingTableDiff = true,
oldTable = oldTable,
newTable = newTable,
})
end,
onBack = function()
self:setState({
renderChanges = false,
})
end,
}),
}),
}), }),
}), }),
StringDiff = e(StudioPluginGui, { ChangesDrawer = e(ChangesDrawer, {
id = "Rojo_ConnectedStringDiff", rendered = self.state.renderChanges,
title = "String diff", transparency = self.props.transparency,
active = self.state.showingStringDiff, patchTree = self.props.patchTree,
serveSession = self.props.serveSession,
height = self.changeDrawerHeight,
layoutOrder = 5,
showSourceDiff = function(oldSource: string, newSource: string)
self:setState({
showingSourceDiff = true,
oldSource = oldSource,
newSource = newSource,
})
end,
onClose = function()
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
frequency = 4,
dampingRatio = 1,
}))
end,
}),
SourceDiff = e(StudioPluginGui, {
id = "Rojo_ConnectedSourceDiff",
title = "Source diff",
active = self.state.showingSourceDiff,
isEphemeral = true, isEphemeral = true,
initDockState = Enum.InitialDockState.Float, initDockState = Enum.InitialDockState.Float,
@@ -550,7 +418,7 @@ function ConnectedPage:render()
onClose = function() onClose = function()
self:setState({ self:setState({
showingStringDiff = false, showingSourceDiff = false,
}) })
end, end,
}, { }, {
@@ -566,46 +434,8 @@ function ConnectedPage:render()
anchorPoint = Vector2.new(0, 0), anchorPoint = Vector2.new(0, 0),
transparency = self.props.transparency, transparency = self.props.transparency,
oldString = self.state.oldString, oldText = self.state.oldSource,
newString = self.state.newString, newText = self.state.newSource,
}),
}),
}),
}),
TableDiff = e(StudioPluginGui, {
id = "Rojo_ConnectedTableDiff",
title = "Table diff",
active = self.state.showingTableDiff,
isEphemeral = true,
initDockState = Enum.InitialDockState.Float,
overridePreviousState = false,
floatingSize = Vector2.new(500, 350),
minimumSize = Vector2.new(400, 250),
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
onClose = function()
self:setState({
showingTableDiff = false,
})
end,
}, {
TooltipsProvider = e(Tooltip.Provider, nil, {
Tooltips = e(Tooltip.Container, nil),
Content = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
e(TableDiffVisualizer, {
size = UDim2.new(1, -10, 1, -10),
position = UDim2.new(0, 5, 0, 5),
anchorPoint = Vector2.new(0, 0),
transparency = self.props.transparency,
oldTable = self.state.oldTable,
newTable = self.state.newTable,
}), }),
}), }),
}), }),

View File

@@ -1,3 +1,5 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages local Packages = Rojo.Packages
@@ -5,7 +7,6 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local TextButton = require(Plugin.App.Components.TextButton) local TextButton = require(Plugin.App.Components.TextButton)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
@@ -23,43 +24,43 @@ function Error:init()
end end
function Error:render() function Error:render()
return Theme.with(function(theme) return e(BorderedContainer, {
return e(BorderedContainer, { size = Roact.joinBindings({
size = Roact.joinBindings({ containerSize = self.props.containerSize,
containerSize = self.props.containerSize, contentSize = self.contentSize,
contentSize = self.contentSize, }):map(function(values)
}):map(function(values) local maximumSize = values.containerSize
local maximumSize = values.containerSize maximumSize -= Vector2.new(14, 14) * 2 -- Page padding
maximumSize -= Vector2.new(14, 14) * 2 -- Page padding maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
local outerSize = values.contentSize + ERROR_PADDING * 2 local outerSize = values.contentSize + ERROR_PADDING * 2
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y)) 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), end),
transparency = self.props.transparency, 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,
}, { }, {
ScrollingFrame = e(ScrollingFrame, { ErrorMessage = Theme.with(function(theme)
size = UDim2.new(1, 0, 1, 0), return e("TextBox", {
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 = getTextBoundsAsync(
self.props.errorMessage,
theme.Font.Code,
theme.TextSize.Code,
containerSize.X
)
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
end,
}, {
ErrorMessage = e("TextBox", {
[Roact.Event.InputBegan] = function(rbx, input) [Roact.Event.InputBegan] = function(rbx, input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
return return
@@ -70,8 +71,8 @@ function Error:render()
Text = self.props.errorMessage, Text = self.props.errorMessage,
TextEditable = false, TextEditable = false,
FontFace = theme.Font.Code, Font = Enum.Font.Code,
TextSize = theme.TextSize.Code, TextSize = 16,
TextColor3 = theme.ErrorColor, TextColor3 = theme.ErrorColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top, TextYAlignment = Enum.TextYAlignment.Top,
@@ -80,17 +81,17 @@ function Error:render()
ClearTextOnFocus = false, ClearTextOnFocus = false,
BackgroundTransparency = 1, BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
}), })
end),
Padding = e("UIPadding", { Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, ERROR_PADDING.X), PaddingLeft = UDim.new(0, ERROR_PADDING.X),
PaddingRight = UDim.new(0, ERROR_PADDING.X), PaddingRight = UDim.new(0, ERROR_PADDING.X),
PaddingTop = UDim.new(0, ERROR_PADDING.Y), PaddingTop = UDim.new(0, ERROR_PADDING.Y),
PaddingBottom = UDim.new(0, ERROR_PADDING.Y), PaddingBottom = UDim.new(0, ERROR_PADDING.Y),
}),
}), }),
}) }),
end) })
end end
local ErrorPage = Roact.Component:extend("ErrorPage") local ErrorPage = Roact.Component:extend("ErrorPage")

View File

@@ -27,8 +27,8 @@ local function AddressEntry(props)
}, { }, {
Host = e("TextBox", { Host = e("TextBox", {
Text = props.host or "", Text = props.host or "",
FontFace = theme.Font.Code, Font = Enum.Font.Code,
TextSize = theme.TextSize.Large, TextSize = 18,
TextColor3 = theme.AddressEntry.TextColor, TextColor3 = theme.AddressEntry.TextColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
@@ -51,8 +51,8 @@ local function AddressEntry(props)
Port = e("TextBox", { Port = e("TextBox", {
Text = props.port or "", Text = props.port or "",
FontFace = theme.Font.Code, Font = Enum.Font.Code,
TextSize = theme.TextSize.Large, TextSize = 18,
TextColor3 = theme.AddressEntry.TextColor, TextColor3 = theme.AddressEntry.TextColor,
TextTransparency = props.transparency, TextTransparency = props.transparency,
PlaceholderText = Config.defaultPort, PlaceholderText = Config.defaultPort,

View File

@@ -1,3 +1,5 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages local Packages = Rojo.Packages
@@ -7,55 +9,24 @@ local Roact = require(Packages.Roact)
local Settings = require(Plugin.Settings) local Settings = require(Plugin.Settings)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local Checkbox = require(Plugin.App.Components.Checkbox) local Checkbox = require(Plugin.App.Components.Checkbox)
local Dropdown = require(Plugin.App.Components.Dropdown) local Dropdown = require(Plugin.App.Components.Dropdown)
local IconButton = require(Plugin.App.Components.IconButton) local IconButton = require(Plugin.App.Components.IconButton)
local Tag = require(Plugin.App.Components.Tag)
local e = Roact.createElement local e = Roact.createElement
local DIVIDER_FADE_SIZE = 0.1 local DIVIDER_FADE_SIZE = 0.1
local TAG_TYPES = {
unstable = {
text = "UNSTABLE",
icon = Assets.Images.Icons.Warning,
color = { "Settings", "Setting", "UnstableColor" },
},
debug = {
text = "DEBUG",
icon = Assets.Images.Icons.Debug,
color = { "Settings", "Setting", "DebugColor" },
},
}
local function getTextBoundsWithLineHeight( local function getTextBounds(text, textSize, font, lineHeight, bounds)
text: string, local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
font: Font,
textSize: number,
width: number,
lineHeight: number
)
local textBounds = getTextBoundsAsync(text, font, textSize, width)
local lineCount = math.ceil(textBounds.Y / textSize) local lineCount = textBounds.Y / textSize
local lineHeightAbsolute = textSize * lineHeight local lineHeightAbsolute = textSize * lineHeight
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize)) return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
end end
local function getThemeColorFromPath(theme, path)
local color = theme
for _, key in path do
if color[key] == nil then
return theme.BrandColor
end
color = color[key]
end
return color
end
local Setting = Roact.Component:extend("Setting") local Setting = Roact.Component:extend("Setting")
function Setting:init() function Setting:init()
@@ -80,11 +51,11 @@ end
function Setting:render() function Setting:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local settingsTheme = theme.Settings theme = theme.Settings
return e("Frame", { return e("Frame", {
Size = self.contentSize:map(function(value) Size = self.contentSize:map(function(value)
return UDim2.new(1, 0, 0, value.Y + 20) return UDim2.new(1, 0, 0, 20 + value.Y + 20)
end), end),
LayoutOrder = self.props.layoutOrder, LayoutOrder = self.props.layoutOrder,
ZIndex = -self.props.layoutOrder, ZIndex = -self.props.layoutOrder,
@@ -114,7 +85,6 @@ function Setting:render()
then self.props.input then self.props.input
elseif self.props.options ~= nil then e(Dropdown, { elseif self.props.options ~= nil then e(Dropdown, {
locked = self.props.locked, locked = self.props.locked,
lockedTooltip = self.props.lockedTooltip,
options = self.props.options, options = self.props.options,
active = self.state.setting, active = self.state.setting,
transparency = self.props.transparency, transparency = self.props.transparency,
@@ -124,7 +94,6 @@ function Setting:render()
}) })
else e(Checkbox, { else e(Checkbox, {
locked = self.props.locked, locked = self.props.locked,
lockedTooltip = self.props.lockedTooltip,
active = self.state.setting, active = self.state.setting,
transparency = self.props.transparency, transparency = self.props.transparency,
onClick = function() onClick = function()
@@ -137,7 +106,7 @@ function Setting:render()
then e(IconButton, { then e(IconButton, {
icon = Assets.Images.Icons.Reset, icon = Assets.Images.Icons.Reset,
iconSize = 24, iconSize = 24,
color = settingsTheme.BackButtonColor, color = theme.BackButtonColor,
transparency = self.props.transparency, transparency = self.props.transparency,
visible = self.props.showReset, visible = self.props.showReset,
layoutOrder = -1, layoutOrder = -1,
@@ -151,49 +120,29 @@ function Setting:render()
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, { }, {
Heading = e("Frame", { Name = e("TextLabel", {
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium), Text = (if self.props.experimental then '<font color="#FF8E3C">⚠ </font>' else "")
.. self.props.name,
Font = Enum.Font.GothamBold,
TextSize = 17,
TextColor3 = theme.Setting.NameColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
RichText = true,
Size = UDim2.new(1, 0, 0, 17),
LayoutOrder = 1,
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, {
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5),
}),
Tag = if self.props.tag and TAG_TYPES[self.props.tag]
then e(Tag, {
layoutOrder = 1,
transparency = self.props.transparency,
text = TAG_TYPES[self.props.tag].text,
icon = TAG_TYPES[self.props.tag].icon,
color = getThemeColorFromPath(theme, TAG_TYPES[self.props.tag].color),
})
else nil,
Name = e("TextLabel", {
Text = self.props.name,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Medium,
TextColor3 = if self.props.tag and TAG_TYPES[self.props.tag]
then getThemeColorFromPath(theme, TAG_TYPES[self.props.tag].color)
else settingsTheme.Setting.NameColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
RichText = true,
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
LayoutOrder = 2,
BackgroundTransparency = 1,
}),
}), }),
Description = e("TextLabel", { Description = e("TextLabel", {
Text = self.props.description, Text = (if self.props.experimental then '<font color="#FF8E3C">[Experimental] </font>' else "")
FontFace = theme.Font.Main, .. self.props.description,
Font = Enum.Font.Gotham,
LineHeight = 1.2, LineHeight = 1.2,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = settingsTheme.Setting.DescriptionColor, TextColor3 = theme.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
TextWrapped = true, TextWrapped = true,
@@ -203,18 +152,20 @@ function Setting:render()
containerSize = self.containerSize, containerSize = self.containerSize,
inputSize = self.inputSize, inputSize = self.inputSize,
}):map(function(values) }):map(function(values)
local desc = (if self.props.experimental then "[Experimental] " else "")
.. self.props.description
local offset = values.inputSize.X + 5 local offset = values.inputSize.X + 5
local textBounds = getTextBoundsWithLineHeight( local textBounds = getTextBounds(
self.props.description, desc,
theme.Font.Main, 14,
theme.TextSize.Body, Enum.Font.Gotham,
values.containerSize.X - offset, 1.2,
1.2 Vector2.new(values.containerSize.X - offset, math.huge)
) )
return UDim2.new(1, -offset, 0, textBounds.Y) return UDim2.new(1, -offset, 0, textBounds.Y)
end), end),
LayoutOrder = 3, LayoutOrder = 2,
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
@@ -222,16 +173,21 @@ function Setting:render()
VerticalAlignment = Enum.VerticalAlignment.Center, VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical, FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder, SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5), Padding = UDim.new(0, 6),
[Roact.Change.AbsoluteContentSize] = function(object) [Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize) self.setContentSize(object.AbsoluteContentSize)
end, end,
}), }),
Padding = e("UIPadding", {
PaddingTop = UDim.new(0, 20),
PaddingBottom = UDim.new(0, 20),
}),
}), }),
Divider = e("Frame", { Divider = e("Frame", {
BackgroundColor3 = settingsTheme.DividerColor, BackgroundColor3 = theme.DividerColor,
BackgroundTransparency = self.props.transparency, BackgroundTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 1), Size = UDim2.new(1, 0, 0, 1),
BorderSizePixel = 0, BorderSizePixel = 0,

View File

@@ -30,7 +30,7 @@ local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted
local function Navbar(props) local function Navbar(props)
return Theme.with(function(theme) return Theme.with(function(theme)
local navbarTheme = theme.Settings.Navbar theme = theme.Settings.Navbar
return e("Frame", { return e("Frame", {
Size = UDim2.new(1, 0, 0, 46), Size = UDim2.new(1, 0, 0, 46),
@@ -40,7 +40,7 @@ local function Navbar(props)
Back = e(IconButton, { Back = e(IconButton, {
icon = Assets.Images.Icons.Back, icon = Assets.Images.Icons.Back,
iconSize = 24, iconSize = 24,
color = navbarTheme.BackButtonColor, color = theme.BackButtonColor,
transparency = props.transparency, transparency = props.transparency,
position = UDim2.new(0, 0, 0.5, 0), position = UDim2.new(0, 0, 0.5, 0),
@@ -55,9 +55,9 @@ local function Navbar(props)
Text = e("TextLabel", { Text = e("TextLabel", {
Text = "Settings", Text = "Settings",
FontFace = theme.Font.Thin, Font = Enum.Font.Gotham,
TextSize = theme.TextSize.Large, TextSize = 18,
TextColor3 = navbarTheme.TextColor, TextColor3 = theme.TextColor,
TextTransparency = props.transparency, TextTransparency = props.transparency,
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
@@ -75,30 +75,26 @@ function SettingsPage:init()
end end
function SettingsPage:render() function SettingsPage:render()
local layoutOrder = 0 return Theme.with(function(theme)
local function layoutIncrement() theme = theme.Settings
layoutOrder += 1
return layoutOrder
end
return Roact.createFragment({ return e(ScrollingFrame, {
Navbar = e(Navbar, { size = UDim2.new(1, 0, 1, 0),
onBack = self.props.onBack,
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
Content = e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, -47),
position = UDim2.new(0, 0, 0, 47),
contentSize = self.contentSize, contentSize = self.contentSize,
transparency = self.props.transparency, transparency = self.props.transparency,
}, { }, {
Navbar = e(Navbar, {
onBack = self.props.onBack,
transparency = self.props.transparency,
layoutOrder = 0,
}),
ShowNotifications = e(Setting, { ShowNotifications = e(Setting, {
id = "showNotifications", id = "showNotifications",
name = "Show Notifications", name = "Show Notifications",
description = "Popup notifications in viewport", description = "Popup notifications in viewport",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = 1,
}), }),
SyncReminder = e(Setting, { SyncReminder = e(Setting, {
@@ -107,7 +103,7 @@ function SettingsPage:render()
description = "Notify to sync when opening a place that has previously been synced", description = "Notify to sync when opening a place that has previously been synced",
transparency = self.props.transparency, transparency = self.props.transparency,
visible = Settings:getBinding("showNotifications"), visible = Settings:getBinding("showNotifications"),
layoutOrder = layoutIncrement(), layoutOrder = 2,
}), }),
ConfirmationBehavior = e(Setting, { ConfirmationBehavior = e(Setting, {
@@ -115,7 +111,7 @@ function SettingsPage:render()
name = "Confirmation Behavior", name = "Confirmation Behavior",
description = "When to prompt for confirmation before syncing", description = "When to prompt for confirmation before syncing",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = 3,
options = confirmationBehaviors, options = confirmationBehaviors,
}), }),
@@ -125,7 +121,7 @@ function SettingsPage:render()
name = "Confirmation Threshold", name = "Confirmation Threshold",
description = "How many modified instances to be considered a large change", description = "How many modified instances to be considered a large change",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = 4,
visible = Settings:getBinding("confirmationBehavior"):map(function(value) visible = Settings:getBinding("confirmationBehavior"):map(function(value)
return value == "Large Changes" return value == "Large Changes"
end), end),
@@ -156,44 +152,17 @@ function SettingsPage:render()
name = "Play Sounds", name = "Play Sounds",
description = "Toggle sound effects", description = "Toggle sound effects",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = 5,
}),
CheckForUpdates = e(Setting, {
id = "checkForUpdates",
name = "Check For Updates",
description = "Notify about newer compatible Rojo releases",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
CheckForPreleases = e(Setting, {
id = "checkForPrereleases",
name = "Include Prerelease Updates",
description = "Include prereleases when checking for updates",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
visible = if string.find(debug.traceback(), "\n[^\n]-user_.-$") == nil
then false -- Must be a local install to allow prerelease checks
else Settings:getBinding("checkForUpdates"),
}),
AutoConnectPlaytestServer = e(Setting, {
id = "autoConnectPlaytestServer",
name = "Auto Connect Playtest Server",
description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
tag = "unstable",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}), }),
OpenScriptsExternally = e(Setting, { OpenScriptsExternally = e(Setting, {
id = "openScriptsExternally", id = "openScriptsExternally",
name = "Open Scripts Externally", name = "Open Scripts Externally",
description = "Attempt to open scripts in an external editor", description = "Attempt to open scripts in an external editor",
tag = "unstable", locked = self.props.syncActive,
experimental = true,
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = 6,
}), }),
TwoWaySync = e(Setting, { TwoWaySync = e(Setting, {
@@ -201,19 +170,17 @@ function SettingsPage:render()
name = "Two-Way Sync", name = "Two-Way Sync",
description = "Editing files in Studio will sync them into the filesystem", description = "Editing files in Studio will sync them into the filesystem",
locked = self.props.syncActive, locked = self.props.syncActive,
lockedTooltip = "(Cannot change while currently syncing. Disconnect first.)", experimental = true,
tag = "unstable",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = 7,
}), }),
LogLevel = e(Setting, { LogLevel = e(Setting, {
id = "logLevel", id = "logLevel",
name = "Log Level", name = "Log Level",
description = "Plugin output verbosity level", description = "Plugin output verbosity level",
tag = "debug",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = 100,
options = invertedLevels, options = invertedLevels,
showReset = Settings:getBinding("logLevel"):map(function(value) showReset = Settings:getBinding("logLevel"):map(function(value)
@@ -228,18 +195,8 @@ function SettingsPage:render()
id = "typecheckingEnabled", id = "typecheckingEnabled",
name = "Typechecking", name = "Typechecking",
description = "Toggle typechecking on the API surface", description = "Toggle typechecking on the API surface",
tag = "debug",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = layoutIncrement(), layoutOrder = 101,
}),
TimingLogsEnabled = e(Setting, {
id = "timingLogsEnabled",
name = "Timing Logs",
description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
tag = "debug",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}), }),
Layout = e("UIListLayout", { Layout = e("UIListLayout", {
@@ -255,8 +212,8 @@ function SettingsPage:render()
PaddingLeft = UDim.new(0, 20), PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20), PaddingRight = UDim.new(0, 20),
}), }),
}), })
}) end)
end end
return SettingsPage return SettingsPage

View File

@@ -1,6 +1,7 @@
--[[ --[[
Theming system provided through Roact's context. Theming system taking advantage of Roact's new context API.
Uses Studio colors when possible. 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 -- Studio does not exist outside Roblox Studio, so we'll lazily initialize it
@@ -14,8 +15,6 @@ local function getStudio()
return _Studio return _Studio
end end
local ContentProvider = game:GetService("ContentProvider")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages local Packages = Rojo.Packages
@@ -25,7 +24,227 @@ local strict = require(script.Parent.Parent.strict)
local BRAND_COLOR = Color3.fromHex("E13835") local BRAND_COLOR = Color3.fromHex("E13835")
local Context = Roact.createContext({}) local lightTheme = strict("LightTheme", {
BackgroundColor = Color3.fromHex("FFFFFF"),
Button = {
Solid = {
ActionFillColor = Color3.fromHex("FFFFFF"),
ActionFillTransparency = 0.8,
Enabled = {
TextColor = Color3.fromHex("FFFFFF"),
BackgroundColor = BRAND_COLOR,
},
Disabled = {
TextColor = Color3.fromHex("FFFFFF"),
BackgroundColor = BRAND_COLOR,
},
},
Bordered = {
ActionFillColor = Color3.fromHex("000000"),
ActionFillTransparency = 0.9,
Enabled = {
TextColor = Color3.fromHex("393939"),
BorderColor = Color3.fromHex("ACACAC"),
},
Disabled = {
TextColor = Color3.fromHex("393939"),
BorderColor = Color3.fromHex("ACACAC"),
},
},
},
Checkbox = {
Active = {
IconColor = Color3.fromHex("FFFFFF"),
BackgroundColor = BRAND_COLOR,
},
Inactive = {
IconColor = Color3.fromHex("EEEEEE"),
BorderColor = Color3.fromHex("AFAFAF"),
},
},
Dropdown = {
TextColor = Color3.fromHex("000000"),
BorderColor = Color3.fromHex("AFAFAF"),
BackgroundColor = Color3.fromHex("EEEEEE"),
Open = {
IconColor = BRAND_COLOR,
},
Closed = {
IconColor = Color3.fromHex("EEEEEE"),
},
},
TextInput = {
Enabled = {
TextColor = Color3.fromHex("000000"),
PlaceholderColor = Color3.fromHex("8C8C8C"),
BorderColor = Color3.fromHex("ACACAC"),
},
Disabled = {
TextColor = Color3.fromHex("393939"),
PlaceholderColor = Color3.fromHex("8C8C8C"),
BorderColor = Color3.fromHex("AFAFAF"),
},
ActionFillColor = Color3.fromHex("000000"),
ActionFillTransparency = 0.9,
},
AddressEntry = {
TextColor = Color3.fromHex("000000"),
PlaceholderColor = Color3.fromHex("8C8C8C"),
},
BorderedContainer = {
BorderColor = Color3.fromHex("CBCBCB"),
BackgroundColor = Color3.fromHex("EEEEEE"),
},
Spinner = {
ForegroundColor = BRAND_COLOR,
BackgroundColor = Color3.fromHex("EEEEEE"),
},
Diff = {
Add = Color3.fromHex("baffbd"),
Remove = Color3.fromHex("ffbdba"),
Edit = Color3.fromHex("bacdff"),
Row = Color3.fromHex("000000"),
Warning = Color3.fromHex("FF8E3C"),
},
ConnectionDetails = {
ProjectNameColor = Color3.fromHex("000000"),
AddressColor = Color3.fromHex("000000"),
DisconnectColor = BRAND_COLOR,
},
Settings = {
DividerColor = Color3.fromHex("CBCBCB"),
Navbar = {
BackButtonColor = Color3.fromHex("000000"),
TextColor = Color3.fromHex("000000"),
},
Setting = {
NameColor = Color3.fromHex("000000"),
DescriptionColor = Color3.fromHex("5F5F5F"),
},
},
Header = {
LogoColor = BRAND_COLOR,
VersionColor = Color3.fromHex("727272"),
},
Notification = {
InfoColor = Color3.fromHex("000000"),
CloseColor = BRAND_COLOR,
},
ErrorColor = Color3.fromHex("000000"),
ScrollBarColor = Color3.fromHex("000000"),
})
local darkTheme = strict("DarkTheme", {
BackgroundColor = Color3.fromHex("2E2E2E"),
Button = {
Solid = {
ActionFillColor = Color3.fromHex("FFFFFF"),
ActionFillTransparency = 0.8,
Enabled = {
TextColor = Color3.fromHex("FFFFFF"),
BackgroundColor = BRAND_COLOR,
},
Disabled = {
TextColor = Color3.fromHex("FFFFFF"),
BackgroundColor = BRAND_COLOR,
},
},
Bordered = {
ActionFillColor = Color3.fromHex("FFFFFF"),
ActionFillTransparency = 0.9,
Enabled = {
TextColor = Color3.fromHex("DBDBDB"),
BorderColor = Color3.fromHex("535353"),
},
Disabled = {
TextColor = Color3.fromHex("DBDBDB"),
BorderColor = Color3.fromHex("535353"),
},
},
},
Checkbox = {
Active = {
IconColor = Color3.fromHex("FFFFFF"),
BackgroundColor = BRAND_COLOR,
},
Inactive = {
IconColor = Color3.fromHex("484848"),
BorderColor = Color3.fromHex("5A5A5A"),
},
},
Dropdown = {
TextColor = Color3.fromHex("FFFFFF"),
BorderColor = Color3.fromHex("5A5A5A"),
BackgroundColor = Color3.fromHex("2B2B2B"),
Open = {
IconColor = BRAND_COLOR,
},
Closed = {
IconColor = Color3.fromHex("484848"),
},
},
TextInput = {
Enabled = {
TextColor = Color3.fromHex("FFFFFF"),
PlaceholderColor = Color3.fromHex("8B8B8B"),
BorderColor = Color3.fromHex("535353"),
},
Disabled = {
TextColor = Color3.fromHex("484848"),
PlaceholderColor = Color3.fromHex("8B8B8B"),
BorderColor = Color3.fromHex("5A5A5A"),
},
ActionFillColor = Color3.fromHex("FFFFFF"),
ActionFillTransparency = 0.9,
},
AddressEntry = {
TextColor = Color3.fromHex("FFFFFF"),
PlaceholderColor = Color3.fromHex("8B8B8B"),
},
BorderedContainer = {
BorderColor = Color3.fromHex("535353"),
BackgroundColor = Color3.fromHex("2B2B2B"),
},
Spinner = {
ForegroundColor = BRAND_COLOR,
BackgroundColor = Color3.fromHex("2B2B2B"),
},
Diff = {
Add = Color3.fromHex("273732"),
Remove = Color3.fromHex("3F2D32"),
Edit = Color3.fromHex("193345"),
Row = Color3.fromHex("FFFFFF"),
Warning = Color3.fromHex("FF8E3C"),
},
ConnectionDetails = {
ProjectNameColor = Color3.fromHex("FFFFFF"),
AddressColor = Color3.fromHex("FFFFFF"),
DisconnectColor = Color3.fromHex("FFFFFF"),
},
Settings = {
DividerColor = Color3.fromHex("535353"),
Navbar = {
BackButtonColor = Color3.fromHex("FFFFFF"),
TextColor = Color3.fromHex("FFFFFF"),
},
Setting = {
NameColor = Color3.fromHex("FFFFFF"),
DescriptionColor = Color3.fromHex("D3D3D3"),
},
},
Header = {
LogoColor = BRAND_COLOR,
VersionColor = Color3.fromHex("D3D3D3"),
},
Notification = {
InfoColor = Color3.fromHex("FFFFFF"),
CloseColor = Color3.fromHex("FFFFFF"),
},
ErrorColor = Color3.fromHex("FFFFFF"),
ScrollBarColor = Color3.fromHex("FFFFFF"),
})
local Context = Roact.createContext(lightTheme)
local StudioProvider = Roact.Component:extend("StudioProvider") local StudioProvider = Roact.Component:extend("StudioProvider")
@@ -33,192 +252,25 @@ local StudioProvider = Roact.Component:extend("StudioProvider")
function StudioProvider:updateTheme() function StudioProvider:updateTheme()
local studioTheme = getStudio().Theme local studioTheme = getStudio().Theme
local isDark = studioTheme.Name == "Dark" 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)
local theme = strict(studioTheme.Name .. "Theme", { self:setState({
Font = { theme = lightTheme,
Main = Font.new("rbxasset://fonts/families/Montserrat.json", Enum.FontWeight.Medium, Enum.FontStyle.Normal), })
Bold = Font.new("rbxasset://fonts/families/Montserrat.json", Enum.FontWeight.Bold, Enum.FontStyle.Normal), end
Thin = Font.new(
"rbxasset://fonts/families/Montserrat.json",
Enum.FontWeight.Regular,
Enum.FontStyle.Normal
),
Code = Font.new(
"rbxasset://fonts/families/Inconsolata.json",
Enum.FontWeight.Regular,
Enum.FontStyle.Normal
),
},
TextSize = {
Body = 15,
Small = 13,
Medium = 16,
Large = 18,
Code = 16,
},
BrandColor = BRAND_COLOR,
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
SubTextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
Button = {
Solid = {
-- Solid uses brand theming, not Studio theming.
ActionFillColor = Color3.fromHex("FFFFFF"),
ActionFillTransparency = 0.8,
Enabled = {
TextColor = Color3.fromHex("FFFFFF"),
BackgroundColor = BRAND_COLOR,
},
Disabled = {
TextColor = Color3.fromHex("FFFFFF"),
BackgroundColor = BRAND_COLOR,
},
},
Bordered = {
ActionFillColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.ButtonText,
Enum.StudioStyleGuideModifier.Selected
),
ActionFillTransparency = 0.9,
Enabled = {
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.ButtonText),
BorderColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldBorder,
Enum.StudioStyleGuideModifier.Disabled
),
},
Disabled = {
TextColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.ButtonText,
Enum.StudioStyleGuideModifier.Disabled
),
BorderColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldBorder,
Enum.StudioStyleGuideModifier.Disabled
),
},
},
},
Checkbox = {
Active = {
-- Active checkboxes use brand theming, not Studio theming.
IconColor = Color3.fromHex("FFFFFF"),
BackgroundColor = BRAND_COLOR,
},
Inactive = {
IconColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldIndicator,
Enum.StudioStyleGuideModifier.Disabled
),
BorderColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldBorder,
Enum.StudioStyleGuideModifier.Disabled
),
},
},
Dropdown = {
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.ButtonText),
BorderColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldBorder,
Enum.StudioStyleGuideModifier.Disabled
),
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
IconColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldIndicator,
Enum.StudioStyleGuideModifier.Disabled
),
},
TextInput = {
Enabled = {
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
BorderColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldBorder,
Enum.StudioStyleGuideModifier.Disabled
),
},
Disabled = {
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
BorderColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldBorder,
Enum.StudioStyleGuideModifier.Disabled
),
},
ActionFillColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
ActionFillTransparency = 0.9,
},
AddressEntry = {
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
},
BorderedContainer = {
BorderColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldBorder,
Enum.StudioStyleGuideModifier.Disabled
),
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
},
Spinner = {
ForegroundColor = BRAND_COLOR,
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
},
Diff = {
-- Studio doesn't have good colors since their diffs use backgrounds, not text
Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45),
Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29),
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
},
ConnectionDetails = {
ProjectNameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
AddressColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
DisconnectColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
},
Settings = {
DividerColor = studioTheme:GetColor(
Enum.StudioStyleGuideColor.CheckedFieldBorder,
Enum.StudioStyleGuideModifier.Disabled
),
Navbar = {
BackButtonColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
},
Setting = {
NameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
DescriptionColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
UnstableColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
DebugColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InfoText),
},
},
Header = {
LogoColor = BRAND_COLOR,
VersionColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
},
Notification = {
InfoColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
CloseColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
},
ErrorColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
ScrollBarColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
})
self:setState({
theme = theme,
})
end end
function StudioProvider:init() function StudioProvider:init()
self:updateTheme() self:updateTheme()
-- Preload the Fonts so that getTextBoundsAsync won't yield
local fontAssetIds = {}
for _, font in self.state.theme.Font do
table.insert(fontAssetIds, font.Family)
end
pcall(ContentProvider.PreloadAsync, ContentProvider, fontAssetIds)
end end
function StudioProvider:render() function StudioProvider:render()

View File

@@ -1,41 +0,0 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Log = require(Packages.Log)
local params = Instance.new("GetTextBoundsParams")
local function getTextBoundsAsync(
text: string,
font: Font,
textSize: number,
width: number,
richText: boolean?
): Vector2
if type(text) ~= "string" then
Log.warn(`Invalid text. Expected string, received {type(text)} instead`)
return Vector2.zero
end
if #text >= 200_000 then
Log.warn(`Invalid text. Exceeds the 199,999 character limit`)
return Vector2.zero
end
params.Text = text
params.Font = font
params.Size = textSize
params.Width = width
params.RichText = not not richText
local success, bounds = pcall(TextService.GetTextBoundsAsync, TextService, params)
if not success then
Log.warn(`Failed to get text bounds: {bounds}`)
return Vector2.zero
end
return bounds
end
return getTextBoundsAsync

View File

@@ -23,7 +23,6 @@ local PatchTree = require(Plugin.PatchTree)
local preloadAssets = require(Plugin.preloadAssets) local preloadAssets = require(Plugin.preloadAssets)
local soundPlayer = require(Plugin.soundPlayer) local soundPlayer = require(Plugin.soundPlayer)
local ignorePlaceIds = require(Plugin.ignorePlaceIds) local ignorePlaceIds = require(Plugin.ignorePlaceIds)
local timeUtil = require(Plugin.timeUtil)
local Theme = require(script.Theme) local Theme = require(script.Theme)
local Page = require(script.Page) local Page = require(script.Page)
@@ -52,9 +51,9 @@ local App = Roact.Component:extend("App")
function App:init() function App:init()
preloadAssets() preloadAssets()
local priorSyncInfo = self:getPriorSyncInfo() local priorHost, priorPort = self:getPriorEndpoint()
self.host, self.setHost = Roact.createBinding(priorSyncInfo.host or "") self.host, self.setHost = Roact.createBinding(priorHost or "")
self.port, self.setPort = Roact.createBinding(priorSyncInfo.port or "") self.port, self.setPort = Roact.createBinding(priorPort or "")
self.confirmationBindable = Instance.new("BindableEvent") self.confirmationBindable = Instance.new("BindableEvent")
self.confirmationEvent = self.confirmationBindable.Event self.confirmationEvent = self.confirmationBindable.Event
@@ -119,13 +118,6 @@ function App:init()
end) end)
end) end)
self.disconnectUpdatesCheckChanged = Settings:onChanged("checkForUpdates", function()
self:checkForUpdates()
end)
self.disconnectPrereleasesCheckChanged = Settings:onChanged("checkForPrereleases", function()
self:checkForUpdates()
end)
self:setState({ self:setState({
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
guiEnabled = false, guiEnabled = false,
@@ -139,73 +131,38 @@ function App:init()
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
}) })
if RunService:IsEdit() then if
self:checkForUpdates() RunService:IsEdit()
and self.serveSession == nil
if and Settings:get("syncReminder")
Settings:get("syncReminder") and self:getLastSyncTimestamp()
and self.serveSession == nil and (self:isSyncLockAvailable())
and self:getPriorSyncInfo().timestamp ~= nil then
and (self:isSyncLockAvailable()) self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, {
then Connect = {
local syncInfo = self:getPriorSyncInfo() text = "Connect",
local timeSinceSync = timeUtil.elapsedToText(os.time() - syncInfo.timestamp) style = "Solid",
local syncDetail = if syncInfo.projectName layoutOrder = 1,
then `project '{syncInfo.projectName}'` onClick = function(notification)
else `{syncInfo.host or Config.defaultHost}:{syncInfo.port or Config.defaultPort}` notification:dismiss()
self:startSession()
self:addNotification( end,
`You synced {syncDetail} to this place {timeSinceSync}. Would you like to reconnect?`, },
300, Dismiss = {
{ text = "Dismiss",
Connect = { style = "Bordered",
text = "Connect", layoutOrder = 2,
style = "Solid", onClick = function(notification)
layoutOrder = 1, notification:dismiss()
onClick = function(notification) end,
notification:dismiss() },
self:startSession() })
end,
},
Dismiss = {
text = "Dismiss",
style = "Bordered",
layoutOrder = 2,
onClick = function(notification)
notification:dismiss()
end,
},
}
)
end
end end
if self:isAutoConnectPlaytestServerAvailable() then
self:useRunningConnectionInfo()
self:startSession()
end
self.autoConnectPlaytestServerListener = Settings:onChanged("autoConnectPlaytestServer", function(enabled)
if enabled then
if self:isAutoConnectPlaytestServerWriteable() and self.serveSession ~= nil then
-- Write the existing session
local baseUrl = self.serveSession.__apiContext.__baseUrl
self:setRunningConnectionInfo(baseUrl)
end
else
self:clearRunningConnectionInfo()
end
end)
end end
function App:willUnmount() function App:willUnmount()
self.waypointConnection:Disconnect() self.waypointConnection:Disconnect()
self.confirmationBindable:Destroy() self.confirmationBindable:Destroy()
self.disconnectUpdatesCheckChanged()
self.disconnectPrereleasesCheckChanged()
self.autoConnectPlaytestServerListener()
self:clearRunningConnectionInfo()
end end
function App:addNotification( function App:addNotification(
@@ -250,66 +207,54 @@ function App:closeNotification(id: number)
}) })
end end
function App:checkForUpdates() function App:getPriorEndpoint()
if not Settings:get("checkForUpdates") then local priorEndpoints = Settings:get("priorEndpoints")
if not priorEndpoints then
return return
end end
local isLocalInstall = string.find(debug.traceback(), "\n[^\n]-user_.-$") ~= nil
local latestCompatibleVersion = Version.retrieveLatestCompatible({
version = Config.version,
includePrereleases = isLocalInstall and Settings:get("checkForPrereleases"),
})
if not latestCompatibleVersion then
return
end
self:addNotification(
string.format(
"A newer compatible version of Rojo, %s, was published %s! Go to the Rojo releases page to learn more.",
Version.display(latestCompatibleVersion.version),
timeUtil.elapsedToText(DateTime.now().UnixTimestamp - latestCompatibleVersion.publishedUnixTimestamp)
),
500,
{
Dismiss = {
text = "Dismiss",
style = "Bordered",
layoutOrder = 2,
onClick = function(notification)
notification:dismiss()
end,
},
}
)
end
function App:getPriorSyncInfo(): { host: string?, port: string?, projectName: string?, timestamp: number? }
local priorSyncInfos = Settings:get("priorEndpoints")
if not priorSyncInfos then
return {}
end
local id = tostring(game.PlaceId) local id = tostring(game.PlaceId)
if ignorePlaceIds[id] then if ignorePlaceIds[id] then
return {} return
end end
return priorSyncInfos[id] or {} local place = priorEndpoints[id]
if not place then
return
end
return place.host, place.port
end end
function App:setPriorSyncInfo(host: string, port: string, projectName: string) function App:getLastSyncTimestamp()
local priorSyncInfos = Settings:get("priorEndpoints") local priorEndpoints = Settings:get("priorEndpoints")
if not priorSyncInfos then if not priorEndpoints then
priorSyncInfos = {} return
end end
local now = os.time() local id = tostring(game.PlaceId)
if ignorePlaceIds[id] then
return
end
local place = priorEndpoints[id]
if not place then
return
end
return place.timestamp
end
function App:setPriorEndpoint(host: string, port: string)
local priorEndpoints = Settings:get("priorEndpoints")
if not priorEndpoints then
priorEndpoints = {}
end
-- Clear any stale saves to avoid disc bloat -- Clear any stale saves to avoid disc bloat
for placeId, syncInfo in priorSyncInfos do for placeId, endpoint in priorEndpoints do
if now - (syncInfo.timestamp or now) > 12_960_000 then if os.time() - endpoint.timestamp > 12_960_000 then
priorSyncInfos[placeId] = nil priorEndpoints[placeId] = nil
Log.trace("Cleared stale saved endpoint for {}", placeId) Log.trace("Cleared stale saved endpoint for {}", placeId)
end end
end end
@@ -319,22 +264,24 @@ function App:setPriorSyncInfo(host: string, port: string, projectName: string)
return return
end end
priorSyncInfos[id] = { priorEndpoints[id] = {
host = if host ~= Config.defaultHost then host else nil, host = if host ~= Config.defaultHost then host else nil,
port = if port ~= Config.defaultPort then port else nil, port = if port ~= Config.defaultPort then port else nil,
projectName = projectName, timestamp = os.time(),
timestamp = now,
} }
Log.trace("Saved last used endpoint for {}", game.PlaceId) Log.trace("Saved last used endpoint for {}", game.PlaceId)
Settings:set("priorEndpoints", priorSyncInfos) Settings:set("priorEndpoints", priorEndpoints)
end end
function App:getHostAndPort() function App:getHostAndPort()
local host = self.host:getValue() local host = self.host:getValue()
local port = self.port:getValue() local port = self.port:getValue()
return if #host > 0 then host else Config.defaultHost, if #port > 0 then port else Config.defaultPort local host = if #host > 0 then host else Config.defaultHost
local port = if #port > 0 then port else Config.defaultPort
return host, port
end end
function App:isSyncLockAvailable() function App:isSyncLockAvailable()
@@ -402,49 +349,6 @@ function App:releaseSyncLock()
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value) Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
end end
function App:isAutoConnectPlaytestServerAvailable()
return RunService:IsRunMode()
and RunService:IsServer()
and Settings:get("autoConnectPlaytestServer")
and workspace:GetAttribute("__Rojo_ConnectionUrl")
end
function App:isAutoConnectPlaytestServerWriteable()
return RunService:IsEdit() and Settings:get("autoConnectPlaytestServer")
end
function App:setRunningConnectionInfo(baseUrl: string)
if not self:isAutoConnectPlaytestServerWriteable() then
return
end
Log.trace("Setting connection info for play solo auto-connect")
workspace:SetAttribute("__Rojo_ConnectionUrl", baseUrl)
end
function App:clearRunningConnectionInfo()
if not RunService:IsEdit() then
-- Only write connection info from edit mode
return
end
Log.trace("Clearing connection info for play solo auto-connect")
workspace:SetAttribute("__Rojo_ConnectionUrl", nil)
end
function App:useRunningConnectionInfo()
local connectionInfo = workspace:GetAttribute("__Rojo_ConnectionUrl")
if not connectionInfo then
return
end
Log.trace("Using connection info for play solo auto-connect")
local host, port = string.match(connectionInfo, "^(.+):(.-)$")
self.setHost(host)
self.setPort(port)
end
function App:startSession() function App:startSession()
local claimedLock, priorOwner = self:claimSyncLock() local claimedLock, priorOwner = self:claimSyncLock()
if not claimedLock then if not claimedLock then
@@ -463,6 +367,11 @@ function App:startSession()
local host, port = self:getHostAndPort() local host, port = self:getHostAndPort()
local sessionOptions = {
openScriptsExternally = Settings:get("openScriptsExternally"),
twoWaySync = Settings:get("twoWaySync"),
}
local baseUrl = if string.find(host, "^https?://") local baseUrl = if string.find(host, "^https?://")
then string.format("%s:%s", host, port) then string.format("%s:%s", host, port)
else string.format("http://%s:%s", host, port) else string.format("http://%s:%s", host, port)
@@ -470,7 +379,8 @@ function App:startSession()
local serveSession = ServeSession.new({ local serveSession = ServeSession.new({
apiContext = apiContext, apiContext = apiContext,
twoWaySync = Settings:get("twoWaySync"), openScriptsExternally = sessionOptions.openScriptsExternally,
twoWaySync = sessionOptions.twoWaySync,
}) })
self.cleanupPrecommit = serveSession.__reconciler:hookPrecommit(function(patch, instanceMap) self.cleanupPrecommit = serveSession.__reconciler:hookPrecommit(function(patch, instanceMap)
@@ -489,7 +399,7 @@ function App:startSession()
end) end)
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied) serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
local now = DateTime.now().UnixTimestamp local now = os.time()
local old = self.state.patchData local old = self.state.patchData
if PatchSet.isEmpty(patch) then if PatchSet.isEmpty(patch) then
@@ -522,6 +432,8 @@ function App:startSession()
serveSession:onStatusChanged(function(status, details) serveSession:onStatusChanged(function(status, details)
if status == ServeSession.Status.Connecting then if status == ServeSession.Status.Connecting then
self:setPriorEndpoint(host, port)
self:setState({ self:setState({
appStatus = AppStatus.Connecting, appStatus = AppStatus.Connecting,
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
@@ -529,8 +441,6 @@ function App:startSession()
self:addNotification("Connecting to session...") self:addNotification("Connecting to session...")
elseif status == ServeSession.Status.Connected then elseif status == ServeSession.Status.Connected then
self.knownProjects[details] = true self.knownProjects[details] = true
self:setPriorSyncInfo(host, port, details)
self:setRunningConnectionInfo(baseUrl)
local address = ("%s:%s"):format(host, port) local address = ("%s:%s"):format(host, port)
self:setState({ self:setState({
@@ -543,7 +453,6 @@ function App:startSession()
elseif status == ServeSession.Status.Disconnected then elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil self.serveSession = nil
self:releaseSyncLock() self:releaseSyncLock()
self:clearRunningConnectionInfo()
self:setState({ self:setState({
patchData = { patchData = {
patch = PatchSet.newEmpty(), patch = PatchSet.newEmpty(),
@@ -579,12 +488,6 @@ function App:startSession()
return "Accept" return "Accept"
end end
-- Play solo auto-connect does not require confirmation
if self:isAutoConnectPlaytestServerAvailable() then
Log.trace("Accepting patch without confirmation because play solo auto-connect is enabled")
return "Accept"
end
local confirmationBehavior = Settings:get("confirmationBehavior") local confirmationBehavior = Settings:get("confirmationBehavior")
if confirmationBehavior == "Initial" then if confirmationBehavior == "Initial" then
-- Only confirm if we haven't synced this project yet this session -- Only confirm if we haven't synced this project yet this session
@@ -703,7 +606,7 @@ function App:render()
value = self.props.plugin, value = self.props.plugin,
}, { }, {
e(Theme.StudioProvider, nil, { e(Theme.StudioProvider, nil, {
tooltip = e(Tooltip.Provider, nil, { e(Tooltip.Provider, nil, {
gui = e(StudioPluginGui, { gui = e(StudioPluginGui, {
id = pluginName, id = pluginName,
title = pluginName, title = pluginName,

View File

@@ -25,12 +25,6 @@ local Assets = {
Back = "rbxassetid://6017213752", Back = "rbxassetid://6017213752",
Reset = "rbxassetid://10142422327", Reset = "rbxassetid://10142422327",
Expand = "rbxassetid://12045401097", Expand = "rbxassetid://12045401097",
Warning = "rbxassetid://16571019891",
Debug = "rbxassetid://16588411361",
Checkmark = "rbxassetid://16571012729",
Exclamation = "rbxassetid://16571172190",
SyncSuccess = "rbxassetid://16565035221",
SyncWarning = "rbxassetid://16565325171",
}, },
Diff = { Diff = {
Add = "rbxassetid://10434145835", Add = "rbxassetid://10434145835",

View File

@@ -112,12 +112,9 @@ end
function InstanceMap:destroyInstance(instance) function InstanceMap:destroyInstance(instance)
local id = self.fromInstances[instance] local id = self.fromInstances[instance]
local descendants = instance:GetDescendants()
-- Because the user might want to Undo this change, we cannot use Destroy local descendants = instance:GetDescendants()
-- since that locks that parent and prevents ChangeHistoryService from instance:Destroy()
-- ever bringing it back. Instead, we parent to nil.
instance.Parent = nil
-- After the instance is successfully destroyed, -- After the instance is successfully destroyed,
-- we can remove all the id mappings -- we can remove all the id mappings

View File

@@ -211,11 +211,9 @@ end
function PatchSet.countChanges(patch) function PatchSet.countChanges(patch)
local count = 0 local count = 0
for _, add in patch.added do for _ in patch.added do
-- Adding an instance is 1 change per property -- Adding an instance is 1 change
for _ in add.Properties do count += 1
count += 1
end
end end
for _ in patch.removed do for _ in patch.removed do
-- Removing an instance is 1 change -- Removing an instance is 1 change

View File

@@ -11,7 +11,6 @@ local Packages = Rojo.Packages
local Log = require(Packages.Log) local Log = require(Packages.Log)
local Timer = require(Plugin.Timer)
local Types = require(Plugin.Types) local Types = require(Plugin.Types)
local decodeValue = require(Plugin.Reconciler.decodeValue) local decodeValue = require(Plugin.Reconciler.decodeValue)
local getProperty = require(Plugin.Reconciler.getProperty) local getProperty = require(Plugin.Reconciler.getProperty)
@@ -79,15 +78,6 @@ function Tree.new()
return setmetatable(tree, Tree) return setmetatable(tree, Tree)
end end
-- Iterates over all nodes and counts them up
function Tree:getCount()
local count = 0
self:forEach(function()
count += 1
end)
return count
end
-- Iterates over all sub-nodes, depth first -- Iterates over all sub-nodes, depth first
-- node is where to start from, defaults to root -- node is where to start from, defaults to root
-- depth is used for recursion but can be used to set the starting depth -- depth is used for recursion but can be used to set the starting depth
@@ -132,7 +122,6 @@ end
-- props must contain id, and cannot contain children or parentId -- props must contain id, and cannot contain children or parentId
-- other than those three, it can hold anything -- other than those three, it can hold anything
function Tree:addNode(parent, props) function Tree:addNode(parent, props)
Timer.start("Tree:addNode")
assert(props.id, "props must contain id") assert(props.id, "props must contain id")
parent = parent or "ROOT" parent = parent or "ROOT"
@@ -143,7 +132,6 @@ function Tree:addNode(parent, props)
for k, v in props do for k, v in props do
node[k] = v node[k] = v
end end
Timer.stop()
return node return node
end end
@@ -154,21 +142,18 @@ function Tree:addNode(parent, props)
local parentNode = self:getNode(parent) local parentNode = self:getNode(parent)
if not parentNode then if not parentNode then
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props) Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
Timer.stop()
return return
end end
parentNode.children[node.id] = node parentNode.children[node.id] = node
self.idToNode[node.id] = node self.idToNode[node.id] = node
Timer.stop()
return node return node
end end
-- Given a list of ancestor ids in descending order, builds the nodes for them -- Given a list of ancestor ids in descending order, builds the nodes for them
-- using the patch and instanceMap info -- using the patch and instanceMap info
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap) function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
Timer.start("Tree:buildAncestryNodes")
-- Build nodes for ancestry by going up the tree -- Build nodes for ancestry by going up the tree
previousId = previousId or "ROOT" previousId = previousId or "ROOT"
@@ -186,8 +171,6 @@ function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, p
}) })
previousId = ancestorId previousId = ancestorId
end end
Timer.stop()
end end
local PatchTree = {} local PatchTree = {}
@@ -195,12 +178,10 @@ local PatchTree = {}
-- Builds a new tree from a patch and instanceMap -- Builds a new tree from a patch and instanceMap
-- uses changeListHeaders in node.changeList -- uses changeListHeaders in node.changeList
function PatchTree.build(patch, instanceMap, changeListHeaders) function PatchTree.build(patch, instanceMap, changeListHeaders)
Timer.start("PatchTree.build")
local tree = Tree.new() local tree = Tree.new()
local knownAncestors = {} local knownAncestors = {}
Timer.start("patch.updated")
for _, change in patch.updated do for _, change in patch.updated do
local instance = instanceMap.fromIds[change.id] local instance = instanceMap.fromIds[change.id]
if not instance then if not instance then
@@ -228,14 +209,15 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap) tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
-- Gather detail text -- Gather detail text
local changeList, changeInfo = nil, nil local changeList, hint = nil, nil
if next(change.changedProperties) or change.changedName then if next(change.changedProperties) or change.changedName then
changeList = {} changeList = {}
local changeIndex = 0 local hintBuffer, i = {}, 0
local function addProp(prop: string, current: any?, incoming: any?, metadata: any?) local function addProp(prop: string, current: any?, incoming: any?, metadata: any?)
changeIndex += 1 i += 1
changeList[changeIndex] = { prop, current, incoming, metadata } hintBuffer[i] = prop
changeList[i] = { prop, current, incoming, metadata }
end end
-- Gather the changes -- Gather the changes
@@ -255,9 +237,19 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
) )
end end
changeInfo = { -- Finalize detail values
edits = changeIndex,
} -- Trim hint to top 3
table.sort(hintBuffer)
if #hintBuffer > 3 then
hintBuffer = {
hintBuffer[1],
hintBuffer[2],
hintBuffer[3],
i - 3 .. " more",
}
end
hint = table.concat(hintBuffer, ", ")
-- Sort changes and add header -- Sort changes and add header
table.sort(changeList, function(a, b) table.sort(changeList, function(a, b)
@@ -273,13 +265,11 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
className = instance.ClassName, className = instance.ClassName,
name = instance.Name, name = instance.Name,
instance = instance, instance = instance,
changeInfo = changeInfo, hint = hint,
changeList = changeList, changeList = changeList,
}) })
end end
Timer.stop()
Timer.start("patch.removed")
for _, idOrInstance in patch.removed do for _, idOrInstance in patch.removed do
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
if not instance then if not instance then
@@ -321,9 +311,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
instance = instance, instance = instance,
}) })
end end
Timer.stop()
Timer.start("patch.added")
for id, change in patch.added do for id, change in patch.added do
-- Gather ancestors from existing DOM or future additions -- Gather ancestors from existing DOM or future additions
local ancestryIds = {} local ancestryIds = {}
@@ -358,24 +346,36 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap) tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
-- Gather detail text -- Gather detail text
local changeList, changeInfo = nil, nil local changeList, hint = nil, nil
if next(change.Properties) then if next(change.Properties) then
changeList = {} changeList = {}
local changeIndex = 0 local hintBuffer, i = {}, 0
local function addProp(prop: string, incoming: any)
changeIndex += 1
changeList[changeIndex] = { prop, "N/A", incoming }
end
for prop, incoming in change.Properties do for prop, incoming in change.Properties do
i += 1
hintBuffer[i] = prop
local success, incomingValue = decodeValue(incoming, instanceMap) local success, incomingValue = decodeValue(incoming, instanceMap)
addProp(prop, if success then incomingValue else select(2, next(incoming))) if success then
table.insert(changeList, { prop, "N/A", incomingValue })
else
table.insert(changeList, { prop, "N/A", select(2, next(incoming)) })
end
end end
changeInfo = { -- Finalize detail values
edits = changeIndex,
} -- Trim hint to top 3
table.sort(hintBuffer)
if #hintBuffer > 3 then
hintBuffer = {
hintBuffer[1],
hintBuffer[2],
hintBuffer[3],
i - 3 .. " more",
}
end
hint = table.concat(hintBuffer, ", ")
-- Sort changes and add header -- Sort changes and add header
table.sort(changeList, function(a, b) table.sort(changeList, function(a, b)
@@ -390,32 +390,40 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
patchType = "Add", patchType = "Add",
className = change.ClassName, className = change.ClassName,
name = change.Name, name = change.Name,
changeInfo = changeInfo, hint = hint,
changeList = changeList, changeList = changeList,
instance = instanceMap.fromIds[id], instance = instanceMap.fromIds[id],
}) })
end end
Timer.stop()
Timer.stop()
return tree return tree
end end
-- Creates a deep copy of a tree for immutability purposes in Roact
function PatchTree.clone(tree)
if not tree then
return
end
local newTree = Tree.new()
tree:forEach(function(node)
newTree:addNode(node.parentId, table.clone(node))
end)
return newTree
end
-- Updates the metadata of a tree with the unapplied patch and currently existing instances -- Updates the metadata of a tree with the unapplied patch and currently existing instances
-- Builds a new tree from the data if one isn't provided -- Builds a new tree from the data if one isn't provided
-- Always returns a new tree for immutability purposes in Roact -- Always returns a new tree for immutability purposes in Roact
function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch) function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
Timer.start("PatchTree.updateMetadata")
if tree then if tree then
-- A shallow copy is enough for our purposes here since we really only need a new top-level object tree = PatchTree.clone(tree)
-- for immutable comparison checks in Roact
tree = table.clone(tree)
else else
tree = PatchTree.build(patch, instanceMap) tree = PatchTree.build(patch, instanceMap)
end end
-- Update isWarning metadata -- Update isWarning metadata
Timer.start("isWarning")
for _, failedChange in unappliedPatch.updated do for _, failedChange in unappliedPatch.updated do
local node = tree:getNode(failedChange.id) local node = tree:getNode(failedChange.id)
if not node then if not node then
@@ -428,8 +436,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
if not node.changeList then if not node.changeList then
continue continue
end end
local warnings = 0
for _, change in node.changeList do for _, change in node.changeList do
local property = change[1] local property = change[1]
local propertyFailedToApply = if property == "Name" local propertyFailedToApply = if property == "Name"
@@ -440,8 +446,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
-- This change didn't fail, no need to mark -- This change didn't fail, no need to mark
continue continue
end end
warnings += 1
if change[4] == nil then if change[4] == nil then
change[4] = { isWarning = true } change[4] = { isWarning = true }
else else
@@ -449,11 +453,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
end end
Log.trace(" Marked property as warning: {}.{}", node.name, property) Log.trace(" Marked property as warning: {}.{}", node.name, property)
end end
node.changeInfo = {
edits = (node.changeInfo.edits or (#node.changeList - 1)) - warnings,
failed = if warnings > 0 then warnings else nil,
}
end end
for failedAdditionId in unappliedPatch.added do for failedAdditionId in unappliedPatch.added do
local node = tree:getNode(failedAdditionId) local node = tree:getNode(failedAdditionId)
@@ -467,7 +466,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
if not node.changeList then if not node.changeList then
continue continue
end end
for _, change in node.changeList do for _, change in node.changeList do
-- Failed addition means that all properties failed to be added -- Failed addition means that all properties failed to be added
if change[4] == nil then if change[4] == nil then
@@ -477,10 +475,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
end end
Log.trace(" Marked property as warning: {}.{}", node.name, change[1]) Log.trace(" Marked property as warning: {}.{}", node.name, change[1])
end end
node.changeInfo = {
failed = node.changeInfo.edits or (#node.changeList - 1),
}
end end
for _, failedRemovalIdOrInstance in unappliedPatch.removed do for _, failedRemovalIdOrInstance in unappliedPatch.removed do
local failedRemovalId = if Types.RbxId(failedRemovalIdOrInstance) local failedRemovalId = if Types.RbxId(failedRemovalIdOrInstance)
@@ -498,10 +492,8 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
node.isWarning = true node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name) Log.trace("Marked node as warning: {} {}", node.id, node.name)
end end
Timer.stop()
-- Update if instances exist -- Update if instances exist
Timer.start("instanceAncestry")
tree:forEach(function(node) tree:forEach(function(node)
if node.instance then if node.instance then
if node.instance.Parent == nil and node.instance ~= game then if node.instance.Parent == nil and node.instance ~= game then
@@ -517,9 +509,7 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
end end
end end
end) end)
Timer.stop()
Timer.stop()
return tree return tree
end end

View File

@@ -16,25 +16,14 @@ local invariant = require(script.Parent.Parent.invariant)
local decodeValue = require(script.Parent.decodeValue) local decodeValue = require(script.Parent.decodeValue)
local reify = require(script.Parent.reify) local reify = require(script.Parent.reify)
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
local setProperty = require(script.Parent.setProperty) local setProperty = require(script.Parent.setProperty)
local function applyPatch(instanceMap, patch) local function applyPatch(instanceMap, patch)
local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us") local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us")
local historyRecording = ChangeHistoryService:TryBeginRecording("Rojo: Patch " .. patchTimestamp)
if not historyRecording then
-- There can only be one recording at a time
Log.debug("Failed to begin history recording for " .. patchTimestamp .. ". Another recording is in progress.")
end
-- Tracks any portions of the patch that could not be applied to the DOM. -- Tracks any portions of the patch that could not be applied to the DOM.
local unappliedPatch = PatchSet.newEmpty() local unappliedPatch = PatchSet.newEmpty()
-- Contains a list of all of the ref properties that we'll need to assign.
-- It is imperative that refs are assigned after all instances are created
-- to ensure that referents can be mapped to instances correctly.
local deferredRefs = {}
for _, removedIdOrInstance in ipairs(patch.removed) do for _, removedIdOrInstance in ipairs(patch.removed) do
local removeInstanceSuccess = pcall(function() local removeInstanceSuccess = pcall(function()
if Types.RbxId(removedIdOrInstance) then if Types.RbxId(removedIdOrInstance) then
@@ -73,9 +62,6 @@ local function applyPatch(instanceMap, patch)
if parentInstance == nil then if parentInstance == nil then
-- This would be peculiar. If you create an instance with no -- This would be peculiar. If you create an instance with no
-- parent, were you supposed to create it at all? -- parent, were you supposed to create it at all?
if historyRecording then
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
end
invariant( invariant(
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}", "Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
id, id,
@@ -84,7 +70,7 @@ local function applyPatch(instanceMap, patch)
) )
end end
local failedToReify = reifyInstance(deferredRefs, instanceMap, patch.added, id, parentInstance) local failedToReify = reify(instanceMap, patch.added, id, parentInstance)
if not PatchSet.isEmpty(failedToReify) then if not PatchSet.isEmpty(failedToReify) then
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify) Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
@@ -149,7 +135,7 @@ local function applyPatch(instanceMap, patch)
[update.id] = mockVirtualInstance, [update.id] = mockVirtualInstance,
} }
local failedToReify = reifyInstance(deferredRefs, instanceMap, mockAdded, update.id, instance.Parent) local failedToReify = reify(instanceMap, mockAdded, update.id, instance.Parent)
local newInstance = instanceMap.fromIds[update.id] local newInstance = instanceMap.fromIds[update.id]
@@ -178,14 +164,10 @@ local function applyPatch(instanceMap, patch)
end end
-- See you later, original instance. -- See you later, original instance.
--
-- Because the user might want to Undo this change, we cannot use Destroy
-- since that locks that parent and prevents ChangeHistoryService from
-- ever bringing it back. Instead, we parent to nil.
-- TODO: Can this fail? Some kinds of instance may not appreciate -- TODO: Can this fail? Some kinds of instance may not appreciate
-- being reparented, like services. -- being destroyed, like services.
instance.Parent = nil instance:Destroy()
-- This completes your rebuilding a plane mid-flight safety -- This completes your rebuilding a plane mid-flight safety
-- instruction. Please sit back, relax, and enjoy your flight. -- instruction. Please sit back, relax, and enjoy your flight.
@@ -212,18 +194,6 @@ local function applyPatch(instanceMap, patch)
if update.changedProperties ~= nil then if update.changedProperties ~= nil then
for propertyName, propertyValue in pairs(update.changedProperties) do for propertyName, propertyValue in pairs(update.changedProperties) do
-- Because refs may refer to instances that we haven't constructed yet,
-- we defer applying any ref properties until all instances are created.
if next(propertyValue) == "Ref" then
table.insert(deferredRefs, {
id = update.id,
instance = instance,
propertyName = propertyName,
virtualValue = propertyValue,
})
continue
end
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap) local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
if not decodeSuccess then if not decodeSuccess then
unappliedUpdate.changedProperties[propertyName] = propertyValue unappliedUpdate.changedProperties[propertyName] = propertyValue
@@ -244,11 +214,7 @@ local function applyPatch(instanceMap, patch)
end end
end end
if historyRecording then ChangeHistoryService:SetWaypoint("Rojo: Patch " .. patchTimestamp)
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
end
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
return unappliedPatch return unappliedPatch
end end

View File

@@ -4,41 +4,25 @@ return function()
local InstanceMap = require(script.Parent.Parent.InstanceMap) local InstanceMap = require(script.Parent.Parent.InstanceMap)
local PatchSet = require(script.Parent.Parent.PatchSet) local PatchSet = require(script.Parent.Parent.PatchSet)
local container = Instance.new("Folder") local dummy = Instance.new("Folder")
local function wasDestroyed(instance)
local tempContainer = Instance.new("Folder")
local function wasRemoved(instance)
-- If an instance was destroyed, its parent property is locked. -- If an instance was destroyed, its parent property is locked.
-- If an instance was removed, its parent property is nil. local ok = pcall(function()
-- We need to ensure we only remove, so that ChangeHistoryService can still Undo.
local isParentUnlocked = pcall(function()
local oldParent = instance.Parent local oldParent = instance.Parent
instance.Parent = tempContainer instance.Parent = dummy
instance.Parent = oldParent instance.Parent = oldParent
end) end)
return instance.Parent == nil and isParentUnlocked return not ok
end end
beforeEach(function()
container:ClearAllChildren()
end)
afterAll(function()
container:Destroy()
tempContainer:Destroy()
end)
it("should return an empty patch if given an empty patch", function() it("should return an empty patch if given an empty patch", function()
local patch = applyPatch(InstanceMap.new(), PatchSet.newEmpty()) local patch = applyPatch(InstanceMap.new(), PatchSet.newEmpty())
assert(PatchSet.isEmpty(patch), "expected remaining patch to be empty") assert(PatchSet.isEmpty(patch), "expected remaining patch to be empty")
end) end)
it("should remove instances listed for remove", function() it("should destroy instances listed for remove", function()
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "ROOT"
root.Parent = container
local child = Instance.new("Folder") local child = Instance.new("Folder")
child.Name = "Child" child.Name = "Child"
@@ -54,16 +38,14 @@ return function()
local unapplied = applyPatch(instanceMap, patch) local unapplied = applyPatch(instanceMap, patch)
assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty")
assert(not wasRemoved(root), "expected root to be left alone") assert(not wasDestroyed(root), "expected root to be left alone")
assert(wasRemoved(child), "expected child to be removed") assert(wasDestroyed(child), "expected child to be destroyed")
instanceMap:stop() instanceMap:stop()
end) end)
it("should remove IDs listed for remove", function() it("should destroy IDs listed for remove", function()
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "ROOT"
root.Parent = container
local child = Instance.new("Folder") local child = Instance.new("Folder")
child.Name = "Child" child.Name = "Child"
@@ -80,8 +62,8 @@ return function()
assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty")
expect(instanceMap:size()).to.equal(1) expect(instanceMap:size()).to.equal(1)
assert(not wasRemoved(root), "expected root to be left alone") assert(not wasDestroyed(root), "expected root to be left alone")
assert(wasRemoved(child), "expected child to be removed") assert(wasDestroyed(child), "expected child to be destroyed")
instanceMap:stop() instanceMap:stop()
end) end)
@@ -91,8 +73,6 @@ return function()
-- tests on reify, not here. -- tests on reify, not here.
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "ROOT"
root.Parent = container
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
instanceMap:insert("ROOT", root) instanceMap:insert("ROOT", root)
@@ -133,8 +113,6 @@ return function()
it("should return unapplied additions when instances cannot be created", function() it("should return unapplied additions when instances cannot be created", function()
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "ROOT"
root.Parent = container
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
instanceMap:insert("ROOT", root) instanceMap:insert("ROOT", root)
@@ -181,7 +159,6 @@ return function()
it("should recreate instances when changedClassName is set, preserving children", function() it("should recreate instances when changedClassName is set, preserving children", function()
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "Initial Root Name" root.Name = "Initial Root Name"
root.Parent = container
local child = Instance.new("Folder") local child = Instance.new("Folder")
child.Name = "Child" child.Name = "Child"

View File

@@ -151,24 +151,7 @@ local function diff(instanceMap, virtualInstances, rootId)
if getProperySuccess then if getProperySuccess then
local existingValue = existingValueOrErr local existingValue = existingValueOrErr
local decodeSuccess, decodedValue local decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
-- If `virtualValue` is a ref then instead of decoding it to an instance,
-- we change `existingValue` to be a ref. This is because `virtualValue`
-- may point to an Instance which doesn't exist yet and therefore
-- decoding it may throw an error.
if next(virtualValue) == "Ref" then
decodeSuccess, decodedValue = true, virtualValue
if existingValue and typeof(existingValue) == "Instance" then
local existingValueRef = instanceMap.fromInstances[existingValue]
if existingValueRef then
existingValue = { Ref = existingValueRef }
end
end
else
decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
end
if decodeSuccess then if decodeSuccess then
if not trueEquals(existingValue, decodedValue) then if not trueEquals(existingValue, decodedValue) then

View File

@@ -3,14 +3,9 @@
and mutating the Roblox DOM. and mutating the Roblox DOM.
]] ]]
local Rojo = script:FindFirstAncestor("Rojo") local Packages = script.Parent.Parent.Packages
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Log = require(Packages.Log) local Log = require(Packages.Log)
local Timer = require(Plugin.Timer)
local applyPatch = require(script.applyPatch) local applyPatch = require(script.applyPatch)
local hydrate = require(script.hydrate) local hydrate = require(script.hydrate)
local diff = require(script.diff) local diff = require(script.diff)
@@ -62,55 +57,31 @@ function Reconciler:hookPostcommit(callback: (patch: any, instanceMap: any, unap
end end
function Reconciler:applyPatch(patch) function Reconciler:applyPatch(patch)
Timer.start("Reconciler:applyPatch")
Timer.start("precommitCallbacks")
-- Precommit callbacks must be serial in order to obey the contract that
-- they execute before commit
for _, callback in self.__precommitCallbacks do for _, callback in self.__precommitCallbacks do
local success, err = pcall(callback, patch, self.__instanceMap) local success, err = pcall(callback, patch, self.__instanceMap)
if not success then if not success then
Log.warn("Precommit hook errored: {}", err) Log.warn("Precommit hook errored: {}", err)
end end
end end
Timer.stop()
Timer.start("apply")
local unappliedPatch = applyPatch(self.__instanceMap, patch) local unappliedPatch = applyPatch(self.__instanceMap, patch)
Timer.stop()
Timer.start("postcommitCallbacks")
-- Postcommit callbacks can be called with spawn since regardless of firing order, they are
-- guaranteed to be called after the commit
for _, callback in self.__postcommitCallbacks do for _, callback in self.__postcommitCallbacks do
task.spawn(function() local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch) if not success then
if not success then Log.warn("Postcommit hook errored: {}", err)
Log.warn("Postcommit hook errored: {}", err) end
end
end)
end end
Timer.stop()
Timer.stop()
return unappliedPatch return unappliedPatch
end end
function Reconciler:hydrate(virtualInstances, rootId, rootInstance) function Reconciler:hydrate(virtualInstances, rootId, rootInstance)
Timer.start("Reconciler:hydrate") return hydrate(self.__instanceMap, virtualInstances, rootId, rootInstance)
local result = hydrate(self.__instanceMap, virtualInstances, rootId, rootInstance)
Timer.stop()
return result
end end
function Reconciler:diff(virtualInstances, rootId) function Reconciler:diff(virtualInstances, rootId)
Timer.start("Reconciler:diff") return diff(self.__instanceMap, virtualInstances, rootId)
local success, result = diff(self.__instanceMap, virtualInstances, rootId)
Timer.stop()
return success, result
end end
return Reconciler return Reconciler

View File

@@ -7,6 +7,26 @@ local PatchSet = require(script.Parent.Parent.PatchSet)
local setProperty = require(script.Parent.setProperty) local setProperty = require(script.Parent.setProperty)
local decodeValue = require(script.Parent.decodeValue) local decodeValue = require(script.Parent.decodeValue)
local reifyInner, applyDeferredRefs
local function reify(instanceMap, virtualInstances, rootId, parentInstance)
-- Create an empty patch that will be populated with any parts of this reify
-- that could not happen, like instances that couldn't be created and
-- properties that could not be assigned.
local unappliedPatch = PatchSet.newEmpty()
-- Contains a list of all of the ref properties that we'll need to assign
-- after all instances are created. We apply refs in a second pass, after
-- we create as many instances as we can, so that we ensure that referents
-- can be mapped to instances correctly.
local deferredRefs = {}
reifyInner(instanceMap, virtualInstances, rootId, parentInstance, unappliedPatch, deferredRefs)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
return unappliedPatch
end
--[[ --[[
Add the given ID and all of its descendants in virtualInstances to the given Add the given ID and all of its descendants in virtualInstances to the given
PatchSet, marked for addition. PatchSet, marked for addition.
@@ -20,21 +40,10 @@ local function addAllToPatch(patchSet, virtualInstances, id)
end end
end end
function reifyInstance(deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
-- Create an empty patch that will be populated with any parts of this reify
-- that could not happen, like instances that couldn't be created and
-- properties that could not be assigned.
local unappliedPatch = PatchSet.newEmpty()
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
return unappliedPatch
end
--[[ --[[
Inner function that defines the core routine. Inner function that defines the core routine.
]] ]]
function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, id, parentInstance) function reifyInner(instanceMap, virtualInstances, id, parentInstance, unappliedPatch, deferredRefs)
local virtualInstance = virtualInstances[id] local virtualInstance = virtualInstances[id]
if virtualInstance == nil then if virtualInstance == nil then
@@ -93,7 +102,7 @@ function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualIn
end end
for _, childId in ipairs(virtualInstance.Children) do for _, childId in ipairs(virtualInstance.Children) do
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, childId, instance) reifyInner(instanceMap, virtualInstances, childId, instance, unappliedPatch, deferredRefs)
end end
instance.Parent = parentInstance instance.Parent = parentInstance
@@ -134,7 +143,6 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end end
local targetInstance = instanceMap.fromIds[refId] local targetInstance = instanceMap.fromIds[refId]
if targetInstance == nil then if targetInstance == nil then
markFailed(entry.id, entry.propertyName, entry.virtualValue) markFailed(entry.id, entry.propertyName, entry.virtualValue)
continue continue
@@ -147,7 +155,4 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end end
end end
return { return reify
reifyInstance = reifyInstance,
applyDeferredRefs = applyDeferredRefs,
}

View File

@@ -1,6 +1,5 @@
return function() return function()
local reify = require(script.Parent.reify) local reify = require(script.Parent.reify)
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
local PatchSet = require(script.Parent.Parent.PatchSet) local PatchSet = require(script.Parent.Parent.PatchSet)
local InstanceMap = require(script.Parent.Parent.InstanceMap) local InstanceMap = require(script.Parent.Parent.InstanceMap)
@@ -21,11 +20,7 @@ return function()
it("should throw when given a bogus ID", function() it("should throw when given a bogus ID", function()
expect(function() expect(function()
local deferredRefs = {} reify(InstanceMap.new(), {}, "Hi, mom!", game)
local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, {}, "Hi, mom!", game)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
end).to.throw() end).to.throw()
end) end)
@@ -39,11 +34,8 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT", nil) local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT", nil)
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(instanceMap:size() == 0, "expected instanceMap to be empty") assert(instanceMap:size() == 0, "expected instanceMap to be empty")
@@ -68,11 +60,8 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT") local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@@ -101,11 +90,8 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT") local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@@ -136,11 +122,8 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT") local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
expect(size(unappliedPatch.added)).to.equal(1) expect(size(unappliedPatch.added)).to.equal(1)
expect(unappliedPatch.added["CHILD"]).to.equal(virtualInstances["CHILD"]) expect(unappliedPatch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
@@ -170,11 +153,8 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT") local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
local instance = instanceMap.fromIds["ROOT"] local instance = instanceMap.fromIds["ROOT"]
expect(instance.ClassName).to.equal("StringValue") expect(instance.ClassName).to.equal("StringValue")
@@ -216,11 +196,8 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT") local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@@ -246,16 +223,13 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local existing = Instance.new("Folder") local existing = Instance.new("Folder")
existing.Name = "Existing" existing.Name = "Existing"
instanceMap:insert("EXISTING", existing) instanceMap:insert("EXISTING", existing)
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT") local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@@ -294,11 +268,8 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT") local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@@ -336,11 +307,8 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT") local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
@@ -364,11 +332,8 @@ return function()
}, },
} }
local deferredRefs = {}
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT") local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
assert(not PatchSet.hasRemoves(unappliedPatch), "expected no removes") assert(not PatchSet.hasRemoves(unappliedPatch), "expected no removes")
assert(not PatchSet.hasAdditions(unappliedPatch), "expected no additions") assert(not PatchSet.hasAdditions(unappliedPatch), "expected no additions")

View File

@@ -7,7 +7,7 @@ local Log = require(Packages.Log)
local RbxDom = require(Packages.RbxDom) local RbxDom = require(Packages.RbxDom)
local Error = require(script.Parent.Error) local Error = require(script.Parent.Error)
local function setProperty(instance, propertyName, value) local function setProperty(instance: Instance, propertyName: string, value: unknown): boolean
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName) local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
-- We can skip unknown properties; they're not likely reflected to Lua. -- We can skip unknown properties; they're not likely reflected to Lua.
@@ -28,6 +28,13 @@ local function setProperty(instance, propertyName, value)
}) })
end end
if value == nil then
if descriptor.dataType == "Float32" or descriptor.dataType == "Float64" then
Log.trace("Skipping nil {} property {}.{}", descriptor.dataType, instance.ClassName, propertyName)
return true
end
end
local writeSuccess, err = descriptor:write(instance, value) local writeSuccess, err = descriptor:write(instance, value)
if not writeSuccess then if not writeSuccess then

View File

@@ -13,7 +13,6 @@ local InstanceMap = require(script.Parent.InstanceMap)
local PatchSet = require(script.Parent.PatchSet) local PatchSet = require(script.Parent.PatchSet)
local Reconciler = require(script.Parent.Reconciler) local Reconciler = require(script.Parent.Reconciler)
local strict = require(script.Parent.strict) local strict = require(script.Parent.strict)
local Settings = require(script.Parent.Settings)
local Status = strict("Session.Status", { local Status = strict("Session.Status", {
NotStarted = "NotStarted", NotStarted = "NotStarted",
@@ -51,6 +50,7 @@ ServeSession.Status = Status
local validateServeOptions = t.strictInterface({ local validateServeOptions = t.strictInterface({
apiContext = t.table, apiContext = t.table,
openScriptsExternally = t.boolean,
twoWaySync = t.boolean, twoWaySync = t.boolean,
}) })
@@ -89,6 +89,7 @@ function ServeSession.new(options)
self = { self = {
__status = Status.NotStarted, __status = Status.NotStarted,
__apiContext = options.apiContext, __apiContext = options.apiContext,
__openScriptsExternally = options.openScriptsExternally,
__twoWaySync = options.twoWaySync, __twoWaySync = options.twoWaySync,
__reconciler = reconciler, __reconciler = reconciler,
__instanceMap = instanceMap, __instanceMap = instanceMap,
@@ -169,7 +170,7 @@ function ServeSession:__applyGameAndPlaceId(serverInfo)
end end
function ServeSession:__onActiveScriptChanged(activeScript) function ServeSession:__onActiveScriptChanged(activeScript)
if not Settings:get("openScriptsExternally") then if not self.__openScriptsExternally then
Log.trace("Not opening script {} because feature not enabled.", activeScript) Log.trace("Not opening script {} because feature not enabled.", activeScript)
return return

View File

@@ -14,15 +14,11 @@ local defaultSettings = {
twoWaySync = false, twoWaySync = false,
showNotifications = true, showNotifications = true,
syncReminder = true, syncReminder = true,
checkForUpdates = true,
checkForPrereleases = false,
autoConnectPlaytestServer = false,
confirmationBehavior = "Initial", confirmationBehavior = "Initial",
largeChangesConfirmationThreshold = 5, largeChangesConfirmationThreshold = 5,
playSounds = true, playSounds = true,
typecheckingEnabled = false, typecheckingEnabled = false,
logLevel = "Info", logLevel = "Info",
timingLogsEnabled = false,
priorEndpoints = {}, priorEndpoints = {},
} }

View File

@@ -1,57 +0,0 @@
local Settings = require(script.Parent.Settings)
local clock = os.clock
local Timer = {
_entries = {},
}
function Timer._start(label)
local start = clock()
if not label then
error("[Rojo-Timer] Timer.start: label is required", 2)
return
end
table.insert(Timer._entries, { label, start })
end
function Timer._stop()
local stop = clock()
local entry = table.remove(Timer._entries)
if not entry then
error("[Rojo-Timer] Timer.stop: no label to stop", 2)
return
end
local label = entry[1]
if #Timer._entries > 0 then
local priorLabels = {}
for _, priorEntry in ipairs(Timer._entries) do
table.insert(priorLabels, priorEntry[1])
end
label = table.concat(priorLabels, "/") .. "/" .. label
end
local start = entry[2]
local duration = stop - start
print(string.format("[Rojo-Timer] %s took %.3f ms", label, duration * 1000))
end
-- Replace functions with no-op if not in debug mode
local function no_op() end
local function setFunctions(enabled)
if enabled then
Timer.start = Timer._start
Timer.stop = Timer._stop
else
Timer.start = no_op
Timer.stop = no_op
end
end
Settings:onChanged("timingLogsEnabled", setFunctions)
setFunctions(Settings:get("timingLogsEnabled"))
return Timer

View File

@@ -1,7 +1,3 @@
local Packages = script.Parent.Parent.Packages
local Http = require(Packages.Http)
local Promise = require(Packages.Promise)
local function compare(a, b) local function compare(a, b)
if a > b then if a > b then
return 1 return 1
@@ -34,48 +30,7 @@ function Version.compare(a, b)
return minor return minor
end end
if revision ~= 0 then return revision
return revision
end
local aPrerelease = if a[4] == "" then nil else a[4]
local bPrerelease = if b[4] == "" then nil else b[4]
-- If neither are prerelease, they are the same
if aPrerelease == nil and bPrerelease == nil then
return 0
end
-- If one is prerelease it is older
if aPrerelease ~= nil and bPrerelease == nil then
return -1
end
if aPrerelease == nil and bPrerelease ~= nil then
return 1
end
-- If they are both prereleases, compare those based on number
local aPrereleaseNumeric = string.match(aPrerelease, "(%d+).*$")
local bPrereleaseNumeric = string.match(bPrerelease, "(%d+).*$")
if aPrereleaseNumeric == nil or bPrereleaseNumeric == nil then
-- If one or both lack a number, comparing isn't meaningful
return 0
end
return compare(tonumber(aPrereleaseNumeric) or 0, tonumber(bPrereleaseNumeric) or 0)
end
function Version.parse(versionString: string)
local version = { string.match(versionString, "^v?(%d+)%.(%d+)%.(%d+)(.*)$") }
for i, v in version do
version[i] = tonumber(v) or v
end
if version[4] == "" then
version[4] = nil
end
return version
end end
function Version.display(version) function Version.display(version)
@@ -88,64 +43,4 @@ function Version.display(version)
return output return output
end end
function Version.retrieveLatestCompatible(options: {
version: { number },
includePrereleases: boolean?,
}): {
version: { number },
prerelease: boolean,
publishedUnixTimestamp: number,
}?
local success, releases = Http.get("https://api.github.com/repos/rojo-rbx/rojo/releases?per_page=10")
:andThen(function(response)
if response.code >= 400 then
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
return Promise.reject(message)
end
return response
end)
:andThen(Http.Response.json)
:await()
if success == false or type(releases) ~= "table" or next(releases) ~= 1 then
return nil
end
-- Iterate through releases, looking for the latest compatible version
local latestCompatible = nil
for _, release in releases do
-- Skip prereleases if they are not requested
if (not options.includePrereleases) and release.prerelease then
continue
end
local releaseVersion = Version.parse(release.tag_name)
-- Skip releases that are potentially incompatible
if releaseVersion[1] > options.version[1] then
continue
end
-- Skip releases that are older than the latest compatible version
if latestCompatible ~= nil and Version.compare(releaseVersion, latestCompatible.version) <= 0 then
continue
end
latestCompatible = {
version = releaseVersion,
prerelease = release.prerelease,
publishedUnixTimestamp = DateTime.fromIsoDate(release.published_at).UnixTimestamp,
}
end
-- Don't return anything if the latest found is not newer than the current version
if latestCompatible == nil or Version.compare(latestCompatible.version, options.version) <= 0 then
return nil
end
return latestCompatible
end
return Version return Version

View File

@@ -3,7 +3,6 @@ return function()
it("should compare equal versions", function() it("should compare equal versions", function()
expect(Version.compare({ 1, 2, 3 }, { 1, 2, 3 })).to.equal(0) expect(Version.compare({ 1, 2, 3 }, { 1, 2, 3 })).to.equal(0)
expect(Version.compare({ 1, 2, 3, "rc1" }, { 1, 2, 3, "rc1" })).to.equal(0)
expect(Version.compare({ 0, 4, 0 }, { 0, 4 })).to.equal(0) expect(Version.compare({ 0, 4, 0 }, { 0, 4 })).to.equal(0)
expect(Version.compare({ 0, 0, 123 }, { 0, 0, 123 })).to.equal(0) expect(Version.compare({ 0, 0, 123 }, { 0, 0, 123 })).to.equal(0)
expect(Version.compare({ 26 }, { 26 })).to.equal(0) expect(Version.compare({ 26 }, { 26 })).to.equal(0)
@@ -14,7 +13,6 @@ return function()
it("should compare newer, older versions", function() it("should compare newer, older versions", function()
expect(Version.compare({ 1 }, { 0 })).to.equal(1) expect(Version.compare({ 1 }, { 0 })).to.equal(1)
expect(Version.compare({ 1, 1 }, { 1, 0 })).to.equal(1) expect(Version.compare({ 1, 1 }, { 1, 0 })).to.equal(1)
expect(Version.compare({ 1, 2, 3 }, { 1, 2, 0 })).to.equal(1)
end) end)
it("should compare different major versions", function() it("should compare different major versions", function()
@@ -27,37 +25,4 @@ return function()
expect(Version.compare({ 1, 2, 3 }, { 1, 3, 2 })).to.equal(-1) expect(Version.compare({ 1, 2, 3 }, { 1, 3, 2 })).to.equal(-1)
expect(Version.compare({ 50, 1 }, { 50, 2 })).to.equal(-1) expect(Version.compare({ 50, 1 }, { 50, 2 })).to.equal(-1)
end) end)
it("should compare different patch versions", function()
expect(Version.compare({ 1, 1, 3 }, { 1, 1, 2 })).to.equal(1)
expect(Version.compare({ 1, 1, 2 }, { 1, 1, 3 })).to.equal(-1)
expect(Version.compare({ 1, 1, 3, "-rc1" }, { 1, 1, 2, "-rc2" })).to.equal(1)
expect(Version.compare({ 1, 1, 2, "-rc5" }, { 1, 1, 3, "-alpha" })).to.equal(-1)
end)
it("should compare prerelease tags", function()
expect(Version.compare({ 1, 0, 0, "-alpha" }, { 1, 0, 0 })).to.equal(-1)
expect(Version.compare({ 1, 0, 0 }, { 1, 0, 0, "-alpha" })).to.equal(1)
expect(Version.compare({ 1, 0, 0, "-rc1" }, { 1, 0, 0, "-rc2" })).to.equal(-1)
expect(Version.compare({ 1, 0, 0, "-rc2" }, { 1, 0, 0, "-rc1" })).to.equal(1)
-- Non number prereleases are not compared since that isn't meaningful
expect(Version.compare({ 1, 0, 0, "-alpha" }, { 1, 0, 0, "-beta" })).to.equal(0)
end)
it("should parse version from strings", function()
local a = Version.parse("v1.0.0")
expect(a).to.be.ok()
expect(a[1]).to.equal(1)
expect(a[2]).to.equal(0)
expect(a[3]).to.equal(0)
expect(a[4]).to.equal(nil)
local b = Version.parse("7.3.1-rc1")
expect(b).to.be.ok()
expect(b[1]).to.equal(7)
expect(b[2]).to.equal(3)
expect(b[3]).to.equal(1)
expect(b[4]).to.equal("-rc1")
end)
end end

View File

@@ -1,31 +0,0 @@
local timeUtil = {}
timeUtil.AGE_UNITS = table.freeze({
{ 31556909, "year" },
{ 2629743, "month" },
{ 604800, "week" },
{ 86400, "day" },
{ 3600, "hour" },
{ 60, "minute" },
})
function timeUtil.elapsedToText(elapsed: number): string
if elapsed < 3 then
return "just now"
end
local ageText = string.format("%d seconds ago", elapsed)
for _, UnitData in timeUtil.AGE_UNITS do
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
if elapsed > UnitSeconds then
local c = math.floor(elapsed / UnitSeconds)
ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "")
break
end
end
return ageText
end
return timeUtil

4
plugin/test Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
rojo build test-place.project.json -o TestPlace.rbxlx
run-in-roblox --script run-tests.server.lua --place TestPlace.rbxlx

View File

@@ -5,7 +5,7 @@
"ReplicatedStorage": { "ReplicatedStorage": {
"Rojo": { "Rojo": {
"$path": "../plugin.project.json" "$path": "default.project.json"
}, },
"Packages": { "Packages": {

2
plugin/watch-build.sh Normal file
View File

@@ -0,0 +1,2 @@
# Continously build the rojo plugin into the local plugin directory on Windows
rojo build plugin/default.project.json -o $LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm --watch

View File

@@ -6,7 +6,6 @@ expression: contents
<Item class="Model" referent="0"> <Item class="Model" referent="0">
<Properties> <Properties>
<string name="Name">init_meta_class_name</string> <string name="Name">init_meta_class_name</string>
<bool name="NeedsPivotMigration">false</bool>
</Properties> </Properties>
</Item> </Item>
</roblox> </roblox>

View File

@@ -1,6 +1,6 @@
--- ---
source: tests/tests/build.rs source: tests/tests/build.rs
assertion_line: 107 assertion_line: 104
expression: contents expression: contents
--- ---
<roblox version="4"> <roblox version="4">

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