Compare commits

..

3 Commits

Author SHA1 Message Date
Lucien Greathouse
66ef335172 ...and build with submodules for other builds too 2022-05-25 19:42:40 -04:00
Lucien Greathouse
4352590ba4 Checkout submodules in plugin build step 2022-05-25 19:40:42 -04:00
Lucien Greathouse
0fe751ef33 Port release workflow from Aftman to test 2022-05-25 19:36:54 -04:00
249 changed files with 5080 additions and 31367 deletions

View File

@@ -1,2 +0,0 @@
# stylua formatting
0f8e1625d572a5fe0f7b5c08653ff92cc837d346

View File

@@ -1,23 +0,0 @@
name: Changelog Check
on:
pull_request:
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
branches:
- master
jobs:
build:
name: Check Actions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Changelog check
uses: Zomzog/changelog-checker@v1.3.0
with:
fileName: CHANGELOG.md
noChangelogLabel: skip changelog
checkNotification: Simple
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -3,11 +3,11 @@ name: CI
on: on:
push: push:
branches: branches:
- master - main
pull_request: pull_request:
branches: branches:
- master - main
jobs: jobs:
build: build:
@@ -16,12 +16,10 @@ jobs:
strategy: strategy:
matrix: matrix:
rust_version: [stable, 1.70.0] rust_version: [stable, 1.55.0]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@@ -30,11 +28,6 @@ jobs:
override: true override: true
profile: minimal profile: minimal
- name: Setup Aftman
uses: ok-nick/setup-aftman@v0.3.0
with:
version: 'v0.2.7'
- name: Build - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
@@ -42,13 +35,11 @@ jobs:
run: cargo test --locked --verbose run: cargo test --locked --verbose
lint: lint:
name: Rustfmt, Clippy, & Stylua name: Rustfmt and Clippy
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@@ -57,17 +48,8 @@ jobs:
override: true override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Setup Aftman
uses: ok-nick/setup-aftman@v0.3.0
with:
version: 'v0.2.7'
- name: Stylua
run: stylua --check plugin/src
- name: Rustfmt - name: Rustfmt
run: cargo fmt -- --check run: cargo fmt -- --check
- name: Clippy - name: Clippy
run: cargo clippy run: cargo clippy

View File

@@ -31,12 +31,10 @@ jobs:
with: with:
submodules: true submodules: true
- name: Setup Aftman - name: Setup Foreman
uses: ok-nick/setup-aftman@v0.1.0 uses: Roblox/setup-foreman@v1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
trust-check: false
version: 'v0.2.6'
- name: Build Plugin - name: Build Plugin
run: rojo build plugin --output Rojo.rbxm run: rojo build plugin --output Rojo.rbxm
@@ -63,21 +61,25 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
# https://doc.rust-lang.org/rustc/platform-support.html # https://doc.rust-lang.org/rustc/platform-support.html
#
# FIXME: After the Rojo VS Code extension updates, add architecture
# names to each of these releases. We'll rename win64 to windows and add
# -x86_64 to each release.
include: include:
- host: linux - host: linux
os: ubuntu-20.04 os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
label: linux-x86_64 label: linux
- host: windows - host: windows
os: windows-latest os: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
label: windows-x86_64 label: win64
- host: macos - host: macos
os: macos-latest os: macos-latest
target: x86_64-apple-darwin target: x86_64-apple-darwin
label: macos-x86_64 label: macos
- host: macos - host: macos
os: macos-latest os: macos-latest
@@ -108,15 +110,8 @@ jobs:
override: true override: true
profile: minimal profile: minimal
- name: Setup Aftman
uses: ok-nick/setup-aftman@v0.1.0
with:
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
env: env:
# Build into a known directory so we can find our build artifact more # Build into a known directory so we can find our build artifact more
# easily. # easily.
@@ -132,11 +127,11 @@ jobs:
mkdir staging mkdir staging
if [ "${{ matrix.host }}" = "windows" ]; then if [ "${{ matrix.host }}" = "windows" ]; then
cp "output/${{ matrix.target }}/release/$BIN.exe" staging/ cp "output/release/$BIN.exe" staging/
cd staging cd staging
7z a ../release.zip * 7z a ../release.zip *
else else
cp "output/${{ matrix.target }}/release/$BIN" staging/ cp "output/release/$BIN" staging/
cd staging cd staging
zip ../release.zip * zip ../release.zip *
fi fi

3
.gitignore vendored
View File

@@ -19,3 +19,6 @@
# Snapshot files from the 'insta' Rust crate # Snapshot files from the 'insta' Rust crate
**/*.snap.new **/*.snap.new
# Selene generates a roblox.toml file that should not be checked in.
/roblox.toml

31
.gitmodules vendored
View File

@@ -1,18 +1,15 @@
[submodule "plugin/Packages/Roact"] [submodule "plugin/modules/roact"]
path = plugin/Packages/Roact path = plugin/modules/roact
url = https://github.com/roblox/roact.git url = https://github.com/Roblox/roact.git
[submodule "plugin/Packages/Flipper"] [submodule "plugin/modules/testez"]
path = plugin/Packages/Flipper path = plugin/modules/testez
url = https://github.com/reselim/flipper.git url = https://github.com/Roblox/testez.git
[submodule "plugin/Packages/Promise"] [submodule "plugin/modules/promise"]
path = plugin/Packages/Promise path = plugin/modules/promise
url = https://github.com/evaera/roblox-lua-promise.git url = https://github.com/LPGhatguy/roblox-lua-promise.git
[submodule "plugin/Packages/t"] [submodule "plugin/modules/t"]
path = plugin/Packages/t path = plugin/modules/t
url = https://github.com/osyrisrblx/t.git url = https://github.com/osyrisrblx/t.git
[submodule "plugin/Packages/TestEZ"] [submodule "plugin/modules/flipper"]
path = plugin/Packages/TestEZ path = plugin/modules/flipper
url = https://github.com/roblox/testez.git url = https://github.com/Reselim/Flipper
[submodule "plugin/Packages/Highlighter"]
path = plugin/Packages/Highlighter
url = https://github.com/boatbomber/highlighter.git

58
.luacheckrc Normal file
View File

@@ -0,0 +1,58 @@
stds.roblox = {
read_globals = {
game = {
other_fields = true,
},
-- Roblox globals
"script",
-- Extra functions
"tick", "warn", "spawn",
"wait", "settings", "typeof",
-- Types
"Vector2", "Vector3",
"Vector2int16", "Vector3int16",
"Color3",
"UDim", "UDim2",
"Rect",
"CFrame",
"Enum",
"Instance",
"DockWidgetPluginGuiInfo",
}
}
stds.plugin = {
read_globals = {
"plugin",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP", "itFIXME",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
"421", -- shadowing local variable
"422", -- shadowing argument
"431", -- shadowing upvalue
"432", -- shadowing upvalue argument
}
std = "lua51+roblox"
files["**/*.server.lua"] = {
std = "+plugin",
}
files["**/*.spec.lua"] = {
std = "+testez",
}

View File

@@ -2,240 +2,6 @@
## Unreleased Changes ## Unreleased Changes
## [7.4.0-rc2] - October 3, 2023
* Fixed bug with parsing version for plugin validation ([#797])
[#797]: https://github.com/rojo-rbx/rojo/pull/797
## [7.4.0-rc1] - October 3, 2023
### Additions
#### Project format
* Added support for `.toml` files to `$path` ([#633])
* Added support for `Font` and `CFrame` attributes ([rbx-dom#299], [rbx-dom#296])
* Added the `emitLegacyScripts` field to the project format ([#765]). The behavior is outlined below:
| `emitLegacyScripts` Value | Action Taken by Rojo |
|---------------------------|------------------------------------------------------------------------------------------------------------------|
| false | Rojo emits Scripts with the appropriate `RunContext` for `*.client.lua` and `*.server.lua` files in the project. |
| true (default) | Rojo emits LocalScripts and Scripts with legacy `RunContext` (same behavior as previously). |
It can be used like this:
```json
{
"emitLegacyScripts": false,
"name": "MyCoolRunContextProject",
"tree": {
"$path": "src"
}
}
```
* Added `Terrain` classname inference, similar to services ([#771])
`Terrain` may now be defined in projects without using `$className`:
```json
"Workspace": {
"Terrain": {
"$path": "path/to/terrain.rbxm"
}
}
```
* Added support for `Terrain.MaterialColors` ([#770])
`Terrain.MaterialColors` is now represented in projects in a human readable format:
```json
"Workspace": {
"Terrain": {
"$path": "path/to/terrain.rbxm"
"$properties": {
"MaterialColors": {
"Grass": [10, 20, 30],
"Asphalt": [40, 50, 60],
"LeafyGrass": [255, 155, 55]
}
}
}
}
```
* Added better support for `Font` properties ([#731])
`FontFace` properties may now be defined using implicit property syntax:
```json
"TextBox": {
"$className": "TextBox",
"$properties": {
"FontFace": {
"family": "rbxasset://fonts/families/RobotoMono.json",
"weight": "Thin",
"style": "Normal"
}
}
}
```
#### Patch visualizer and notifications
* Added a setting to control patch confirmation behavior ([#774])
This is a new setting for controlling when the Rojo plugin prompts for confirmation before syncing. It has four options:
* Initial (default): prompts only once for a project in a given Studio session
* Always: always prompts for confirmation
* Large Changes: only prompts when there are more than X changed instances. The number of instances is configurable - an additional setting for the number of instances becomes available when this option is chosen
* Unlisted PlaceId: only prompts if the place ID is not present in servePlaceIds
* Added the ability to select Instances in patch visualizer ([#709])
Double-clicking an instance in the patch visualizer sets Roblox Studio's selection to the instance.
* Added a sync reminder notification. ([#689])
Rojo detects if you have previously synced to a place, and displays a notification reminding you to sync again:
![Rojo reminds you to sync a place that you've synced previously](https://user-images.githubusercontent.com/40185666/242397435-ccdfddf2-a63f-420c-bc18-a6e3d6455bba.png)
* Added rich Source diffs in patch visualizer ([#748])
A "View Diff" button for script sources is now present in the patch visualizer. Clicking it displays a side-by-side diff of the script changes:
![The patch visualizer contains a "view diff" button](https://user-images.githubusercontent.com/40185666/256065992-3f03558f-84b0-45a1-80eb-901f348cf067.png)
![The "View Diff" button opens a widget that displays a diff](https://user-images.githubusercontent.com/40185666/256066084-1d9d8fe8-7dad-4ee7-a542-b4aee35a5644.png)
* Patch visualizer now indicates what changes failed to apply. ([#717])
A clickable warning label is displayed when the Rojo plugin is unable to apply changes. Clicking the label displays precise information about which changes failed:
![Patch visualizer displays a clickable warning label when changes fail to apply](https://user-images.githubusercontent.com/40185666/252063660-f08399ef-1e16-4f1c-bed8-552821f98cef.png)
#### Miscellaneous
* Added `plugin` flag to the `build` command that outputs to the local plugins folder ([#735])
This is a flag that builds a Rojo project into Roblox Studio's plugins directory. This allows you to build a Rojo project and load it into Studio as a plugin without having to type the full path to the plugins directory. It can be used like this: `rojo build <PATH-TO-PROJECT> --plugin <FILE-NAME>`
* Added new plugin template to the `init` command ([#738])
This is a new template geared towards plugins. It is similar to the model template, but creates a `Script` instead of a `ModuleScript` in the `src` directory. It can be used like this: `rojo init --kind plugin`
* Added protection against syncing non-place projects as a place. ([#691])
* Add buttons for navigation on the Connected page ([#722])
### Fixes
* Significantly improved performance of `rojo serve` and `rojo build` on macOS. [#783]
* Significantly improved performance of `rojo sourcemap` ([#668])
* Fixed the diff visualizer of connected sessions. ([#674])
* Fixed disconnected session activity. ([#675])
* Skip confirming patches that contain only a datamodel name change. ([#688])
* Fix Rojo breaking when users undo/redo in Studio ([#708])
* Improve tooltip behavior ([#723])
* Better settings controls ([#725])
* Rework patch visualizer with many fixes and improvements ([#713], [#726], [#755])
[#668]: https://github.com/rojo-rbx/rojo/pull/668
[#674]: https://github.com/rojo-rbx/rojo/pull/674
[#675]: https://github.com/rojo-rbx/rojo/pull/675
[#688]: https://github.com/rojo-rbx/rojo/pull/688
[#689]: https://github.com/rojo-rbx/rojo/pull/689
[#691]: https://github.com/rojo-rbx/rojo/pull/691
[#709]: https://github.com/rojo-rbx/rojo/pull/709
[#708]: https://github.com/rojo-rbx/rojo/pull/708
[#713]: https://github.com/rojo-rbx/rojo/pull/713
[#717]: https://github.com/rojo-rbx/rojo/pull/717
[#722]: https://github.com/rojo-rbx/rojo/pull/722
[#723]: https://github.com/rojo-rbx/rojo/pull/723
[#725]: https://github.com/rojo-rbx/rojo/pull/725
[#726]: https://github.com/rojo-rbx/rojo/pull/726
[#633]: https://github.com/rojo-rbx/rojo/pull/633
[#735]: https://github.com/rojo-rbx/rojo/pull/735
[#731]: https://github.com/rojo-rbx/rojo/pull/731
[#738]: https://github.com/rojo-rbx/rojo/pull/738
[#748]: https://github.com/rojo-rbx/rojo/pull/748
[#755]: https://github.com/rojo-rbx/rojo/pull/755
[#765]: https://github.com/rojo-rbx/rojo/pull/765
[#770]: https://github.com/rojo-rbx/rojo/pull/770
[#771]: https://github.com/rojo-rbx/rojo/pull/771
[#774]: https://github.com/rojo-rbx/rojo/pull/774
[#783]: https://github.com/rojo-rbx/rojo/pull/783
[rbx-dom#299]: https://github.com/rojo-rbx/rbx-dom/pull/299
[rbx-dom#296]: https://github.com/rojo-rbx/rbx-dom/pull/296
## [7.3.0] - April 22, 2023
* Added `$attributes` to project format. ([#574])
* Added `--watch` flag to `rojo sourcemap`. ([#602])
* Added support for `init.csv` files. ([#594])
* Added real-time sync status to the Studio plugin. ([#569])
* Added support for copying error messages to the clipboard. ([#614])
* Added sync locking for Team Create. ([#590])
* Added support for specifying HTTP or HTTPS protocol in plugin. ([#642])
* Added tooltips to buttons in the Studio plugin. ([#637])
* Added visual diffs when connecting from the Studio plugin. ([#603])
* Host and port are now saved in the Studio plugin. ([#613])
* Improved padding on notifications in Studio plugin. ([#589])
* Renamed `Common` to `Shared` in the default Rojo project. ([#611])
* Reduced the minimum size of the Studio plugin widget. ([#606])
* Fixed current directory in `rojo fmt-project`. ([#581])
* Fixed errors after a session has already ended. ([#587])
* Fixed an uncommon security permission error ([#619])
[#569]: https://github.com/rojo-rbx/rojo/pull/569
[#574]: https://github.com/rojo-rbx/rojo/pull/574
[#581]: https://github.com/rojo-rbx/rojo/pull/581
[#587]: https://github.com/rojo-rbx/rojo/pull/587
[#589]: https://github.com/rojo-rbx/rojo/pull/589
[#590]: https://github.com/rojo-rbx/rojo/pull/590
[#594]: https://github.com/rojo-rbx/rojo/pull/594
[#602]: https://github.com/rojo-rbx/rojo/pull/602
[#603]: https://github.com/rojo-rbx/rojo/pull/603
[#606]: https://github.com/rojo-rbx/rojo/pull/606
[#611]: https://github.com/rojo-rbx/rojo/pull/611
[#613]: https://github.com/rojo-rbx/rojo/pull/613
[#614]: https://github.com/rojo-rbx/rojo/pull/614
[#619]: https://github.com/rojo-rbx/rojo/pull/619
[#637]: https://github.com/rojo-rbx/rojo/pull/637
[#642]: https://github.com/rojo-rbx/rojo/pull/642
[7.3.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.3.0
## [7.2.1] - July 8, 2022
* Fixed notification sound by changing it to a generic sound. ([#566])
* Added setting to turn off sound effects. ([#568])
[#566]: https://github.com/rojo-rbx/rojo/pull/566
[#568]: https://github.com/rojo-rbx/rojo/pull/568
[7.2.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.1
## [7.2.0] - June 29, 2022
* Added support for `.luau` files. ([#552])
* Added support for live syncing Attributes and Tags. ([#553])
* Added notification popups in the Roblox Studio plugin. ([#540])
* Fixed `init.meta.json` when used with `init.lua` and related files. ([#549])
* Fixed incorrect output when serving from a non-default address or port ([#556])
* Fixed Linux binaries not running on systems with older glibc. ([#561])
* Added `camelCase` casing for JSON models, deprecating `PascalCase` names. ([#563])
* Switched from structopt to clap for command line argument parsing.
* Significantly improved performance of building and serving. ([#548])
* Increased minimum supported Rust version to 1.57.0. ([#564])
[#540]: https://github.com/rojo-rbx/rojo/pull/540
[#548]: https://github.com/rojo-rbx/rojo/pull/548
[#549]: https://github.com/rojo-rbx/rojo/pull/549
[#552]: https://github.com/rojo-rbx/rojo/pull/552
[#553]: https://github.com/rojo-rbx/rojo/pull/553
[#556]: https://github.com/rojo-rbx/rojo/pull/556
[#561]: https://github.com/rojo-rbx/rojo/pull/561
[#563]: https://github.com/rojo-rbx/rojo/pull/563
[#564]: https://github.com/rojo-rbx/rojo/pull/564
[7.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.0
## [7.1.1] - May 26, 2022
* Fixed sourcemap command not stripping paths correctly ([#544])
* Fixed Studio plugin settings not saving correctly.
[#544]: https://github.com/rojo-rbx/rojo/pull/544
[#545]: https://github.com/rojo-rbx/rojo/pull/545
[7.1.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.1.1
## [7.1.0] - May 22, 2022 ## [7.1.0] - May 22, 2022
* Added support for specifying an address to be used by default in project files. ([#507]) * Added support for specifying an address to be used by default in project files. ([#507])
* Added support for optional paths in project files. ([#472]) * Added support for optional paths in project files. ([#472])
@@ -725,4 +491,4 @@ This is a general maintenance release for the Rojo 0.5.x release series.
* More robust syncing with a new reconciler * More robust syncing with a new reconciler
## [0.1.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.1.0) (November 29, 2017) ## [0.1.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.1.0) (November 29, 2017)
* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs) * Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)

View File

@@ -49,9 +49,11 @@ The Rojo release process is pretty manual right now. If you need to do it, here'
* `cargo publish` * `cargo publish`
8. Publish the Plugin 8. Publish the Plugin
* `cargo run -- upload plugin --asset_id 6415005344` * `cargo run -- upload plugin --asset_id 6415005344`
* `cargo run -- build plugin --output Rojo.rbxm`
9. Push commits and tags 9. Push commits and tags
* `git push && git push --tags` * `git push && git push --tags`
10. Copy GitHub release content from previous release 10. Copy GitHub release content from previous release
* Update the leading text with a summary about the release * Update the leading text with a summary about the release
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md) * Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
* Write a small summary of each major feature * Write a small summary of each major feature
* Attach release artifacts from GitHub Actions for each platform

1596
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.4.0-rc2" version = "7.1.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"
license = "MPL-2.0" license = "MPL-2.0"
@@ -9,10 +8,12 @@ homepage = "https://rojo.space"
documentation = "https://rojo.space/docs" documentation = "https://rojo.space/docs"
repository = "https://github.com/rojo-rbx/rojo" repository = "https://github.com/rojo-rbx/rojo"
readme = "README.md" readme = "README.md"
edition = "2021" edition = "2018"
build = "build.rs" build = "build.rs"
exclude = ["/test-projects/**"] exclude = [
"/test-projects/**",
]
[profile.dev] [profile.dev]
panic = "abort" panic = "abort"
@@ -26,10 +27,11 @@ 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 = []
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
[workspace] [workspace]
members = ["crates/*"] members = [
"rojo-insta-ext",
"memofs",
]
[lib] [lib]
name = "librojo" name = "librojo"
@@ -40,7 +42,7 @@ name = "build"
harness = false harness = false
[dependencies] [dependencies]
memofs = { version = "0.2.0", path = "crates/memofs" } memofs = { version = "0.2.0", path = "memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously # These dependencies can be uncommented when working on rbx-dom simultaneously
# rbx_binary = { path = "../rbx-dom/rbx_binary" } # rbx_binary = { path = "../rbx-dom/rbx_binary" }
@@ -49,11 +51,11 @@ memofs = { version = "0.2.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 = "0.7.2" rbx_binary = "0.6.4"
rbx_dom_weak = "2.6.0" rbx_dom_weak = "2.3.0"
rbx_reflection = "4.4.0" rbx_reflection = "4.2.0"
rbx_reflection_database = "0.2.8" rbx_reflection_database = "0.2.2"
rbx_xml = "0.13.2" rbx_xml = "0.12.3"
anyhow = "1.0.44" anyhow = "1.0.44"
backtrace = "0.3.61" backtrace = "0.3.61"
@@ -70,45 +72,35 @@ jod-thread = "0.1.2"
log = "0.4.14" log = "0.4.14"
maplit = "1.0.2" maplit = "1.0.2"
notify = "4.0.17" notify = "4.0.17"
num_cpus = "1.15.0"
opener = "0.5.0" opener = "0.5.0"
rayon = "1.7.0" reqwest = { version = "0.11.10", features = ["blocking", "json"] }
reqwest = { version = "0.11.10", features = [
"blocking",
"json",
"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.130", features = ["derive", "rc"] } serde = { version = "1.0.130", features = ["derive", "rc"] }
serde_json = "1.0.68" serde_json = "1.0.68"
toml = "0.5.9" structopt = "0.3.23"
termcolor = "1.1.2" termcolor = "1.1.2"
thiserror = "1.0.30" thiserror = "1.0.30"
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] } tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
uuid = { version = "1.0.0", features = ["v4", "serde"] } uuid = { version = "1.0.0", features = ["v4", "serde"] }
clap = { version = "3.1.18", features = ["derive"] }
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"
[build-dependencies] [build-dependencies]
memofs = { version = "0.2.0", path = "crates/memofs" } memofs = { version = "0.2.0", path = "memofs" }
embed-resource = "1.6.4" embed-resource = "1.6.4"
anyhow = "1.0.44" anyhow = "1.0.44"
bincode = "1.3.3" bincode = "1.3.3"
fs-err = "2.6.0" fs-err = "2.6.0"
maplit = "1.0.2" maplit = "1.0.2"
semver = "1.0.19"
[dev-dependencies] [dev-dependencies]
rojo-insta-ext = { path = "crates/rojo-insta-ext" } rojo-insta-ext = { path = "rojo-insta-ext" }
criterion = "0.3.5" criterion = "0.3.5"
insta = { version = "1.8.0", features = ["redactions", "yaml"] } insta = { version = "1.8.0", features = ["redactions"] }
paste = "1.0.5" paste = "1.0.5"
pretty_assertions = "1.2.1" pretty_assertions = "1.2.1"
serde_yaml = "0.8.21" serde_yaml = "0.8.21"

View File

@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
Pull requests are welcome! 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.46.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 +0,0 @@
[tools]
rojo = "rojo-rbx/rojo@7.3.0"
selene = "Kampfkarren/selene@0.25.0"
stylua = "JohnnyMorganz/stylua@0.18.2"
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"

Binary file not shown.

View File

@@ -2,7 +2,7 @@
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}. Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started ## Getting Started
To build this library, use: To build this library or plugin, use:
```bash ```bash
rojo build -o "{project_name}.rbxmx" rojo build -o "{project_name}.rbxmx"

View File

@@ -4,7 +4,7 @@
"$className": "DataModel", "$className": "DataModel",
"ReplicatedStorage": { "ReplicatedStorage": {
"Shared": { "Common": {
"$path": "src/shared" "$path": "src/shared"
} }
}, },

View File

@@ -1,17 +0,0 @@
# {project_name}
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
## Getting Started
To build this plugin to your local plugins folder, use:
```bash
rojo build -p "{project_name}.rbxm"
```
You can include the `watch` flag to re-build it on save:
```bash
rojo build -p "{project_name}.rbxm" --watch
```
For more help, check out [the Rojo documentation](https://rojo.space/docs).

View File

@@ -1,6 +0,0 @@
{
"name": "{project_name}",
"tree": {
"$path": "src"
}
}

View File

@@ -1,3 +0,0 @@
# Plugin model files
/{project_name}.rbxmx
/{project_name}.rbxm

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use librojo::cli::BuildCommand; use librojo::cli::{build, BuildCommand};
pub fn benchmark_small_place(c: &mut Criterion) { pub fn benchmark_small_place(c: &mut Criterion) {
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place") bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
@@ -20,7 +20,7 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
group.bench_function("build", |b| { group.bench_function("build", |b| {
b.iter_batched( b.iter_batched(
|| place_setup(path), || place_setup(path),
|(_dir, options)| options.run().unwrap(), |(_dir, options)| build(options).unwrap(),
BatchSize::SmallInput, BatchSize::SmallInput,
) )
}); });
@@ -31,12 +31,11 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) { fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let input = input_path.as_ref().to_path_buf(); let input = input_path.as_ref().to_path_buf();
let output = Some(dir.path().join("output.rbxlx")); let output = dir.path().join("output.rbxlx");
let options = BuildCommand { let options = BuildCommand {
project: input, project: input,
watch: false, watch: false,
plugin: None,
output, output,
}; };

5
bin/dev-plugin.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -e
watchexec -c -w plugin "sh -c './bin/install-dev-plugin.sh'"

13
bin/install-dev-plugin.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
set -e
DIR="$( mktemp -d )"
PLUGIN_FILE="$DIR/Rojo.rbxm"
TESTEZ_FILE="$DIR/TestEZ.rbxm"
rojo build plugin -o "$PLUGIN_FILE"
rojo build plugin/testez.project.json -o "$TESTEZ_FILE"
remodel bin/mark-plugin-as-dev.lua "$PLUGIN_FILE" "$TESTEZ_FILE" 2>/dev/null
cp "$PLUGIN_FILE" "$LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm"

5
bin/install-release-plugin.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -e
rojo build plugin -o "$LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm"

View File

@@ -0,0 +1,12 @@
local pluginPath, testezPath = ...
local plugin = remodel.readModelFile(pluginPath)[1]
local testez = remodel.readModelFile(testezPath)[1]
local marker = Instance.new("Folder")
marker.Name = "ROJO_DEV_BUILD"
marker.Parent = plugin
testez.Parent = plugin
remodel.writeModelFile(plugin, pluginPath)

View File

@@ -0,0 +1,8 @@
local pluginPath, placePath = ...
local plugin = remodel.readModelFile(pluginPath)[1]
local place = remodel.readPlaceFile(placePath)
plugin.Parent = place:GetService("ReplicatedStorage")
remodel.writePlaceFile(place, placePath)

6
bin/run-all-tests.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
set -e
./bin/run-cli-tests.sh
./bin/run-plugin-tests.sh

9
bin/run-cli-tests.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -e
cargo test --all --locked
cargo fmt -- --check
touch src/lib.rs # Nudge Rust source to make Clippy actually check things
cargo clippy

16
bin/run-plugin-tests.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
set -e
DIR="$( mktemp -d )"
PLUGIN_FILE="$DIR/Rojo.rbxmx"
PLACE_FILE="$DIR/RojoTestPlace.rbxlx"
rojo build plugin -o "$PLUGIN_FILE"
rojo build plugin/place.project.json -o "$PLACE_FILE"
remodel bin/put-plugin-in-test-place.lua "$PLUGIN_FILE" "$PLACE_FILE"
run-in-roblox -s plugin/testBootstrap.server.lua "$PLACE_FILE"
luacheck plugin/src plugin/log plugin/http

View File

@@ -7,7 +7,6 @@ use fs_err as fs;
use fs_err::File; use fs_err::File;
use maplit::hashmap; use maplit::hashmap;
use memofs::VfsSnapshot; use memofs::VfsSnapshot;
use semver::Version;
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> { fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
println!("cargo:rerun-if-changed={}", path.display()); println!("cargo:rerun-if-changed={}", path.display());
@@ -22,7 +21,7 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
// 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") {
continue; continue;
} }
@@ -44,18 +43,7 @@ fn main() -> Result<(), anyhow::Error> {
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
let plugin_root = PathBuf::from(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 plugin_modules = plugin_root.join("modules");
let plugin_version =
Version::parse(fs::read_to_string(&plugin_root.join("Version.txt"))?.trim())?;
assert!(
our_version.major == plugin_version.major,
"plugin version does not match Cargo version"
);
assert!(
our_version.minor == plugin_version.minor,
"plugin version does not match Cargo version"
);
let snapshot = VfsSnapshot::dir(hashmap! { let snapshot = VfsSnapshot::dir(hashmap! {
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?, "default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
@@ -64,12 +52,24 @@ fn main() -> Result<(), anyhow::Error> {
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?, "log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?, "rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?, "src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
"Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?, "modules" => VfsSnapshot::dir(hashmap! {
"Version.txt" => snapshot_from_fs_path(&plugin_root.join("Version.txt"))?, "roact" => VfsSnapshot::dir(hashmap! {
"src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))?
}),
"promise" => VfsSnapshot::dir(hashmap! {
"lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))?
}),
"t" => VfsSnapshot::dir(hashmap! {
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
}),
"flipper" => VfsSnapshot::dir(hashmap! {
"src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))?
}),
}),
}); });
let out_path = Path::new(&out_dir).join("plugin.bincode"); let out_path = Path::new(&out_dir).join("plugin.bincode");
let out_file = File::create(out_path)?; let out_file = File::create(&out_path)?;
bincode::serialize_into(out_file, &snapshot)?; bincode::serialize_into(out_file, &snapshot)?;

3
foreman.toml Normal file
View File

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

View File

@@ -1,7 +1,6 @@
# memofs Changelog # memofs Changelog
## Unreleased Changes ## Unreleased Changes
* Changed the `StdBackend` file watcher to use `PollWatcher` on macOS.
## 0.2.0 (2021-08-23) ## 0.2.0 (2021-08-23)
* Updated to `crossbeam-channel` 0.5.1. * Updated to `crossbeam-channel` 0.5.1.
@@ -16,4 +15,4 @@
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate. * Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
## 0.1.0 (2020-03-10) ## 0.1.0 (2020-03-10)
* Initial release * Initial release

View File

@@ -50,12 +50,6 @@ impl InMemoryFs {
} }
} }
impl Default for InMemoryFs {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)] #[derive(Debug)]
struct InMemoryFsInner { struct InMemoryFsInner {
entries: HashMap<PathBuf, Entry>, entries: HashMap<PathBuf, Entry>,

View File

@@ -194,8 +194,11 @@ impl VfsInner {
} }
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> { fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
if let VfsEvent::Remove(path) = event { match event {
let _ = self.backend.unwatch(path); VfsEvent::Remove(path) => {
let _ = self.backend.unwatch(&path);
}
_ => {}
} }
Ok(()) Ok(())

View File

@@ -74,9 +74,3 @@ impl VfsBackend for NoopBackend {
)) ))
} }
} }
impl Default for NoopBackend {
fn default() -> Self {
Self::new()
}
}

View File

@@ -5,34 +5,19 @@ use std::thread;
use std::time::Duration; use std::time::Duration;
use crossbeam_channel::Receiver; use crossbeam_channel::Receiver;
use notify::{DebouncedEvent, RecursiveMode, Watcher}; use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
#[cfg(target_os = "macos")]
use notify::PollWatcher;
#[cfg(not(target_os = "macos"))]
use notify::{watcher, RecommendedWatcher};
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent}; use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
/// `VfsBackend` that uses `std::fs` and the `notify` crate. /// `VfsBackend` that uses `std::fs` and the `notify` crate.
pub struct StdBackend { pub struct StdBackend {
// We use PollWatcher on macos because using the KQueue watcher
// can cause some gnarly performance problems.
#[cfg(target_os = "macos")]
watcher: PollWatcher,
#[cfg(not(target_os = "macos"))]
watcher: RecommendedWatcher, watcher: RecommendedWatcher,
watcher_receiver: Receiver<VfsEvent>, watcher_receiver: Receiver<VfsEvent>,
} }
impl StdBackend { impl StdBackend {
pub fn new() -> StdBackend { pub fn new() -> StdBackend {
let (notify_tx, notify_rx) = mpsc::channel(); let (notify_tx, notify_rx) = mpsc::channel();
#[cfg(target_os = "macos")]
let watcher = PollWatcher::new(notify_tx, Duration::from_millis(50)).unwrap();
#[cfg(not(target_os = "macos"))]
let watcher = watcher(notify_tx, Duration::from_millis(50)).unwrap(); let watcher = watcher(notify_tx, Duration::from_millis(50)).unwrap();
let (tx, rx) = crossbeam_channel::unbounded(); let (tx, rx) = crossbeam_channel::unbounded();
@@ -123,9 +108,3 @@ impl VfsBackend for StdBackend {
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner)) .map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
} }
} }
impl Default for StdBackend {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1 +0,0 @@
7.4.0-rc2

View File

@@ -5,23 +5,29 @@
"Plugin": { "Plugin": {
"$path": "src" "$path": "src"
}, },
"Packages": { "Log": {
"$path": "Packages", "$path": "log"
"Log": {
"$path": "log"
},
"Http": {
"$path": "http"
},
"Fmt": {
"$path": "fmt"
},
"RbxDom": {
"$path": "rbx_dom_lua"
}
}, },
"Version": { "Http": {
"$path": "Version.txt" "$path": "http"
},
"Fmt": {
"$path": "fmt"
},
"RbxDom": {
"$path": "rbx_dom_lua"
},
"Roact": {
"$path": "modules/roact/src"
},
"Promise": {
"$path": "modules/promise/lib"
},
"t": {
"$path": "modules/t/lib"
},
"Flipper": {
"$path": "modules/flipper/src"
} }
} }
} }

1
plugin/modules/roact Submodule

Submodule plugin/modules/roact added at f7d2f1ce1d

1
plugin/modules/t Submodule

Submodule plugin/modules/t added at f643b50682

1
plugin/modules/testez Submodule

Submodule plugin/modules/testez added at 25d957d4d5

View File

@@ -20,54 +20,11 @@ local function serializeFloat(value)
return value return value
end end
local ALL_AXES = { "X", "Y", "Z" } local ALL_AXES = {"X", "Y", "Z"}
local ALL_FACES = { "Right", "Top", "Back", "Left", "Bottom", "Front" } local ALL_FACES = {"Right", "Top", "Back", "Left", "Bottom", "Front"}
local EncodedValue = {}
local types local types
types = { types = {
Attributes = {
fromPod = function(pod)
local output = {}
for key, value in pairs(pod) do
local ok, result = EncodedValue.decode(value)
if ok then
output[key] = result
else
local warning = ("Could not decode attribute value of type %q: %s"):format(
typeof(value),
tostring(result)
)
warn(warning)
end
end
return output
end,
toPod = function(roblox)
local output = {}
for key, value in pairs(roblox) do
local ok, result = EncodedValue.encodeNaive(value)
if ok then
output[key] = result
else
local warning = ("Could not encode attribute value of type %q: %s"):format(
typeof(value),
tostring(result)
)
warn(warning)
end
end
return output
end,
},
Axes = { Axes = {
fromPod = function(pod) fromPod = function(pod)
local axes = {} local axes = {}
@@ -117,7 +74,6 @@ types = {
local pos = pod.position local pos = pod.position
local orient = pod.orientation local orient = pod.orientation
--stylua: ignore
return CFrame.new( return CFrame.new(
pos[1], pos[2], pos[3], pos[1], pos[2], pos[3],
orient[1][1], orient[1][2], orient[1][3], orient[1][1], orient[1][2], orient[1][3],
@@ -127,14 +83,17 @@ types = {
end, end,
toPod = function(roblox) toPod = function(roblox)
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = roblox:GetComponents() local x, y, z,
r00, r01, r02,
r10, r11, r12,
r20, r21, r22 = roblox:GetComponents()
return { return {
position = { x, y, z }, position = {x, y, z},
orientation = { orientation = {
{ r00, r01, r02 }, {r00, r01, r02},
{ r10, r11, r12 }, {r10, r11, r12},
{ r20, r21, r22 }, {r20, r21, r22},
}, },
} }
end, end,
@@ -144,7 +103,7 @@ types = {
fromPod = unpackDecoder(Color3.new), fromPod = unpackDecoder(Color3.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.r, roblox.g, roblox.b } return {roblox.r, roblox.g, roblox.b}
end, end,
}, },
@@ -165,7 +124,10 @@ types = {
local keypoints = {} local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = ColorSequenceKeypoint.new(keypoint.time, types.Color3.fromPod(keypoint.color)) keypoints[index] = ColorSequenceKeypoint.new(
keypoint.time,
types.Color3.fromPod(keypoint.color)
)
end end
return ColorSequence.new(keypoints) return ColorSequence.new(keypoints)
@@ -239,23 +201,6 @@ types = {
toPod = serializeFloat, toPod = serializeFloat,
}, },
Font = {
fromPod = function(pod)
return Font.new(
pod.family,
if pod.weight ~= nil then Enum.FontWeight[pod.weight] else nil,
if pod.style ~= nil then Enum.FontStyle[pod.style] else nil
)
end,
toPod = function(roblox)
return {
family = roblox.Family,
weight = roblox.Weight.Name,
style = roblox.Style.Name,
}
end,
},
Int32 = { Int32 = {
fromPod = identity, fromPod = identity,
toPod = identity, toPod = identity,
@@ -266,32 +211,11 @@ types = {
toPod = identity, toPod = identity,
}, },
MaterialColors = {
fromPod = function(pod: { [string]: { number } })
local real = {}
for name, color in pod do
real[Enum.Material[name]] = Color3.fromRGB(color[1], color[2], color[3])
end
return real
end,
toPod = function(roblox: { [Enum.Material]: Color3 })
local pod = {}
for material, color in roblox do
pod[material.Name] = {
math.round(math.clamp(color.R, 0, 1) * 255),
math.round(math.clamp(color.G, 0, 1) * 255),
math.round(math.clamp(color.B, 0, 1) * 255),
}
end
return pod
end,
},
NumberRange = { NumberRange = {
fromPod = unpackDecoder(NumberRange.new), fromPod = unpackDecoder(NumberRange.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.Min, roblox.Max } return {roblox.Min, roblox.Max}
end, end,
}, },
@@ -300,7 +224,11 @@ types = {
local keypoints = {} local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do for index, keypoint in ipairs(pod.keypoints) do
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope) keypoints[index] = NumberSequenceKeypoint.new(
keypoint.time,
keypoint.value,
keypoint.envelope
)
end end
return NumberSequence.new(keypoints) return NumberSequence.new(keypoints)
@@ -355,7 +283,10 @@ types = {
Ray = { Ray = {
fromPod = function(pod) fromPod = function(pod)
return Ray.new(types.Vector3.fromPod(pod.origin), types.Vector3.fromPod(pod.direction)) return Ray.new(
types.Vector3.fromPod(pod.origin),
types.Vector3.fromPod(pod.direction)
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -368,7 +299,10 @@ types = {
Rect = { Rect = {
fromPod = function(pod) fromPod = function(pod)
return Rect.new(types.Vector2.fromPod(pod[1]), types.Vector2.fromPod(pod[2])) return Rect.new(
types.Vector2.fromPod(pod[1]),
types.Vector2.fromPod(pod[2])
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -380,28 +314,31 @@ types = {
}, },
Ref = { Ref = {
fromPod = function(_) fromPod = function(_pod)
error("Ref cannot be decoded on its own") error("Ref cannot be decoded on its own")
end, end,
toPod = function(_) toPod = function(_roblox)
error("Ref can not be encoded on its own") error("Ref can not be encoded on its own")
end, end,
}, },
Region3 = { Region3 = {
fromPod = function(_) fromPod = function(pod)
error("Region3 is not implemented") error("Region3 is not implemented")
end, end,
toPod = function(_) toPod = function(roblox)
error("Region3 is not implemented") error("Region3 is not implemented")
end, end,
}, },
Region3int16 = { Region3int16 = {
fromPod = function(pod) fromPod = function(pod)
return Region3int16.new(types.Vector3int16.fromPod(pod[1]), types.Vector3int16.fromPod(pod[2])) return Region3int16.new(
types.Vector3int16.fromPod(pod[1]),
types.Vector3int16.fromPod(pod[2])
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -412,21 +349,12 @@ types = {
end, end,
}, },
SecurityCapabilities = {
fromPod = function(_pod)
error("SecurityCapabilities is not implemented")
end,
toPod = function(_roblox)
error("SecurityCapabilities is not implemented")
end,
},
SharedString = { SharedString = {
fromPod = function(_pod) fromPod = function(pod)
error("SharedString is not supported") error("SharedString is not supported")
end, end,
toPod = function(_roblox) toPod = function(roblox)
error("SharedString is not supported") error("SharedString is not supported")
end, end,
}, },
@@ -440,13 +368,16 @@ types = {
fromPod = unpackDecoder(UDim.new), fromPod = unpackDecoder(UDim.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.Scale, roblox.Offset } return {roblox.Scale, roblox.Offset}
end, end,
}, },
UDim2 = { UDim2 = {
fromPod = function(pod) fromPod = function(pod)
return UDim2.new(types.UDim.fromPod(pod[1]), types.UDim.fromPod(pod[2])) return UDim2.new(
types.UDim.fromPod(pod[1]),
types.UDim.fromPod(pod[2])
)
end, end,
toPod = function(roblox) toPod = function(roblox)
@@ -477,7 +408,7 @@ types = {
fromPod = unpackDecoder(Vector2int16.new), fromPod = unpackDecoder(Vector2int16.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.X, roblox.Y } return {roblox.X, roblox.Y}
end, end,
}, },
@@ -497,11 +428,13 @@ types = {
fromPod = unpackDecoder(Vector3int16.new), fromPod = unpackDecoder(Vector3int16.new),
toPod = function(roblox) toPod = function(roblox)
return { roblox.X, roblox.Y, roblox.Z } return {roblox.X, roblox.Y, roblox.Z}
end, end,
}, },
} }
local EncodedValue = {}
function EncodedValue.decode(encodedValue) function EncodedValue.decode(encodedValue)
local ty, value = next(encodedValue) local ty, value = next(encodedValue)
@@ -526,19 +459,4 @@ function EncodedValue.encode(rbxValue, propertyType)
} }
end end
local propertyTypeRenames = {
number = "Float64",
boolean = "Bool",
string = "String",
}
function EncodedValue.encodeNaive(rbxValue)
local propertyType = typeof(rbxValue)
if propertyTypeRenames[propertyType] ~= nil then
propertyType = propertyTypeRenames[propertyType]
end
return EncodedValue.encode(rbxValue, propertyType)
end
return EncodedValue return EncodedValue

View File

@@ -0,0 +1,72 @@
return function()
local HttpService = game:GetService("HttpService")
local EncodedValue = require(script.Parent.EncodedValue)
local allValues = require(script.Parent.allValues)
local function deepEq(a, b)
if typeof(a) ~= typeof(b) then
return false
end
local ty = typeof(a)
if ty == "table" then
local visited = {}
for key, valueA in pairs(a) do
visited[key] = true
if not deepEq(valueA, b[key]) then
return false
end
end
for key, valueB in pairs(b) do
if visited[key] then
continue
end
if not deepEq(valueB, a[key]) then
return false
end
end
return true
else
return a == b
end
end
local extraAssertions = {
CFrame = function(value)
expect(value).to.equal(CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
end,
}
for testName, testEntry in pairs(allValues) do
it("round trip " .. testName, function()
local ok, decoded = EncodedValue.decode(testEntry.value)
assert(ok, decoded)
if extraAssertions[testName] ~= nil then
extraAssertions[testName](decoded)
end
local ok, encoded = EncodedValue.encode(decoded, testEntry.ty)
assert(ok, encoded)
if not deepEq(encoded, testEntry.value) then
local expected = HttpService:JSONEncode(testEntry.value)
local actual = HttpService:JSONEncode(encoded)
local message = string.format(
"Round-trip results did not match.\nExpected:\n%s\nActual:\n%s",
expected, actual
)
error(message)
end
end)
end
end

View File

@@ -25,4 +25,4 @@ function Error:__tostring()
return ("Error(%s: %s)"):format(self.kind, tostring(self.extra)) return ("Error(%s: %s)"):format(self.kind, tostring(self.extra))
end end
return Error return Error

View File

@@ -53,11 +53,6 @@ function PropertyDescriptor:read(instance)
end end
if self.scriptability == "Custom" then if self.scriptability == "Custom" then
if customProperties[self.className] == nil then
local fullName = ("%s.%s"):format(instance.className, self.name)
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
end
local interface = customProperties[self.className][self.name] local interface = customProperties[self.className][self.name]
return interface.read(instance, self.name) return interface.read(instance, self.name)
@@ -84,11 +79,6 @@ function PropertyDescriptor:write(instance, value)
end end
if self.scriptability == "Custom" then if self.scriptability == "Custom" then
if customProperties[self.className] == nil then
local fullName = ("%s.%s"):format(instance.className, self.name)
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
end
local interface = customProperties[self.className][self.name] local interface = customProperties[self.className][self.name]
return interface.write(instance, self.name, value) return interface.write(instance, self.name, value)

View File

@@ -1,73 +1,4 @@
{ {
"Attributes": {
"value": {
"Attributes": {
"TestBool": {
"Bool": true
},
"TestBrickColor": {
"BrickColor": 24
},
"TestColor3": {
"Color3": [
1.0,
0.5,
0.0
]
},
"TestNumber": {
"Float64": 1337.0
},
"TestRect": {
"Rect": [
[
1.0,
2.0
],
[
3.0,
4.0
]
]
},
"TestString": {
"String": "Test"
},
"TestUDim": {
"UDim": [
1.0,
2
]
},
"TestUDim2": {
"UDim2": [
[
1.0,
2
],
[
3.0,
4
]
]
},
"TestVector2": {
"Vector2": [
1.0,
2.0
]
},
"TestVector3": {
"Vector3": [
1.0,
2.0,
3.0
]
}
}
},
"ty": "Attributes"
},
"Axes": { "Axes": {
"value": { "value": {
"Axes": [ "Axes": [
@@ -207,17 +138,6 @@
}, },
"ty": "Float64" "ty": "Float64"
}, },
"Font": {
"value": {
"Font": {
"family": "rbxasset://fonts/families/SourceSansPro.json",
"weight": "Regular",
"style": "Normal",
"cachedFaceId": null
}
},
"ty": "Font"
},
"Int32": { "Int32": {
"value": { "value": {
"Int32": 6014 "Int32": 6014
@@ -230,118 +150,6 @@
}, },
"ty": "Int64" "ty": "Int64"
}, },
"MaterialColors": {
"value": {
"MaterialColors": {
"Grass": [
106,
127,
63
],
"Slate": [
63,
127,
107
],
"Concrete": [
127,
102,
63
],
"Brick": [
138,
86,
62
],
"Sand": [
143,
126,
95
],
"WoodPlanks": [
139,
109,
79
],
"Rock": [
102,
108,
111
],
"Glacier": [
101,
176,
234
],
"Snow": [
195,
199,
218
],
"Sandstone": [
137,
90,
71
],
"Mud": [
58,
46,
36
],
"Basalt": [
30,
30,
37
],
"Ground": [
102,
92,
59
],
"CrackedLava": [
232,
156,
74
],
"Asphalt": [
115,
123,
107
],
"Cobblestone": [
132,
123,
90
],
"Ice": [
129,
194,
224
],
"LeafyGrass": [
115,
132,
74
],
"Salt": [
198,
189,
181
],
"Limestone": [
206,
173,
148
],
"Pavement": [
148,
148,
140
]
}
},
"ty": "MaterialColors"
},
"NumberRange": { "NumberRange": {
"value": { "value": {
"NumberRange": [ "NumberRange": [

View File

@@ -136,4 +136,4 @@ end
return { return {
decode = decodeBase64, decode = decodeBase64,
encode = encodeBase64, encode = encodeBase64,
} }

View File

@@ -0,0 +1,29 @@
return function()
local base64 = require(script.Parent.base64)
it("should encode and decode", function()
local function try(str, expected)
local encoded = base64.encode(str)
expect(encoded).to.equal(expected)
expect(base64.decode(encoded)).to.equal(str)
end
try("Man", "TWFu")
try("Ma", "TWE=")
try("M", "TQ==")
try("ManM", "TWFuTQ==")
try(
[[Man is distinguished, not only by his reason, but by this ]]..
[[singular passion from other animals, which is a lust of the ]]..
[[mind, that by a perseverance of delight in the continued and ]]..
[[indefatigable generation of knowledge, exceeds the short ]]..
[[vehemence of any carnal pleasure.]],
[[TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sI]]..
[[GJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYW]]..
[[xzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJ]]..
[[zZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRl]]..
[[ZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZ]]..
[[SBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=]]
)
end)
end

View File

@@ -1,55 +1,10 @@
local CollectionService = game:GetService("CollectionService") local CollectionService = game:GetService("CollectionService")
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
local TERRAIN_MATERIAL_COLORS = {
Enum.Material.Grass,
Enum.Material.Slate,
Enum.Material.Concrete,
Enum.Material.Brick,
Enum.Material.Sand,
Enum.Material.WoodPlanks,
Enum.Material.Rock,
Enum.Material.Glacier,
Enum.Material.Snow,
Enum.Material.Sandstone,
Enum.Material.Mud,
Enum.Material.Basalt,
Enum.Material.Ground,
Enum.Material.CrackedLava,
Enum.Material.Asphalt,
Enum.Material.Cobblestone,
Enum.Material.Ice,
Enum.Material.LeafyGrass,
Enum.Material.Salt,
Enum.Material.Limestone,
Enum.Material.Pavement,
}
-- Defines how to read and write properties that aren't directly scriptable. -- Defines how to read and write properties that aren't directly scriptable.
-- --
-- The reflection database refers to these as having scriptability = "Custom" -- The reflection database refers to these as having scriptability = "Custom"
return { return {
Instance = { Instance = {
Attributes = {
read = function(instance)
return true, instance:GetAttributes()
end,
write = function(instance, _, value)
local existing = instance:GetAttributes()
for key, attr in pairs(value) do
instance:SetAttribute(key, attr)
end
for key in pairs(existing) do
if value[key] == nil then
instance:SetAttribute(key, nil)
end
end
return true
end,
},
Tags = { Tags = {
read = function(instance) read = function(instance)
return true, CollectionService:GetTags(instance) return true, CollectionService:GetTags(instance)
@@ -77,43 +32,13 @@ return {
}, },
LocalizationTable = { LocalizationTable = {
Contents = { Contents = {
read = function(instance, _) read = function(instance, key)
return true, instance:GetContents() return true, instance:GetContents()
end, end,
write = function(instance, _, value) write = function(instance, key, value)
instance:SetContents(value) instance:SetContents(value)
return true return true
end, end,
}, },
}, },
Model = {
Scale = {
read = function(instance, _, _)
return true, instance:GetScale()
end,
write = function(instance, _, value)
return true, instance:ScaleTo(value)
end,
},
},
Terrain = {
MaterialColors = {
read = function(instance: Terrain)
-- There's no way to get a list of every color, so we have to
-- make one.
local colors = {}
for _, material in TERRAIN_MATERIAL_COLORS do
colors[material] = instance:GetMaterialColor(material)
end
return true, colors
end,
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
for material, color in value do
instance:SetMaterialColor(material, color)
end
return true
end,
},
},
} }

File diff suppressed because it is too large Load Diff

View File

@@ -24,8 +24,7 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
return PropertyDescriptor.fromRaw( return PropertyDescriptor.fromRaw(
currentClass.Properties[aliasData.AliasFor], currentClass.Properties[aliasData.AliasFor],
currentClassName, currentClassName,
aliasData.AliasFor aliasData.AliasFor)
)
end end
return nil return nil

View File

@@ -0,0 +1,7 @@
return function()
local RbxDom = require(script.Parent)
it("should load", function()
expect(RbxDom).to.be.ok()
end)
end

View File

@@ -1,11 +1,19 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.Packages.TestEZ) local TestEZ = require(ReplicatedStorage.TestEZ)
local Rojo = ReplicatedStorage.Rojo local Rojo = ReplicatedStorage.Rojo
local Settings = require(Rojo.Plugin.Settings) local DevSettings = require(Rojo.Plugin.DevSettings)
Settings:set("logLevel", "Trace")
Settings:set("typecheckingEnabled", true) local setDevSettings = not DevSettings:hasChangedValues()
if setDevSettings then
DevSettings:createTestSettings()
end
require(Rojo.Plugin.runTests)(TestEZ) require(Rojo.Plugin.runTests)(TestEZ)
if setDevSettings then
DevSettings:resetValues()
end

View File

@@ -1,7 +1,6 @@
local Packages = script.Parent.Parent.Packages local Http = require(script.Parent.Parent.Http)
local Http = require(Packages.Http) local Log = require(script.Parent.Parent.Log)
local Log = require(Packages.Log) local Promise = require(script.Parent.Parent.Promise)
local Promise = require(Packages.Promise)
local Config = require(script.Parent.Config) local Config = require(script.Parent.Config)
local Types = require(script.Parent.Types) local Types = require(script.Parent.Types)
@@ -11,6 +10,13 @@ local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse) local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse) local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
--[[
Returns a promise that will never resolve nor reject.
]]
local function hangingPromise()
return Promise.new(function() end)
end
local function rejectFailedRequests(response) local function rejectFailedRequests(response)
if response.code >= 400 then if response.code >= 400 then
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body) local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
@@ -24,17 +30,15 @@ end
local function rejectWrongProtocolVersion(infoResponseBody) local function rejectWrongProtocolVersion(infoResponseBody)
if infoResponseBody.protocolVersion ~= Config.protocolVersion then if infoResponseBody.protocolVersion ~= Config.protocolVersion then
local message = ( local message = (
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." "Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
.. "\nMake sure you have matching versions of both the Rojo plugin and server!" "\nMake sure you have matching versions of both the Rojo plugin and server!" ..
.. "\n\nYour client is version %s, with protocol version %s. It expects server version %s." "\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
.. "\nYour server is version %s, with protocol version %s." "\nYour server is version %s, with protocol version %s." ..
.. "\n\nGo to https://github.com/rojo-rbx/rojo for more details." "\n\nGo to https://github.com/rojo-rbx/rojo for more details."
):format( ):format(
Version.display(Config.version), Version.display(Config.version), Config.protocolVersion,
Config.protocolVersion,
Config.expectedServerVersionString, Config.expectedServerVersionString,
infoResponseBody.serverVersion, infoResponseBody.serverVersion, infoResponseBody.protocolVersion
infoResponseBody.protocolVersion
) )
return Promise.reject(message) return Promise.reject(message)
@@ -61,11 +65,14 @@ local function rejectWrongPlaceId(infoResponseBody)
end end
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 %s, 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(tostring(game.PlaceId), table.concat(idList, "\n")) ):format(
tostring(game.PlaceId),
table.concat(idList, "\n")
)
return Promise.reject(message) return Promise.reject(message)
end end
@@ -78,14 +85,13 @@ local ApiContext = {}
ApiContext.__index = ApiContext ApiContext.__index = ApiContext
function ApiContext.new(baseUrl) function ApiContext.new(baseUrl)
assert(type(baseUrl) == "string", "baseUrl must be a string") assert(type(baseUrl) == "string")
local self = { local self = {
__baseUrl = baseUrl, __baseUrl = baseUrl,
__sessionId = nil, __sessionId = nil,
__messageCursor = -1, __messageCursor = -1,
__connected = true, __connected = true,
__activeRequests = {},
} }
return setmetatable(self, ApiContext) return setmetatable(self, ApiContext)
@@ -106,11 +112,6 @@ end
function ApiContext:disconnect() function ApiContext:disconnect()
self.__connected = false self.__connected = false
for request in self.__activeRequests do
Log.trace("Cancelling request {}", request)
request:cancel()
end
self.__activeRequests = {}
end end
function ApiContext:setMessageCursor(index) function ApiContext:setMessageCursor(index)
@@ -140,15 +141,18 @@ end
function ApiContext:read(ids) function ApiContext:read(ids)
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ",")) local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) return Http.get(url)
if body.sessionId ~= self.__sessionId then :andThen(rejectFailedRequests)
return Promise.reject("Server changed ID") :andThen(Http.Response.json)
end :andThen(function(body)
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
assert(validateApiRead(body)) assert(validateApiRead(body))
return body return body
end) end)
end end
function ApiContext:write(patch) function ApiContext:write(patch)
@@ -185,58 +189,63 @@ function ApiContext:write(patch)
body = Http.jsonEncode(body) body = Http.jsonEncode(body)
return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) return Http.post(url, body)
Log.info("Write response: {:?}", body) :andThen(rejectFailedRequests)
:andThen(Http.Response.json)
:andThen(function(body)
Log.info("Write response: {:?}", body)
return body return body
end) end)
end end
function ApiContext:retrieveMessages() function ApiContext:retrieveMessages()
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor) local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
local function sendRequest() local function sendRequest()
local request = Http.get(url):catch(function(err) return Http.get(url)
if err.type == Http.Error.Kind.Timeout and self.__connected then :catch(function(err)
return sendRequest() if err.type == Http.Error.Kind.Timeout then
end if self.__connected then
return sendRequest()
else
return hangingPromise()
end
end
return Promise.reject(err) return Promise.reject(err)
end) end)
Log.trace("Tracking request {}", request)
self.__activeRequests[request] = true
return request:finally(function(...)
Log.trace("Cleaning up request {}", request)
self.__activeRequests[request] = nil
return ...
end)
end end
return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) return sendRequest()
if body.sessionId ~= self.__sessionId then :andThen(rejectFailedRequests)
return Promise.reject("Server changed ID") :andThen(Http.Response.json)
end :andThen(function(body)
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
assert(validateApiSubscribe(body)) assert(validateApiSubscribe(body))
self:setMessageCursor(body.messageCursor) self:setMessageCursor(body.messageCursor)
return body.messages return body.messages
end) end)
end end
function ApiContext:open(id) function ApiContext:open(id)
local url = ("%s/api/open/%s"):format(self.__baseUrl, id) local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) return Http.post(url, "")
if body.sessionId ~= self.__sessionId then :andThen(rejectFailedRequests)
return Promise.reject("Server changed ID") :andThen(Http.Response.json)
end :andThen(function(body)
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
return nil return nil
end) end)
end end
return ApiContext return ApiContext

View File

@@ -1,8 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -24,10 +23,8 @@ local function BorderedContainer(props)
layoutOrder = props.layoutOrder, layoutOrder = props.layoutOrder,
}, { }, {
Content = e("Frame", { Content = e("Frame", {
Size = UDim2.new(1, -2, 1, -2), Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 1, 0, 1),
BackgroundTransparency = 1, BackgroundTransparency = 1,
ZIndex = 2,
}, props[Roact.Children]), }, props[Roact.Children]),
Border = e(SlicedImage, { Border = e(SlicedImage, {
@@ -41,4 +38,4 @@ local function BorderedContainer(props)
end) end)
end end
return BorderedContainer return BorderedContainer

View File

@@ -1,16 +1,14 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Flipper = require(Packages.Flipper) local Flipper = require(Rojo.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 SlicedImage = require(script.Parent.SlicedImage) local SlicedImage = require(script.Parent.SlicedImage)
local Tooltip = require(script.Parent.Tooltip)
local e = Roact.createElement local e = Roact.createElement
@@ -23,10 +21,12 @@ end
function Checkbox:didUpdate(lastProps) function Checkbox:didUpdate(lastProps)
if lastProps.active ~= self.props.active then if lastProps.active ~= self.props.active then
self.motor:setGoal(Flipper.Spring.new(self.props.active and 1 or 0, { self.motor:setGoal(
frequency = 6, Flipper.Spring.new(self.props.active and 1 or 0, {
dampingRatio = 1.1, frequency = 6,
})) dampingRatio = 1.1,
})
)
end end
end end
@@ -49,18 +49,8 @@ function Checkbox:render()
ZIndex = self.props.zIndex, ZIndex = self.props.zIndex,
BackgroundTransparency = 1, BackgroundTransparency = 1,
[Roact.Event.Activated] = function() [Roact.Event.Activated] = self.props.onClick,
if self.props.locked then
return
end
self.props.onClick()
end,
}, { }, {
StateTip = e(Tooltip.Trigger, {
text = (if self.props.locked then "[LOCKED] " else "")
.. (if self.props.active then "Enabled" else "Disabled"),
}),
Active = e(SlicedImage, { Active = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = theme.Active.BackgroundColor, color = theme.Active.BackgroundColor,
@@ -69,7 +59,7 @@ function Checkbox:render()
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 = Assets.Images.Checkbox.Active,
ImageColor3 = theme.Active.IconColor, ImageColor3 = theme.Active.IconColor,
ImageTransparency = activeTransparency, ImageTransparency = activeTransparency,
@@ -88,9 +78,7 @@ function Checkbox:render()
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
}, { }, {
Icon = e("ImageLabel", { Icon = e("ImageLabel", {
Image = if self.props.locked Image = Assets.Images.Checkbox.Inactive,
then Assets.Images.Checkbox.Locked
else Assets.Images.Checkbox.Inactive,
ImageColor3 = theme.Inactive.IconColor, ImageColor3 = theme.Inactive.IconColor,
ImageTransparency = self.props.transparency, ImageTransparency = self.props.transparency,
@@ -105,4 +93,4 @@ function Checkbox:render()
end) end)
end end
return Checkbox return Checkbox

View File

@@ -1,61 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Highlighter = require(Packages.Highlighter)
Highlighter.matchStudioSettings()
local e = Roact.createElement
local CodeLabel = Roact.PureComponent:extend("CodeLabel")
function CodeLabel:init()
self.labelRef = Roact.createRef()
self.highlightsRef = Roact.createRef()
end
function CodeLabel:didMount()
Highlighter.highlight({
textObject = self.labelRef:getValue(),
})
self:updateHighlights()
end
function CodeLabel:didUpdate()
self:updateHighlights()
end
function CodeLabel:updateHighlights()
local highlights = self.highlightsRef:getValue()
if not highlights then
return
end
for _, lineLabel in highlights:GetChildren() do
local lineNum = tonumber(string.match(lineLabel.Name, "%d+") or "0")
lineLabel.BackgroundColor3 = self.props.lineBackground
lineLabel.BorderSizePixel = 0
lineLabel.BackgroundTransparency = if self.props.markedLines[lineNum] then 0.25 else 1
end
end
function CodeLabel:render()
return e("TextLabel", {
Size = self.props.size,
Position = self.props.position,
Text = self.props.text,
BackgroundTransparency = 1,
Font = Enum.Font.RobotoMono,
TextSize = 16,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextColor3 = Color3.fromRGB(255, 255, 255),
[Roact.Ref] = self.labelRef,
}, {
SyntaxHighlights = e("Folder", {
[Roact.Ref] = self.highlightsRef,
}),
})
end
return CodeLabel

View File

@@ -1,178 +0,0 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local bindingUtil = require(Plugin.App.bindingUtil)
local SlicedImage = require(script.Parent.SlicedImage)
local ScrollingFrame = require(script.Parent.ScrollingFrame)
local e = Roact.createElement
local Dropdown = Roact.Component:extend("Dropdown")
function Dropdown:init()
self.openMotor = Flipper.SingleMotor.new(0)
self.openBinding = bindingUtil.fromMotor(self.openMotor)
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
self:setState({
open = false,
})
end
function Dropdown:didUpdate(prevProps)
if self.props.locked and not prevProps.locked then
self:setState({
open = false,
})
end
self.openMotor:setGoal(Flipper.Spring.new(self.state.open and 1 or 0, {
frequency = 6,
dampingRatio = 1.1,
}))
end
function Dropdown:render()
return Theme.with(function(theme)
theme = theme.Dropdown
local optionButtons = {}
local width = -1
for i, option in self.props.options do
local text = tostring(option or "")
local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
if textSize.X > width then
width = textSize.X
end
optionButtons[text] = e("TextButton", {
Text = text,
LayoutOrder = i,
Size = UDim2.new(1, 0, 0, 24),
BackgroundColor3 = theme.BackgroundColor,
TextTransparency = self.props.transparency,
BackgroundTransparency = self.props.transparency,
BorderSizePixel = 0,
TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 15,
Font = Enum.Font.GothamMedium,
[Roact.Event.Activated] = function()
if self.props.locked then
return
end
self:setState({
open = false,
})
self.props.onClick(option)
end,
}, {
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 6),
}),
})
end
return e("ImageButton", {
Size = UDim2.new(0, width + 50, 0, 28),
Position = self.props.position,
AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder,
ZIndex = self.props.zIndex,
BackgroundTransparency = 1,
[Roact.Event.Activated] = function()
if self.props.locked then
return
end
self:setState({
open = not self.state.open,
})
end,
}, {
Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = theme.BorderColor,
transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0),
}, {
DropArrow = e("ImageLabel", {
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
ImageColor3 = self.openBinding:map(function(a)
return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a)
end),
ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 18, 0, 18),
Position = UDim2.new(1, -6, 0.5, 0),
AnchorPoint = Vector2.new(1, 0.5),
Rotation = self.openBinding:map(function(a)
return a * 180
end),
BackgroundTransparency = 1,
}),
Active = e("TextLabel", {
Size = UDim2.new(1, -30, 1, 0),
Position = UDim2.new(0, 6, 0, 0),
BackgroundTransparency = 1,
Text = self.props.active,
Font = Enum.Font.GothamMedium,
TextSize = 15,
TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
}),
}),
Options = if self.state.open
then e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = theme.BackgroundColor,
position = UDim2.new(1, 0, 1, 3),
size = self.openBinding:map(function(a)
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
end),
anchorPoint = Vector2.new(1, 0),
}, {
Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = theme.BorderColor,
transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0),
}),
ScrollingFrame = e(ScrollingFrame, {
size = UDim2.new(1, -4, 1, -4),
position = UDim2.new(0, 2, 0, 2),
transparency = self.props.transparency,
contentSize = self.contentSize,
}, {
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 0),
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Roact.createFragment(optionButtons),
}),
})
else nil,
})
end)
end
return Dropdown

View File

@@ -1,8 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -53,4 +52,4 @@ local function Header(props)
end) end)
end end
return Header return Header

View File

@@ -1,9 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Flipper = require(Packages.Flipper) local Flipper = require(Rojo.Flipper)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil) local bindingUtil = require(Plugin.App.bindingUtil)
@@ -30,7 +29,6 @@ function IconButton:render()
Position = self.props.position, Position = self.props.position,
AnchorPoint = self.props.anchorPoint, AnchorPoint = self.props.anchorPoint,
Visible = self.props.visible,
LayoutOrder = self.props.layoutOrder, LayoutOrder = self.props.layoutOrder,
ZIndex = self.props.zIndex, ZIndex = self.props.zIndex,
BackgroundTransparency = 1, BackgroundTransparency = 1,
@@ -38,11 +36,15 @@ function IconButton:render()
[Roact.Event.Activated] = self.props.onClick, [Roact.Event.Activated] = self.props.onClick,
[Roact.Event.MouseEnter] = function() [Roact.Event.MouseEnter] = function()
self.motor:setGoal(Flipper.Spring.new(1, HOVER_SPRING_PROPS)) self.motor:setGoal(
Flipper.Spring.new(1, HOVER_SPRING_PROPS)
)
end, end,
[Roact.Event.MouseLeave] = function() [Roact.Event.MouseLeave] = function()
self.motor:setGoal(Flipper.Spring.new(0, HOVER_SPRING_PROPS)) self.motor:setGoal(
Flipper.Spring.new(0, HOVER_SPRING_PROPS)
)
end, end,
}, { }, {
Icon = e("ImageLabel", { Icon = e("ImageLabel", {
@@ -71,9 +73,7 @@ function IconButton:render()
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
Children = Roact.createFragment(self.props[Roact.Children]),
}) })
end end
return IconButton return IconButton

View File

@@ -1,265 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local DisplayValue = require(script.Parent.DisplayValue)
local EMPTY_TABLE = {}
local e = Roact.createElement
local ChangeList = Roact.Component:extend("ChangeList")
function ChangeList:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
end
function ChangeList:render()
return Theme.with(function(theme)
local props = self.props
local changes = props.changes
-- Color alternating rows for readability
local rowTransparency = props.transparency:map(function(t)
return 0.93 + (0.07 * t)
end)
local rows = {}
local pad = {
PaddingLeft = UDim.new(0, 5),
PaddingRight = UDim.new(0, 5),
}
local headers = e("Frame", {
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = rowTransparency,
BackgroundColor3 = theme.Diff.Row,
LayoutOrder = 0,
}, {
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 = tostring(changes[1][1]),
BackgroundTransparency = 1,
Font = Enum.Font.GothamBold,
TextSize = 14,
TextColor3 = 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,
}),
B = e("TextLabel", {
Text = tostring(changes[1][2]),
BackgroundTransparency = 1,
Font = Enum.Font.GothamBold,
TextSize = 14,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 2,
}),
C = e("TextLabel", {
Text = tostring(changes[1][3]),
BackgroundTransparency = 1,
Font = Enum.Font.GothamBold,
TextSize = 14,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 3,
}),
})
for row, values in changes do
if row == 1 then
continue -- Skip headers, already handled above
end
local metadata = values[4] or EMPTY_TABLE
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", {
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,
}),
B = e(
"Frame",
{
BackgroundTransparency = 1,
Size = UDim2.new(0.35, 0, 1, 0),
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
table.insert(
rows,
e("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Top,
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
})
)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}, {
Headers = headers,
Values = e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, -30),
position = UDim2.new(0, 0, 0, 30),
contentSize = self.contentSize,
transparency = props.transparency,
}, rows),
})
end)
end
return ChangeList

View File

@@ -1,106 +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 e = Roact.createElement
local function DisplayValue(props)
return Theme.with(function(theme)
local t = typeof(props.value)
if t == "Color3" then
-- Colors get a blot that shows the color
return Roact.createFragment({
Blot = e("Frame", {
BackgroundTransparency = props.transparency,
BackgroundColor3 = props.value,
Size = UDim2.new(0, 20, 0, 20),
Position = UDim2.new(0, 0, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
}, {
Corner = e("UICorner", {
CornerRadius = UDim.new(0, 4),
}),
Stroke = e("UIStroke", {
Color = theme.BorderedContainer.BorderColor,
Transparency = props.transparency,
}),
}),
Label = e("TextLabel", {
Text = string.format("%d,%d,%d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(1, -25, 1, 0),
Position = UDim2.new(0, 25, 0, 0),
}),
})
elseif t == "table" then
-- Showing a memory address for tables is useless, so we want to show the best we can
local textRepresentation = nil
local meta = getmetatable(props.value)
if meta and meta.__tostring then
-- If the table has a tostring metamethod, use that
textRepresentation = tostring(props.value)
elseif next(props.value) == nil then
-- If it's empty, show empty braces
textRepresentation = "{}"
else
-- If it has children, list them out
local out, i = {}, 0
for k, v in pairs(props.value) do
i += 1
-- Wrap strings in quotes
if type(k) == "string" then
k = '"' .. k .. '"'
end
if type(v) == "string" then
v = '"' .. v .. '"'
end
out[i] = string.format("[%s] = %s", tostring(k), tostring(v))
end
textRepresentation = "{ " .. table.concat(out, ", ") .. " }"
end
return e("TextLabel", {
Text = textRepresentation,
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(1, 0, 1, 0),
})
end
-- TODO: Maybe add visualizations to other datatypes?
-- Or special text handling tostring for some?
-- Will add as needed, let's see what cases arise.
return e("TextLabel", {
Text = string.gsub(tostring(props.value), "%s", " "),
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(1, 0, 1, 0),
})
end)
end
return DisplayValue

View File

@@ -1,220 +0,0 @@
local SelectionService = game:GetService("Selection")
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local bindingUtil = require(Plugin.App.bindingUtil)
local e = Roact.createElement
local ChangeList = require(script.Parent.ChangeList)
local Tooltip = require(script.Parent.Parent.Tooltip)
local Expansion = Roact.Component:extend("Expansion")
function Expansion:render()
local props = self.props
if not props.rendered then
return nil
end
return e("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, -props.indent, 1, -30),
Position = UDim2.new(0, props.indent, 0, 30),
}, {
ChangeList = e(ChangeList, {
changes = props.changeList,
transparency = props.transparency,
showSourceDiff = props.showSourceDiff,
}),
})
end
local DomLabel = Roact.Component:extend("DomLabel")
function DomLabel:init()
local initHeight = self.props.elementHeight:getValue()
self.expanded = initHeight > 30
self.motor = Flipper.SingleMotor.new(initHeight)
self.binding = bindingUtil.fromMotor(self.motor)
self:setState({
renderExpansion = self.expanded,
})
self.motor:onStep(function(value)
local renderExpansion = value > 30
self.props.setElementHeight(value)
if self.props.updateEvent then
self.props.updateEvent:Fire()
end
self:setState(function(state)
if state.renderExpansion == renderExpansion then
return nil
end
return {
renderExpansion = renderExpansion,
}
end)
end)
end
function DomLabel:didUpdate(prevProps)
if
prevProps.instance ~= self.props.instance
or prevProps.patchType ~= self.props.patchType
or prevProps.name ~= self.props.name
or prevProps.changeList ~= self.props.changeList
then
-- Close the expansion when the domlabel is changed to a different thing
self.expanded = false
self.motor:setGoal(Flipper.Spring.new(30, {
frequency = 5,
dampingRatio = 1,
}))
end
end
function DomLabel:render()
local props = self.props
return Theme.with(function(theme)
local iconProps = StudioService:GetClassIcon(props.className)
local indent = (props.depth or 0) * 20 + 25
-- Line guides help indent depth remain readable
local lineGuides = {}
for i = 1, props.depth or 0 do
table.insert(
lineGuides,
e("Frame", {
Name = "Line_" .. i,
Size = UDim2.new(0, 2, 1, 2),
Position = UDim2.new(0, (20 * i) + 15, 0, -1),
BorderSizePixel = 0,
BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
})
)
end
return e("Frame", {
Name = "Change",
ClipsDescendants = true,
BackgroundColor3 = if props.patchType then theme.Diff[props.patchType] else nil,
BorderSizePixel = 0,
BackgroundTransparency = props.patchType and props.transparency or 1,
Size = self.binding:map(function(expand)
return UDim2.new(1, 0, 0, expand)
end),
}, {
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 10),
PaddingRight = UDim.new(0, 10),
}),
Button = e("TextButton", {
BackgroundTransparency = 1,
Text = "",
Size = UDim2.new(1, 0, 1, 0),
[Roact.Event.Activated] = function(_rbx: Instance, _input: InputObject, clickCount: number)
if clickCount == 1 then
-- Double click opens the instance in explorer
self.lastDoubleClickTime = os.clock()
if props.instance then
SelectionService:Set({ props.instance })
end
elseif clickCount == 0 then
-- Single click expands the changes
task.wait(0.25)
if os.clock() - (self.lastDoubleClickTime or 0) <= 0.25 then
-- This is a double click, so don't expand
return
end
if props.changeList then
self.expanded = not self.expanded
local goalHeight = 30
+ (if self.expanded then math.clamp(#props.changeList * 30, 30, 30 * 6) else 0)
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
frequency = 5,
dampingRatio = 1,
}))
end
end
end,
}, {
StateTip = if (props.instance or props.changeList)
then e(Tooltip.Trigger, {
text = (if props.changeList
then "Click to " .. (if self.expanded then "hide" else "view") .. " changes"
else "") .. (if props.instance
then (if props.changeList then " & d" else "D") .. "ouble click to open in Explorer"
else ""),
})
else nil,
}),
Expansion = if props.changeList
then e(Expansion, {
rendered = self.state.renderExpansion,
indent = indent,
transparency = props.transparency,
changeList = props.changeList,
showSourceDiff = props.showSourceDiff,
})
else nil,
DiffIcon = if props.patchType
then e("ImageLabel", {
Image = Assets.Images.Diff[props.patchType],
ImageColor3 = theme.AddressEntry.PlaceholderColor,
ImageTransparency = props.transparency,
BackgroundTransparency = 1,
Size = UDim2.new(0, 20, 0, 20),
Position = UDim2.new(0, 0, 0, 15),
AnchorPoint = Vector2.new(0, 0.5),
})
else nil,
ClassIcon = e("ImageLabel", {
Image = iconProps.Image,
ImageTransparency = props.transparency,
ImageRectOffset = iconProps.ImageRectOffset,
ImageRectSize = iconProps.ImageRectSize,
BackgroundTransparency = 1,
Size = UDim2.new(0, 20, 0, 20),
Position = UDim2.new(0, indent, 0, 15),
AnchorPoint = Vector2.new(0, 0.5),
}),
InstanceName = e("TextLabel", {
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,
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
TextColor3 = if props.isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(1, -indent - 50, 0, 30),
Position = UDim2.new(0, indent + 30, 0, 0),
}),
LineGuides = e("Folder", nil, lineGuides),
})
end)
end
return DomLabel

View File

@@ -1,123 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local PatchTree = require(Plugin.PatchTree)
local PatchSet = require(Plugin.PatchSet)
local Theme = require(Plugin.App.Theme)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
local e = Roact.createElement
local DomLabel = require(script.DomLabel)
local PatchVisualizer = Roact.Component:extend("PatchVisualizer")
function PatchVisualizer:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
self.updateEvent = Instance.new("BindableEvent")
end
function PatchVisualizer:willUnmount()
self.updateEvent:Destroy()
end
function PatchVisualizer:shouldUpdate(nextProps)
if self.props.patchTree ~= nextProps.patchTree then
return true
end
local currentPatch, nextPatch = self.props.patch, nextProps.patch
if currentPatch ~= nil or nextPatch ~= nil then
return not PatchSet.isEqual(currentPatch, nextPatch)
end
return false
end
function PatchVisualizer:render()
local patchTree = self.props.patchTree
if patchTree == nil and self.props.patch ~= nil then
patchTree = PatchTree.build(
self.props.patch,
self.props.instanceMap,
self.props.changeListHeaders or { "Property", "Current", "Incoming" }
)
if self.props.unappliedPatch then
patchTree =
PatchTree.updateMetadata(patchTree, self.props.patch, self.props.instanceMap, self.props.unappliedPatch)
end
end
-- Recusively draw tree
local scrollElements, elementHeights = {}, {}
if patchTree then
local function drawNode(node, depth)
local elementHeight, setElementHeight = Roact.createBinding(30)
table.insert(elementHeights, elementHeight)
table.insert(
scrollElements,
e(DomLabel, {
updateEvent = self.updateEvent,
elementHeight = elementHeight,
setElementHeight = setElementHeight,
patchType = node.patchType,
className = node.className,
isWarning = node.isWarning,
instance = node.instance,
name = node.name,
hint = node.hint,
changeList = node.changeList,
depth = depth,
transparency = self.props.transparency,
showSourceDiff = self.props.showSourceDiff,
})
)
end
patchTree:forEach(function(node, depth)
drawNode(node, depth)
end)
end
return Theme.with(function(theme)
return e(BorderedContainer, {
transparency = self.props.transparency,
size = self.props.size,
position = self.props.position,
layoutOrder = self.props.layoutOrder,
}, {
CleanMerge = e("TextLabel", {
Visible = #scrollElements == 0,
Text = "No changes to sync, project is up to date.",
Font = Enum.Font.GothamMedium,
TextSize = 15,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextWrapped = true,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}),
VirtualScroller = e(VirtualScroller, {
size = UDim2.new(1, 0, 1, 0),
transparency = self.props.transparency,
count = #scrollElements,
updateEvent = self.updateEvent.Event,
render = function(i)
return scrollElements[i]
end,
getHeightBinding = function(i)
return elementHeights[i]
end,
}),
})
end)
end
return PatchVisualizer

View File

@@ -1,8 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
@@ -23,28 +22,21 @@ local function ScrollingFrame(props)
BottomImage = Assets.Images.ScrollBar.Bottom, BottomImage = Assets.Images.ScrollBar.Bottom,
ElasticBehavior = Enum.ElasticBehavior.Always, ElasticBehavior = Enum.ElasticBehavior.Always,
ScrollingDirection = props.scrollingDirection or Enum.ScrollingDirection.Y, ScrollingDirection = Enum.ScrollingDirection.Y,
Size = props.size, Size = props.size,
Position = props.position, Position = props.position,
AnchorPoint = props.anchorPoint, AnchorPoint = props.anchorPoint,
CanvasSize = props.contentSize:map(function(value) CanvasSize = props.contentSize:map(function(value)
return UDim2.new( return UDim2.new(0, 0, 0, value.Y)
0,
if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y)
then value.X
else 0,
0,
value.Y
)
end), end),
BorderSizePixel = 0, BorderSizePixel = 0,
BackgroundTransparency = 1, BackgroundTransparency = 1,
[Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize], [Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize]
}, props[Roact.Children]) }, props[Roact.Children])
end) end)
end end
return ScrollingFrame return ScrollingFrame

View File

@@ -1,7 +1,6 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local e = Roact.createElement local e = Roact.createElement
@@ -27,4 +26,4 @@ local function SlicedImage(props)
}, props[Roact.Children]) }, props[Roact.Children])
end end
return SlicedImage return SlicedImage

View File

@@ -2,9 +2,8 @@ local RunService = game:GetService("RunService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -64,4 +63,4 @@ function Spinner:willUnmount()
self.stepper:Disconnect() self.stepper:Disconnect()
end end
return Spinner return Spinner

View File

@@ -1,441 +0,0 @@
--[[
Based on DiffMatchPatch by Neil Fraser.
https://github.com/google/diff-match-patch
]]
export type DiffAction = number
export type Diff = { actionType: DiffAction, value: string }
export type Diffs = { Diff }
local StringDiff = {
ActionTypes = table.freeze({
Equal = 0,
Delete = 1,
Insert = 2,
}),
}
function StringDiff.findDiffs(text1: string, text2: string): Diffs
-- Validate inputs
if type(text1) ~= "string" or type(text2) ~= "string" then
error(
string.format(
"Invalid inputs to StringDiff.findDiffs, expected strings and got (%s, %s)",
type(text1),
type(text2)
),
2
)
end
-- Shortcut if the texts are identical
if text1 == text2 then
return { { actionType = StringDiff.ActionTypes.Equal, value = text1 } }
end
-- Trim off any shared prefix and suffix
-- These are easy to detect and can be dealt with quickly without needing a complex diff
-- and later we simply add them as Equal to the start and end of the diff
local sharedPrefix, sharedSuffix
local prefixLength = StringDiff._sharedPrefix(text1, text2)
if prefixLength > 0 then
-- Store the prefix
sharedPrefix = string.sub(text1, 1, prefixLength)
-- Now trim it off
text1 = string.sub(text1, prefixLength + 1)
text2 = string.sub(text2, prefixLength + 1)
end
local suffixLength = StringDiff._sharedSuffix(text1, text2)
if suffixLength > 0 then
-- Store the suffix
sharedSuffix = string.sub(text1, -suffixLength)
-- Now trim it off
text1 = string.sub(text1, 1, -suffixLength - 1)
text2 = string.sub(text2, 1, -suffixLength - 1)
end
-- Compute the diff on the middle block where the changes lie
local diffs = StringDiff._computeDiff(text1, text2)
-- Restore the prefix and suffix
if sharedPrefix then
table.insert(diffs, 1, { actionType = StringDiff.ActionTypes.Equal, value = sharedPrefix })
end
if sharedSuffix then
table.insert(diffs, { actionType = StringDiff.ActionTypes.Equal, value = sharedSuffix })
end
-- Cleanup the diff
diffs = StringDiff._reorderAndMerge(diffs)
return diffs
end
function StringDiff._sharedPrefix(text1: string, text2: string): number
-- Uses a binary search to find the largest common prefix between the two strings
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
-- Shortcut common cases
if (#text1 == 0) or (#text2 == 0) or (string.byte(text1, 1) ~= string.byte(text2, 1)) then
return 0
end
local pointerMin = 1
local pointerMax = math.min(#text1, #text2)
local pointerMid = pointerMax
local pointerStart = 1
while pointerMin < pointerMid do
if string.sub(text1, pointerStart, pointerMid) == string.sub(text2, pointerStart, pointerMid) then
pointerMin = pointerMid
pointerStart = pointerMin
else
pointerMax = pointerMid
end
pointerMid = math.floor(pointerMin + (pointerMax - pointerMin) / 2)
end
return pointerMid
end
function StringDiff._sharedSuffix(text1: string, text2: string): number
-- Uses a binary search to find the largest common suffix between the two strings
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
-- Shortcut common cases
if (#text1 == 0) or (#text2 == 0) or (string.byte(text1, -1) ~= string.byte(text2, -1)) then
return 0
end
local pointerMin = 1
local pointerMax = math.min(#text1, #text2)
local pointerMid = pointerMax
local pointerEnd = 1
while pointerMin < pointerMid do
if string.sub(text1, -pointerMid, -pointerEnd) == string.sub(text2, -pointerMid, -pointerEnd) then
pointerMin = pointerMid
pointerEnd = pointerMin
else
pointerMax = pointerMid
end
pointerMid = math.floor(pointerMin + (pointerMax - pointerMin) / 2)
end
return pointerMid
end
function StringDiff._computeDiff(text1: string, text2: string): Diffs
-- Assumes that the prefix and suffix have already been trimmed off
-- and shortcut returns have been made so these texts must be different
local text1Length, text2Length = #text1, #text2
if text1Length == 0 then
-- It's simply inserting all of text2 into text1
return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } }
end
if text2Length == 0 then
-- It's simply deleting all of text1
return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } }
end
local longText = if text1Length > text2Length then text1 else text2
local shortText = if text1Length > text2Length then text2 else text1
local shortTextLength = #shortText
-- Shortcut if the shorter string exists entirely inside the longer one
local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true)
if indexOf ~= nil then
local diffs = {
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) },
{ actionType = StringDiff.ActionTypes.Equal, value = shortText },
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) },
}
-- Swap insertions for deletions if diff is reversed
if text1Length > text2Length then
diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete
end
return diffs
end
if shortTextLength == 1 then
-- Single character string
-- After the previous shortcut, the character can't be an equality
return {
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
}
end
return StringDiff._bisect(text1, text2)
end
function StringDiff._bisect(text1: string, text2: string): Diffs
-- Find the 'middle snake' of a diff, split the problem in two
-- and return the recursively constructed diff
-- See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations
-- Cache the text lengths to prevent multiple calls
local text1Length = #text1
local text2Length = #text2
local _sub, _element
local maxD = math.ceil((text1Length + text2Length) / 2)
local vOffset = maxD
local vLength = 2 * maxD
local v1 = table.create(vLength)
local v2 = table.create(vLength)
-- Setting all elements to -1 is faster in Lua than mixing integers and nil
for x = 0, vLength - 1 do
v1[x] = -1
v2[x] = -1
end
v1[vOffset + 1] = 0
v2[vOffset + 1] = 0
local delta = text1Length - text2Length
-- If the total number of characters is odd, then
-- the front path will collide with the reverse path
local front = (delta % 2 ~= 0)
-- Offsets for start and end of k loop
-- Prevents mapping of space beyond the grid
local k1Start = 0
local k1End = 0
local k2Start = 0
local k2End = 0
for d = 0, maxD - 1 do
-- Walk the front path one step
for k1 = -d + k1Start, d - k1End, 2 do
local k1_offset = vOffset + k1
local x1
if (k1 == -d) or ((k1 ~= d) and (v1[k1_offset - 1] < v1[k1_offset + 1])) then
x1 = v1[k1_offset + 1]
else
x1 = v1[k1_offset - 1] + 1
end
local y1 = x1 - k1
while
(x1 <= text1Length)
and (y1 <= text2Length)
and (string.sub(text1, x1, x1) == string.sub(text2, y1, y1))
do
x1 = x1 + 1
y1 = y1 + 1
end
v1[k1_offset] = x1
if x1 > text1Length + 1 then
-- Ran off the right of the graph
k1End = k1End + 2
elseif y1 > text2Length + 1 then
-- Ran off the bottom of the graph
k1Start = k1Start + 2
elseif front then
local k2_offset = vOffset + delta - k1
if k2_offset >= 0 and k2_offset < vLength and v2[k2_offset] ~= -1 then
-- Mirror x2 onto top-left coordinate system
local x2 = text1Length - v2[k2_offset] + 1
if x1 > x2 then
-- Overlap detected
return StringDiff._bisectSplit(text1, text2, x1, y1)
end
end
end
end
-- Walk the reverse path one step
for k2 = -d + k2Start, d - k2End, 2 do
local k2_offset = vOffset + k2
local x2
if (k2 == -d) or ((k2 ~= d) and (v2[k2_offset - 1] < v2[k2_offset + 1])) then
x2 = v2[k2_offset + 1]
else
x2 = v2[k2_offset - 1] + 1
end
local y2 = x2 - k2
while
(x2 <= text1Length)
and (y2 <= text2Length)
and (string.sub(text1, -x2, -x2) == string.sub(text2, -y2, -y2))
do
x2 = x2 + 1
y2 = y2 + 1
end
v2[k2_offset] = x2
if x2 > text1Length + 1 then
-- Ran off the left of the graph
k2End = k2End + 2
elseif y2 > text2Length + 1 then
-- Ran off the top of the graph
k2Start = k2Start + 2
elseif not front then
local k1_offset = vOffset + delta - k2
if k1_offset >= 0 and k1_offset < vLength and v1[k1_offset] ~= -1 then
local x1 = v1[k1_offset]
local y1 = vOffset + x1 - k1_offset
-- Mirror x2 onto top-left coordinate system
x2 = text1Length - x2 + 1
if x1 > x2 then
-- Overlap detected
return StringDiff._bisectSplit(text1, text2, x1, y1)
end
end
end
end
end
-- Number of diffs equals number of characters, no commonality at all
return {
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
}
end
function StringDiff._bisectSplit(text1: string, text2: string, x: number, y: number): Diffs
-- Given the location of the 'middle snake',
-- split the diff in two parts and recurse
local text1a = string.sub(text1, 1, x - 1)
local text2a = string.sub(text2, 1, y - 1)
local text1b = string.sub(text1, x)
local text2b = string.sub(text2, y)
-- Compute both diffs serially
local diffs = StringDiff.findDiffs(text1a, text2a)
local diffsB = StringDiff.findDiffs(text1b, text2b)
-- Merge diffs
table.move(diffsB, 1, #diffsB, #diffs + 1, diffs)
return diffs
end
function StringDiff._reorderAndMerge(diffs: Diffs): Diffs
-- Reorder and merge like edit sections and merge equalities
-- Any edit section can move as long as it doesn't cross an equality
-- Add a dummy entry at the end
table.insert(diffs, { actionType = StringDiff.ActionTypes.Equal, value = "" })
local pointer = 1
local countDelete, countInsert = 0, 0
local textDelete, textInsert = "", ""
local commonLength
while diffs[pointer] do
local actionType = diffs[pointer].actionType
if actionType == StringDiff.ActionTypes.Insert then
countInsert = countInsert + 1
textInsert = textInsert .. diffs[pointer].value
pointer = pointer + 1
elseif actionType == StringDiff.ActionTypes.Delete then
countDelete = countDelete + 1
textDelete = textDelete .. diffs[pointer].value
pointer = pointer + 1
elseif actionType == StringDiff.ActionTypes.Equal then
-- Upon reaching an equality, check for prior redundancies
if countDelete + countInsert > 1 then
if (countDelete > 0) and (countInsert > 0) then
-- Factor out any common prefixies
commonLength = StringDiff._sharedPrefix(textInsert, textDelete)
if commonLength > 0 then
local back_pointer = pointer - countDelete - countInsert
if
(back_pointer > 1) and (diffs[back_pointer - 1].actionType == StringDiff.ActionTypes.Equal)
then
diffs[back_pointer - 1].value = diffs[back_pointer - 1].value
.. string.sub(textInsert, 1, commonLength)
else
table.insert(diffs, 1, {
actionType = StringDiff.ActionTypes.Equal,
value = string.sub(textInsert, 1, commonLength),
})
pointer = pointer + 1
end
textInsert = string.sub(textInsert, commonLength + 1)
textDelete = string.sub(textDelete, commonLength + 1)
end
-- Factor out any common suffixies
commonLength = StringDiff._sharedSuffix(textInsert, textDelete)
if commonLength ~= 0 then
diffs[pointer].value = string.sub(textInsert, -commonLength) .. diffs[pointer].value
textInsert = string.sub(textInsert, 1, -commonLength - 1)
textDelete = string.sub(textDelete, 1, -commonLength - 1)
end
end
-- Delete the offending records and add the merged ones
pointer = pointer - countDelete - countInsert
for _ = 1, countDelete + countInsert do
table.remove(diffs, pointer)
end
if #textDelete > 0 then
table.insert(diffs, pointer, { actionType = StringDiff.ActionTypes.Delete, value = textDelete })
pointer = pointer + 1
end
if #textInsert > 0 then
table.insert(diffs, pointer, { actionType = StringDiff.ActionTypes.Insert, value = textInsert })
pointer = pointer + 1
end
pointer = pointer + 1
elseif (pointer > 1) and (diffs[pointer - 1].actionType == StringDiff.ActionTypes.Equal) then
-- Merge this equality with the previous one
diffs[pointer - 1].value = diffs[pointer - 1].value .. diffs[pointer].value
table.remove(diffs, pointer)
else
pointer = pointer + 1
end
countInsert, countDelete = 0, 0
textDelete, textInsert = "", ""
end
end
if diffs[#diffs].value == "" then
-- Remove the dummy entry at the end
diffs[#diffs] = nil
end
-- Second pass: look for single edits surrounded on both sides by equalities
-- which can be shifted sideways to eliminate an equality
-- e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
local changes = false
pointer = 2
-- Intentionally ignore the first and last element (don't need checking)
while pointer < #diffs do
local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1]
if
(prevDiff.actionType == StringDiff.ActionTypes.Equal)
and (nextDiff.actionType == StringDiff.ActionTypes.Equal)
then
-- This is a single edit surrounded by equalities
local currentDiff = diffs[pointer]
local currentText = currentDiff.value
local prevText = prevDiff.value
local nextText = nextDiff.value
if #prevText == 0 then
table.remove(diffs, pointer - 1)
changes = true
elseif string.sub(currentText, -#prevText) == prevText then
-- Shift the edit over the previous equality
currentDiff.value = prevText .. string.sub(currentText, 1, -#prevText - 1)
nextDiff.value = prevText .. nextDiff.value
table.remove(diffs, pointer - 1)
changes = true
elseif string.sub(currentText, 1, #nextText) == nextText then
-- Shift the edit over the next equality
prevDiff.value = prevText .. nextText
currentDiff.value = string.sub(currentText, #nextText + 1) .. nextText
table.remove(diffs, pointer + 1)
changes = true
end
end
pointer = pointer + 1
end
-- If shifts were made, the diffs need reordering and another shift sweep
if changes then
return StringDiff._reorderAndMerge(diffs)
end
return diffs
end
return StringDiff

View File

@@ -1,202 +0,0 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Log = require(Packages.Log)
local Highlighter = require(Packages.Highlighter)
local StringDiff = require(script:FindFirstChild("StringDiff"))
local Theme = require(Plugin.App.Theme)
local CodeLabel = require(Plugin.App.Components.CodeLabel)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local e = Roact.createElement
local StringDiffVisualizer = Roact.Component:extend("StringDiffVisualizer")
function StringDiffVisualizer:init()
self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0))
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
-- Ensure that the script background is up to date with the current theme
self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function()
task.defer(function()
-- Defer to allow Highlighter to process the theme change first
self:updateScriptBackground()
end)
end)
self:calculateContentSize()
self:updateScriptBackground()
self:setState({
add = {},
remove = {},
})
end
function StringDiffVisualizer:willUnmount()
self.themeChangedConnection:Disconnect()
end
function StringDiffVisualizer:updateScriptBackground()
local backgroundColor = Highlighter.getTokenColor("background")
if backgroundColor ~= self.scriptBackground:getValue() then
self.setScriptBackground(backgroundColor)
end
end
function StringDiffVisualizer:didUpdate(previousProps)
if previousProps.oldText ~= self.props.oldText or previousProps.newText ~= self.props.newText then
self:calculateContentSize()
local add, remove = self:calculateDiffLines()
self:setState({
add = add,
remove = remove,
})
end
end
function StringDiffVisualizer:calculateContentSize()
local oldText, newText = self.props.oldText, self.props.newText
local oldTextBounds = TextService:GetTextSize(oldText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
local newTextBounds = TextService:GetTextSize(newText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
self.setContentSize(
Vector2.new(math.max(oldTextBounds.X, newTextBounds.X), math.max(oldTextBounds.Y, newTextBounds.Y))
)
end
function StringDiffVisualizer:calculateDiffLines()
local oldText, newText = self.props.oldText, self.props.newText
-- Diff the two texts
local startClock = os.clock()
local diffs = StringDiff.findDiffs(oldText, newText)
local stopClock = os.clock()
Log.trace(
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
#oldText,
#newText,
math.round((stopClock - startClock) * 1000 * 1000),
#diffs
)
-- Determine which lines to highlight
local add, remove = {}, {}
local oldLineNum, newLineNum = 1, 1
for _, diff in diffs do
local actionType, text = diff.actionType, diff.value
local lines = select(2, string.gsub(text, "\n", "\n"))
if actionType == StringDiff.ActionTypes.Equal then
oldLineNum += lines
newLineNum += lines
elseif actionType == StringDiff.ActionTypes.Insert then
if lines > 0 then
local textLines = string.split(text, "\n")
for i, textLine in textLines do
if string.match(textLine, "%S") then
add[newLineNum + i - 1] = true
end
end
else
if string.match(text, "%S") then
add[newLineNum] = true
end
end
newLineNum += lines
elseif actionType == StringDiff.ActionTypes.Delete then
if lines > 0 then
local textLines = string.split(text, "\n")
for i, textLine in textLines do
if string.match(textLine, "%S") then
remove[oldLineNum + i - 1] = true
end
end
else
if string.match(text, "%S") then
remove[oldLineNum] = true
end
end
oldLineNum += lines
else
Log.warn("Unknown diff action: {} {}", actionType, text)
end
end
return add, remove
end
function StringDiffVisualizer:render()
local oldText, newText = self.props.oldText, self.props.newText
return Theme.with(function(theme)
return e(BorderedContainer, {
size = self.props.size,
position = self.props.position,
anchorPoint = self.props.anchorPoint,
transparency = self.props.transparency,
}, {
Background = e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
BorderSizePixel = 0,
BackgroundColor3 = self.scriptBackground,
ZIndex = -10,
}, {
UICorner = e("UICorner", {
CornerRadius = UDim.new(0, 5),
}),
}),
Separator = e("Frame", {
Size = UDim2.new(0, 2, 1, 0),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(0.5, 0),
BorderSizePixel = 0,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
BackgroundTransparency = 0.5,
}),
Old = e(ScrollingFrame, {
position = UDim2.new(0, 2, 0, 2),
size = UDim2.new(0.5, -7, 1, -4),
scrollingDirection = Enum.ScrollingDirection.XY,
transparency = self.props.transparency,
contentSize = self.contentSize,
}, {
Source = e(CodeLabel, {
size = UDim2.new(1, 0, 1, 0),
position = UDim2.new(0, 0, 0, 0),
text = oldText,
lineBackground = theme.Diff.Remove,
markedLines = self.state.remove,
}),
}),
New = e(ScrollingFrame, {
position = UDim2.new(0.5, 5, 0, 2),
size = UDim2.new(0.5, -7, 1, -4),
scrollingDirection = Enum.ScrollingDirection.XY,
transparency = self.props.transparency,
contentSize = self.contentSize,
}, {
Source = e(CodeLabel, {
size = UDim2.new(1, 0, 1, 0),
position = UDim2.new(0, 0, 0, 0),
text = newText,
lineBackground = theme.Diff.Add,
markedLines = self.state.add,
}),
}),
})
end)
end
return StringDiffVisualizer

View File

@@ -1,8 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)
@@ -10,15 +9,11 @@ local StudioPluginContext = require(script.Parent.StudioPluginContext)
local e = Roact.createElement local e = Roact.createElement
local StudioPluginAction = Roact.Component:extend("StudioPluginAction") local StudioPluginAction = Roact.Component:extend("StudioPluginAction")
function StudioPluginAction:init() function StudioPluginAction:init()
self.pluginAction = self.props.plugin:CreatePluginAction( self.pluginAction = self.props.plugin:CreatePluginAction(
self.props.name, self.props.name, self.props.title, self.props.description, self.props.icon, self.props.bindable
self.props.title,
self.props.description,
self.props.icon,
self.props.bindable
) )
self.pluginAction.Triggered:Connect(self.props.onTriggered) self.pluginAction.Triggered:Connect(self.props.onTriggered)
@@ -35,12 +30,9 @@ end
local function StudioPluginActionWrapper(props) local function StudioPluginActionWrapper(props)
return e(StudioPluginContext.Consumer, { return e(StudioPluginContext.Consumer, {
render = function(plugin) render = function(plugin)
return e( return e(StudioPluginAction, Dictionary.merge(props, {
StudioPluginAction, plugin = plugin,
Dictionary.merge(props, { }))
plugin = plugin,
})
)
end, end,
}) })
end end

View File

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

View File

@@ -1,13 +1,9 @@
local HttpService = game:GetService("HttpService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)
local Theme = require(Plugin.App.Theme)
local StudioPluginContext = require(script.Parent.StudioPluginContext) local StudioPluginContext = require(script.Parent.StudioPluginContext)
@@ -32,16 +28,11 @@ function StudioPluginGui:init()
self.props.initDockState, self.props.initDockState,
self.props.active, self.props.active,
self.props.overridePreviousState, self.props.overridePreviousState,
floatingSize.X, floatingSize.X, floatingSize.Y,
floatingSize.Y, minimumSize.X, minimumSize.Y
minimumSize.X,
minimumSize.Y
) )
local pluginGui = self.props.plugin:CreateDockWidgetPluginGui( local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(self.props.id, dockWidgetPluginGuiInfo)
if self.props.isEphemeral then HttpService:GenerateGUID(false) else self.props.id,
dockWidgetPluginGuiInfo
)
pluginGui.Name = self.props.id pluginGui.Name = self.props.id
pluginGui.Title = self.props.title pluginGui.Title = self.props.title
@@ -65,28 +56,13 @@ end
function StudioPluginGui:render() function StudioPluginGui:render()
return e(Roact.Portal, { return e(Roact.Portal, {
target = self.pluginGui, target = self.pluginGui,
}, { }, self.props[Roact.Children])
Background = Theme.with(function(theme)
return e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundColor3 = theme.BackgroundColor,
ZIndex = 0,
BorderSizePixel = 0,
}, self.props[Roact.Children])
end),
})
end end
function StudioPluginGui:didUpdate(lastProps) function StudioPluginGui:didUpdate(lastProps)
if self.props.active ~= lastProps.active then if self.props.active ~= lastProps.active then
-- This is intentionally in didUpdate to make sure the initial active state -- This is intentionally in didUpdate to make sure the initial active state
-- (if the PluginGui is open initially) is preserved. -- (if the PluginGui is open initially) is preserved.
-- Studio widgets are very unreliable and sometimes need to be flickered
-- in order to force them to render correctly
-- This happens within a single frame so it doesn't flicker visibly
self.pluginGui.Enabled = self.props.active
self.pluginGui.Enabled = not self.props.active
self.pluginGui.Enabled = self.props.active self.pluginGui.Enabled = self.props.active
end end
end end
@@ -98,14 +74,11 @@ end
local function StudioPluginGuiWrapper(props) local function StudioPluginGuiWrapper(props)
return e(StudioPluginContext.Consumer, { return e(StudioPluginContext.Consumer, {
render = function(plugin) render = function(plugin)
return e( return e(StudioPluginGui, Dictionary.merge(props, {
StudioPluginGui, plugin = plugin,
Dictionary.merge(props, { }))
plugin = plugin,
})
)
end, end,
}) })
end end
return StudioPluginGuiWrapper return StudioPluginGuiWrapper

View File

@@ -1,8 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)
@@ -18,8 +17,12 @@ StudioToggleButton.defaultProps = {
} }
function StudioToggleButton:init() function StudioToggleButton:init()
local button = local button = self.props.toolbar:CreateButton(
self.props.toolbar:CreateButton(self.props.name, self.props.tooltip, self.props.icon, self.props.text) self.props.name,
self.props.tooltip,
self.props.icon,
self.props.text
)
button.Click:Connect(function() button.Click:Connect(function()
if self.props.onClick then if self.props.onClick then
@@ -57,12 +60,9 @@ end
local function StudioToggleButtonWrapper(props) local function StudioToggleButtonWrapper(props)
return e(StudioToolbarContext.Consumer, { return e(StudioToolbarContext.Consumer, {
render = function(toolbar) render = function(toolbar)
return e( return e(StudioToggleButton, Dictionary.merge(props, {
StudioToggleButton, toolbar = toolbar,
Dictionary.merge(props, { }))
toolbar = toolbar,
})
)
end, end,
}) })
end end

View File

@@ -1,8 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)
@@ -36,14 +35,11 @@ end
local function StudioToolbarWrapper(props) local function StudioToolbarWrapper(props)
return e(StudioPluginContext.Consumer, { return e(StudioPluginContext.Consumer, {
render = function(plugin) render = function(plugin)
return e( return e(StudioToolbar, Dictionary.merge(props, {
StudioToolbar, plugin = plugin,
Dictionary.merge(props, { }))
plugin = plugin,
})
)
end, end,
}) })
end end
return StudioToolbarWrapper return StudioToolbarWrapper

View File

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

View File

@@ -2,10 +2,9 @@ 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 Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Flipper = require(Packages.Flipper) local Flipper = require(Rojo.Flipper)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
@@ -41,8 +40,10 @@ end
function TextButton:render() function TextButton:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local textSize = local textSize = TextService:GetTextSize(
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamSemibold, Vector2.new(math.huge, math.huge)) self.props.text, 18, Enum.Font.GothamSemibold,
Vector2.new(math.huge, math.huge)
)
local style = self.props.style local style = self.props.style
@@ -122,21 +123,15 @@ function TextButton:render()
Background = style == "Solid" and e(SlicedImage, { Background = style == "Solid" and e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = bindingUtil.mapLerp( color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BackgroundColor, theme.Disabled.BackgroundColor),
bindingEnabled,
theme.Enabled.BackgroundColor,
theme.Disabled.BackgroundColor
),
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
zIndex = -2, zIndex = -2,
}), }),
Children = Roact.createFragment(self.props[Roact.Children]),
}) })
end) end)
end end
return TextButton return TextButton

View File

@@ -1,107 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil)
local SlicedImage = require(script.Parent.SlicedImage)
local SPRING_PROPS = {
frequency = 5,
dampingRatio = 1,
}
local e = Roact.createElement
local TextInput = Roact.Component:extend("TextInput")
function TextInput:init()
self.motor = Flipper.GroupMotor.new({
hover = 0,
enabled = self.props.enabled and 1 or 0,
})
self.binding = bindingUtil.fromMotor(self.motor)
end
function TextInput:didUpdate(lastProps)
if lastProps.enabled ~= self.props.enabled then
self.motor:setGoal({
enabled = Flipper.Spring.new(self.props.enabled and 1 or 0),
})
end
end
function TextInput:render()
return Theme.with(function(theme)
theme = theme.TextInput
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
return e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
transparency = self.props.transparency,
size = self.props.size or UDim2.new(1, 0, 1, 0),
position = self.props.position,
layoutOrder = self.props.layoutOrder,
anchorPoint = self.props.anchorPoint,
}, {
HoverOverlay = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = theme.ActionFillColor,
transparency = Roact.joinBindings({
hover = bindingHover:map(function(value)
return 1 - value
end),
transparency = self.props.transparency,
}):map(function(values)
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
end),
size = UDim2.new(1, 0, 1, 0),
zIndex = -1,
}),
Input = e("TextBox", {
BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1),
Text = self.props.text,
PlaceholderText = self.props.placeholder,
Font = Enum.Font.GothamMedium,
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
PlaceholderColor3 = bindingUtil.mapLerp(
bindingEnabled,
theme.Disabled.PlaceholderColor,
theme.Enabled.PlaceholderColor
),
TextSize = 18,
TextEditable = self.props.enabled,
ClearTextOnFocus = self.props.clearTextOnFocus,
[Roact.Event.MouseEnter] = function()
self.motor:setGoal({
hover = Flipper.Spring.new(1, SPRING_PROPS),
})
end,
[Roact.Event.MouseLeave] = function()
self.motor:setGoal({
hover = Flipper.Spring.new(0, SPRING_PROPS),
})
end,
[Roact.Event.FocusLost] = function(rbx)
self.props.onEntered(rbx.Text)
end,
}),
Children = Roact.createFragment(self.props[Roact.Children]),
})
end)
end
return TextInput

View File

@@ -1,276 +0,0 @@
local TextService = game:GetService("TextService")
local HttpService = game:GetService("HttpService")
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 BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local e = Roact.createElement
local DELAY = 0.75 -- How long to hover before a popup is shown (seconds)
local TEXT_PADDING = Vector2.new(8 * 2, 6 * 2) -- Padding for the popup text containers
local TAIL_SIZE = 16 -- Size of the triangle tail piece
local X_OFFSET = 30 -- How far right (from left) the tail will be (assuming enough space)
local Y_OVERLAP = 10 -- Let the triangle tail piece overlap the target a bit to help "connect" it
local TooltipContext = Roact.createContext({})
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 e(BorderedContainer, {
position = UDim2.fromOffset(X, Y),
size = UDim2.fromOffset(textSize.X, textSize.Y),
transparency = props.transparency,
}, {
Label = e("TextLabel", {
BackgroundTransparency = 1,
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),
Text = props.Text,
TextSize = 16,
Font = Enum.Font.GothamMedium,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = theme.Button.Bordered.Enabled.TextColor,
TextTransparency = props.transparency,
}),
Tail = e("ImageLabel", {
ZIndex = 100,
Position = if displayAbove
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, textSize.X - 6), 0, -TAIL_SIZE + 1),
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
AnchorPoint = Vector2.new(0.5, 0),
Rotation = if displayAbove then 180 else 0,
BackgroundTransparency = 1,
Image = "rbxassetid://10983945016",
ImageColor3 = theme.BorderedContainer.BackgroundColor,
ImageTransparency = props.transparency,
}, {
Border = e("ImageLabel", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
Image = "rbxassetid://10983946430",
ImageColor3 = theme.BorderedContainer.BorderColor,
ImageTransparency = props.transparency,
}),
}),
})
end)
end
local Provider = Roact.Component:extend("TooltipManager")
function Provider:init()
self:setState({
tips = {},
addTip = function(id: string, data: { Text: string, Position: Vector2, Trigger: any })
self:setState(function(state)
state.tips[id] = data
return state
end)
end,
removeTip = function(id: string)
self:setState(function(state)
state.tips[id] = nil
return state
end)
end,
})
end
function Provider:render()
return Roact.createElement(TooltipContext.Provider, {
value = self.state,
}, self.props[Roact.Children])
end
local Container = Roact.Component:extend("TooltipContainer")
function Container:init()
self:setState({
size = Vector2.new(200, 100),
})
end
function Container:render()
return Roact.createElement(TooltipContext.Consumer, {
render = function(context)
local tips = context.tips
local popups = {}
for key, value in tips do
popups[key] = e(Popup, {
Text = value.Text or "",
Position = value.Position or Vector2.zero,
Trigger = value.Trigger,
parentSize = self.state.size,
})
end
return e("Frame", {
[Roact.Change.AbsoluteSize] = function(rbx)
self:setState({
size = rbx.AbsoluteSize,
})
end,
ZIndex = 100,
BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1),
}, popups)
end,
})
end
local Trigger = Roact.Component:extend("TooltipTrigger")
function Trigger:init()
self.id = HttpService:GenerateGUID(false)
self.ref = Roact.createRef()
self.mousePos = Vector2.zero
self.showingPopup = false
self.destroy = function()
self.props.context.removeTip(self.id)
self.showingPopup = false
end
end
function Trigger:willUnmount()
if self.showDelayThread then
pcall(task.cancel, self.showDelayThread)
end
if self.destroy then
self.destroy()
end
end
function Trigger:didUpdate(prevProps)
if prevProps.text ~= self.props.text then
-- Any existing popup is now invalid
self.props.context.removeTip(self.id)
self.showingPopup = false
-- Let the new text propagate
self:managePopup()
end
end
function Trigger:isHovering()
local rbx = self.ref.current
if rbx then
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
return false
end
function Trigger:managePopup()
if self:isHovering() then
if self.showingPopup or self.showDelayThread then
-- Don't duplicate popups
return
end
self.showDelayThread = task.delay(DELAY, function()
self.props.context.addTip(self.id, {
Text = self.props.text,
Position = self.mousePos,
Trigger = self.ref,
})
self.showDelayThread = nil
self.showingPopup = true
end)
else
if self.showDelayThread then
pcall(task.cancel, self.showDelayThread)
self.showDelayThread = nil
end
self.props.context.removeTip(self.id)
self.showingPopup = false
end
end
function Trigger:render()
local function recalculate(rbx)
local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui")
if not widget then
return
end
self.mousePos = widget:GetRelativeMousePosition()
self:managePopup()
end
return e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ZIndex = self.props.zIndex or 100,
[Roact.Ref] = self.ref,
[Roact.Change.AbsolutePosition] = recalculate,
[Roact.Change.AbsoluteSize] = recalculate,
[Roact.Event.MouseMoved] = recalculate,
[Roact.Event.MouseLeave] = recalculate,
[Roact.Event.MouseEnter] = recalculate,
})
end
local function TriggerConsumer(props)
return Roact.createElement(TooltipContext.Consumer, {
render = function(context)
local innerProps = table.clone(props)
innerProps.context = context
return e(Trigger, innerProps)
end,
})
end
return {
Provider = Provider,
Container = Container,
Trigger = TriggerConsumer,
}

View File

@@ -1,9 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Flipper = require(Packages.Flipper) local Flipper = require(Rojo.Flipper)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil) local bindingUtil = require(Plugin.App.bindingUtil)
@@ -24,7 +23,9 @@ function TouchRipple:init()
}) })
self.binding = bindingUtil.fromMotor(self.motor) self.binding = bindingUtil.fromMotor(self.motor)
self.position, self.setPosition = Roact.createBinding(Vector2.new(0, 0)) self.position, self.setPosition = Roact.createBinding(
Vector2.new(0, 0)
)
end end
function TouchRipple:reset() function TouchRipple:reset()
@@ -41,7 +42,10 @@ function TouchRipple:calculateRadius(position)
local container = self.ref.current local container = self.ref.current
if container then if container then
local corner = Vector2.new(math.floor((1 - position.X) + 0.5), math.floor((1 - position.Y) + 0.5)) local corner = Vector2.new(
math.floor((1 - position.X) + 0.5),
math.floor((1 - position.Y) + 0.5)
)
local size = container.AbsoluteSize local size = container.AbsoluteSize
local ratio = size / math.min(size.X, size.Y) local ratio = size / math.min(size.X, size.Y)
@@ -88,7 +92,10 @@ function TouchRipple:render()
input:GetPropertyChangedSignal("UserInputState"):Connect(function() input:GetPropertyChangedSignal("UserInputState"):Connect(function()
local userInputState = input.UserInputState local userInputState = input.UserInputState
if userInputState == Enum.UserInputState.Cancel or userInputState == Enum.UserInputState.End then if
userInputState == Enum.UserInputState.Cancel
or userInputState == Enum.UserInputState.End
then
self.motor:setGoal({ self.motor:setGoal({
opacity = Flipper.Spring.new(0, { opacity = Flipper.Spring.new(0, {
frequency = 5, frequency = 5,
@@ -119,10 +126,8 @@ function TouchRipple:render()
local containerAspect = containerSize.X / containerSize.Y local containerAspect = containerSize.X / containerSize.Y
return UDim2.new( return UDim2.new(
currentSize / math.max(containerAspect, 1), currentSize / math.max(containerAspect, 1), 0,
0, currentSize * math.min(containerAspect, 1), 0
currentSize * math.min(containerAspect, 1),
0
) )
end end
end), end),
@@ -137,4 +142,4 @@ function TouchRipple:render()
}) })
end end
return TouchRipple return TouchRipple

View File

@@ -1,168 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Assets = require(Plugin.Assets)
local Theme = require(Plugin.App.Theme)
local bindingUtil = require(Plugin.App.bindingUtil)
local e = Roact.createElement
local VirtualScroller = Roact.Component:extend("VirtualScroller")
function VirtualScroller:init()
self.scrollFrameRef = Roact.createRef()
self:setState({
WindowSize = Vector2.new(),
CanvasPosition = Vector2.new(),
})
self.totalCanvas, self.setTotalCanvas = Roact.createBinding(0)
self.padding, self.setPadding = Roact.createBinding(0)
self:refresh()
if self.props.updateEvent then
self.connection = self.props.updateEvent:Connect(function()
self:refresh()
end)
end
end
function VirtualScroller:didMount()
local rbx = self.scrollFrameRef:getValue()
local windowSizeSignal = rbx:GetPropertyChangedSignal("AbsoluteWindowSize")
self.windowSizeChanged = windowSizeSignal:Connect(function()
self:setState({ WindowSize = rbx.AbsoluteWindowSize })
self:refresh()
end)
local canvasPositionSignal = rbx:GetPropertyChangedSignal("CanvasPosition")
self.canvasPositionChanged = canvasPositionSignal:Connect(function()
if math.abs(rbx.CanvasPosition.Y - self.state.CanvasPosition.Y) > 5 then
self:setState({ CanvasPosition = rbx.CanvasPosition })
self:refresh()
end
end)
self:refresh()
end
function VirtualScroller:willUnmount()
self.windowSizeChanged:Disconnect()
self.canvasPositionChanged:Disconnect()
if self.connection then
self.connection:Disconnect()
self.connection = nil
end
end
function VirtualScroller:refresh()
local props = self.props
local state = self.state
local count = props.count
local windowSize, canvasPosition = state.WindowSize.Y, state.CanvasPosition.Y
local bottom = canvasPosition + windowSize
local minIndex, maxIndex = 1, count
local padding, canvasSize = 0, 0
local pos = 0
for i = 1, count do
local height = props.getHeightBinding(i):getValue()
canvasSize += height
if pos > bottom then
-- Below window
if maxIndex > i then
maxIndex = i
end
end
pos += height
if pos < canvasPosition then
-- Above window
minIndex = i
padding = pos - height
end
end
self.setPadding(padding)
self.setTotalCanvas(canvasSize)
self:setState({
Start = minIndex,
End = maxIndex,
})
end
function VirtualScroller:didUpdate(previousProps)
if self.props.count ~= previousProps.count then
-- Items have changed, so we need to refresh
self:refresh()
end
end
function VirtualScroller:render()
local props, state = self.props, self.state
local items = {}
for i = state.Start, state.End do
local content = props.render(i)
if content == nil then
continue
end
items["Item" .. i] = e("Frame", {
LayoutOrder = i,
Size = props.getHeightBinding(i):map(function(height)
return UDim2.new(1, 0, 0, height)
end),
BackgroundTransparency = 1,
}, content)
end
return Theme.with(function(theme)
return e("ScrollingFrame", {
Size = props.size,
Position = props.position,
AnchorPoint = props.anchorPoint,
BackgroundTransparency = props.backgroundTransparency or 1,
BackgroundColor3 = props.backgroundColor3,
BorderColor3 = props.borderColor3,
CanvasSize = self.totalCanvas:map(function(s)
return UDim2.fromOffset(0, s)
end),
ScrollBarThickness = 9,
ScrollBarImageColor3 = theme.ScrollBarColor,
ScrollBarImageTransparency = props.transparency:map(function(value)
return bindingUtil.blendAlpha({ 0.65, value })
end),
TopImage = Assets.Images.ScrollBar.Top,
MidImage = Assets.Images.ScrollBar.Middle,
BottomImage = Assets.Images.ScrollBar.Bottom,
ElasticBehavior = Enum.ElasticBehavior.Always,
ScrollingDirection = Enum.ScrollingDirection.Y,
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
[Roact.Ref] = self.scrollFrameRef,
}, {
Layout = e("UIListLayout", {
Padding = UDim.new(0, 0),
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = Enum.FillDirection.Vertical,
}),
Padding = e("UIPadding", {
PaddingTop = self.padding:map(function(p)
return UDim.new(0, p)
end),
}),
Content = Roact.createFragment(items),
})
end)
end
return VirtualScroller

View File

@@ -1,227 +0,0 @@
local TextService = game:GetService("TextService")
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)
local Log = require(Packages.Log)
local bindingUtil = require(script.Parent.bindingUtil)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local TextButton = require(Plugin.App.Components.TextButton)
local baseClock = DateTime.now().UnixTimestampMillis
local e = Roact.createElement
local Notification = Roact.Component:extend("Notification")
function Notification:init()
self.motor = Flipper.SingleMotor.new(0)
self.binding = bindingUtil.fromMotor(self.motor)
self.lifetime = self.props.timeout
self.motor:onStep(function(value)
if value <= 0 and self.props.onClose then
self.props.onClose()
end
end)
end
function Notification:dismiss()
self.motor:setGoal(Flipper.Spring.new(0, {
frequency = 5,
dampingRatio = 1,
}))
end
function Notification:didMount()
self.motor:setGoal(Flipper.Spring.new(1, {
frequency = 3,
dampingRatio = 1,
}))
self.props.soundPlayer:play(Assets.Sounds.Notification)
self.timeout = task.spawn(function()
local clock = os.clock()
local seen = false
while task.wait(1 / 10) do
local now = os.clock()
local dt = now - clock
clock = now
if not seen then
seen = StudioService.ActiveScript == nil
end
if not seen then
-- Don't run down timer before being viewed
continue
end
self.lifetime -= dt
if self.lifetime <= 0 then
self:dismiss()
break
end
end
end)
end
function Notification:willUnmount()
task.cancel(self.timeout)
end
function Notification:render()
local transparency = self.binding:map(function(value)
return 1 - value
end)
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 += TextService:GetTextSize(
action.text,
18,
Enum.Font.GothamMedium,
Vector2.new(math.huge, math.huge)
).X + 30
count += 1
end
buttonsX += (count - 1) * 5
end
local paddingY, logoSize = 20, 32
local actionsY = if self.props.actions then 35 else 0
local contentX = math.max(textBounds.X, buttonsX)
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", {
BackgroundTransparency = 1,
Size = size,
LayoutOrder = self.props.layoutOrder,
Text = "",
ClipsDescendants = true,
[Roact.Event.Activated] = function()
self:dismiss()
end,
}, {
e(BorderedContainer, {
transparency = transparency,
size = UDim2.new(1, 0, 1, 0),
}, {
Contents = e("Frame", {
Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
Position = UDim2.new(0, 0, 0, paddingY / 2),
BackgroundTransparency = 1,
}, {
Logo = e("ImageLabel", {
ImageTransparency = transparency,
Image = Assets.Images.PluginButton,
BackgroundTransparency = 1,
Size = UDim2.new(0, logoSize, 0, logoSize),
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
Info = e("TextLabel", {
Text = self.props.text,
Font = Enum.Font.GothamMedium,
TextSize = 15,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
Position = UDim2.fromOffset(35, 0),
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Actions = if self.props.actions
then e("Frame", {
Size = UDim2.new(1, -40, 0, 35),
Position = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(1, 1),
BackgroundTransparency = 1,
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5),
}),
Buttons = Roact.createFragment(actionButtons),
})
else nil,
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15),
}),
}),
})
end)
end
local Notifications = Roact.Component:extend("Notifications")
function Notifications:render()
local notifs = {}
for id, notif in self.props.notifications do
notifs["NotifID_" .. id] = e(Notification, {
soundPlayer = self.props.soundPlayer,
text = notif.text,
timestamp = notif.timestamp,
timeout = notif.timeout,
actions = notif.actions,
layoutOrder = (notif.timestamp - baseClock),
onClose = function()
self.props.onClose(id)
end,
})
end
return Roact.createFragment(notifs)
end
return Notifications

View File

@@ -1,9 +1,8 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Flipper = require(Packages.Flipper) local Flipper = require(Rojo.Flipper)
local Dictionary = require(Plugin.Dictionary) local Dictionary = require(Plugin.Dictionary)
@@ -15,7 +14,7 @@ local Page = Roact.Component:extend("Page")
function Page:init() function Page:init()
self:setState({ self:setState({
rendered = self.props.active, rendered = self.props.active
}) })
self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0) self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0)
@@ -51,22 +50,21 @@ function Page:render()
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, { }, {
Component = e( Component = e(self.props.component, Dictionary.merge(self.props, {
self.props.component, transparency = transparency,
Dictionary.merge(self.props, { }))
transparency = transparency,
})
),
}) })
end end
function Page:didUpdate(lastProps) function Page:didUpdate(lastProps)
if self.props.active ~= lastProps.active then if self.props.active ~= lastProps.active then
self.motor:setGoal(Flipper.Spring.new(self.props.active and 1 or 0, { self.motor:setGoal(
frequency = 6, Flipper.Spring.new(self.props.active and 1 or 0, {
dampingRatio = 1, frequency = 6,
})) dampingRatio = 1,
})
)
end end
end end
return Page return Page

View File

@@ -0,0 +1,121 @@
--[[
Persistent plugin settings that can be accessed via Roact context.
]]
local Rojo = script:FindFirstAncestor("Rojo")
local Roact = require(Rojo.Roact)
local defaultSettings = {
openScriptsExternally = false,
twoWaySync = false,
}
local Settings = {}
Settings.__index = Settings
function Settings.fromPlugin(plugin)
local values = {}
for name, defaultValue in pairs(defaultSettings) do
local savedValue = plugin:GetSetting("Rojo_" .. name)
if savedValue == nil then
plugin:SetSetting("Rojo_" .. name, defaultValue)
values[name] = defaultValue
else
values[name] = savedValue
end
end
return setmetatable({
__values = values,
__plugin = plugin,
__updateListeners = {},
}, Settings)
end
function Settings:get(name)
if defaultSettings[name] == nil then
error("Invalid setings name " .. tostring(name), 2)
end
return self.__values[name]
end
function Settings:set(name, value)
self.__plugin:SetSetting("Rojo_" .. name, value)
self.__values[name] = value
for callback in pairs(self.__updateListeners) do
callback(name, value)
end
end
function Settings:onUpdate(newCallback)
local newListeners = {}
for callback in pairs(self.__updateListeners) do
newListeners[callback] = true
end
newListeners[newCallback] = true
self.__updateListeners = newListeners
return function()
local newListeners = {}
for callback in pairs(self.__updateListeners) do
if callback ~= newCallback then
newListeners[callback] = true
end
end
self.__updateListeners = newListeners
end
end
local Context = Roact.createContext(nil)
local StudioProvider = Roact.Component:extend("StudioProvider")
function StudioProvider:init()
self.settings = Settings.fromPlugin(self.props.plugin)
end
function StudioProvider:render()
return Roact.createElement(Context.Provider, {
value = self.settings,
}, self.props[Roact.Children])
end
local InternalConsumer = Roact.Component:extend("InternalConsumer")
function InternalConsumer:render()
return self.props.render(self.props.settings)
end
function InternalConsumer:didMount()
self.disconnect = self.props.settings:onUpdate(function()
-- Trigger a dummy state update to update the settings consumer.
self:setState({})
end)
end
function InternalConsumer:willUnmount()
self.disconnect()
end
local function with(callback)
return Roact.createElement(Context.Consumer, {
render = function(settings)
return Roact.createElement(InternalConsumer, {
settings = settings,
render = callback,
})
end,
})
end
return {
StudioProvider = StudioProvider,
with = with,
}

View File

@@ -1,209 +0,0 @@
local TextService = game:GetService("TextService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Settings = require(Plugin.Settings)
local Theme = require(Plugin.App.Theme)
local TextButton = require(Plugin.App.Components.TextButton)
local Header = require(Plugin.App.Components.Header)
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
local Tooltip = require(Plugin.App.Components.Tooltip)
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
local e = Roact.createElement
local ConfirmingPage = Roact.Component:extend("ConfirmingPage")
function ConfirmingPage:init()
self.contentSize, self.setContentSize = Roact.createBinding(0)
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
self:setState({
showingSourceDiff = false,
oldSource = "",
newSource = "",
})
end
function ConfirmingPage:render()
return Theme.with(function(theme)
local pageContent = Roact.createFragment({
Header = e(Header, {
transparency = self.props.transparency,
layoutOrder = 1,
}),
Title = e("TextLabel", {
Text = string.format(
"Sync changes for project '%s':",
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
),
LayoutOrder = 2,
Font = Enum.Font.Gotham,
LineHeight = 1.2,
TextSize = 14,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 20),
BackgroundTransparency = 1,
}),
PatchVisualizer = e(PatchVisualizer, {
size = UDim2.new(1, 0, 1, -150),
transparency = self.props.transparency,
layoutOrder = 3,
changeListHeaders = { "Property", "Current", "Incoming" },
patch = self.props.confirmData.patch,
instanceMap = self.props.confirmData.instanceMap,
showSourceDiff = function(oldSource: string, newSource: string)
self:setState({
showingSourceDiff = true,
oldSource = oldSource,
newSource = newSource,
})
end,
}),
Buttons = e("Frame", {
Size = UDim2.new(1, 0, 0, 34),
LayoutOrder = 4,
BackgroundTransparency = 1,
}, {
Abort = e(TextButton, {
text = "Abort",
style = "Bordered",
transparency = self.props.transparency,
layoutOrder = 1,
onClick = self.props.onAbort,
}, {
Tip = e(Tooltip.Trigger, {
text = "Stop the connection process",
}),
}),
Reject = if Settings:get("twoWaySync")
then e(TextButton, {
text = "Reject",
style = "Bordered",
transparency = self.props.transparency,
layoutOrder = 2,
onClick = self.props.onReject,
}, {
Tip = e(Tooltip.Trigger, {
text = "Push Studio changes to the Rojo server",
}),
})
else nil,
Accept = e(TextButton, {
text = "Accept",
style = "Solid",
transparency = self.props.transparency,
layoutOrder = 3,
onClick = self.props.onAccept,
}, {
Tip = e(Tooltip.Trigger, {
text = "Pull Rojo server changes to Studio",
}),
}),
Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Right,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
}),
Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
}),
SourceDiff = e(StudioPluginGui, {
id = "Rojo_ConfirmingSourceDiff",
title = "Source diff",
active = self.state.showingSourceDiff,
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({
showingSourceDiff = false,
})
end,
}, {
TooltipsProvider = e(Tooltip.Provider, nil, {
Tooltips = e(Tooltip.Container, nil),
Content = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
e(StringDiffVisualizer, {
size = UDim2.new(1, -10, 1, -10),
position = UDim2.new(0, 5, 0, 5),
anchorPoint = Vector2.new(0, 0),
transparency = self.props.transparency,
oldText = self.state.oldSource,
newText = self.state.newSource,
}),
}),
}),
}),
})
if self.props.createPopup then
return e(StudioPluginGui, {
id = "Rojo_DiffSync",
title = string.format(
"Confirm sync for project '%s':",
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
),
active = true,
isEphemeral = true,
initDockState = Enum.InitialDockState.Float,
overridePreviousState = false,
floatingSize = Vector2.new(500, 350),
minimumSize = Vector2.new(400, 250),
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
onClose = self.props.onAbort,
}, {
Tooltips = e(Tooltip.Container, nil),
Content = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, pageContent),
})
end
return pageContent
end)
end
return ConfirmingPage

View File

@@ -1,108 +1,17 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Flipper = require(Packages.Flipper)
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 StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
local Header = require(Plugin.App.Components.Header) local Header = require(Plugin.App.Components.Header)
local IconButton = require(Plugin.App.Components.IconButton) local IconButton = require(Plugin.App.Components.IconButton)
local TextButton = require(Plugin.App.Components.TextButton)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local Tooltip = require(Plugin.App.Components.Tooltip)
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
local e = Roact.createElement local e = Roact.createElement
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
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
-- so that it can still render during the fade out after disconnecting
self.serveSession = self.props.serveSession
end
function ChangesDrawer:render()
if self.props.rendered == false or self.serveSession == nil then
return nil
end
return Theme.with(function(theme)
return e(BorderedContainer, {
transparency = self.props.transparency,
size = self.props.height:map(function(y)
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,
}, {
Tip = e(Tooltip.Trigger, {
text = "Close the patch visualizer",
}),
}),
PatchVisualizer = e(PatchVisualizer, {
size = UDim2.new(1, 0, 1, 0),
transparency = self.props.transparency,
layoutOrder = 3,
patchTree = self.props.patchTree,
showSourceDiff = self.props.showSourceDiff,
}),
})
end)
end
local function ConnectionDetails(props) local function ConnectionDetails(props)
return Theme.with(function(theme) return Theme.with(function(theme)
return e(BorderedContainer, { return e(BorderedContainer, {
@@ -112,7 +21,7 @@ local function ConnectionDetails(props)
}, { }, {
TextContainer = e("Frame", { TextContainer = e("Frame", {
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1
}, { }, {
ProjectName = e("TextLabel", { ProjectName = e("TextLabel", {
Text = props.projectName, Text = props.projectName,
@@ -150,6 +59,18 @@ local function ConnectionDetails(props)
}), }),
}), }),
Disconnect = e(IconButton, {
icon = Assets.Images.Icons.Close,
iconSize = 24,
color = theme.ConnectionDetails.DisconnectColor,
transparency = props.transparency,
position = UDim2.new(1, 0, 0.5, 0),
anchorPoint = Vector2.new(1, 0.5),
onClick = props.onDisconnect,
}),
Padding = e("UIPadding", { Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17), PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15), PaddingRight = UDim.new(0, 15),
@@ -160,288 +81,34 @@ end
local ConnectedPage = Roact.Component:extend("ConnectedPage") local ConnectedPage = Roact.Component:extend("ConnectedPage")
function ConnectedPage:getChangeInfoText()
local patchData = self.props.patchData
if patchData == nil then
return ""
end
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
function ConnectedPage:startChangeInfoTextUpdater()
-- Cancel any existing updater
self:stopChangeInfoTextUpdater()
-- Start a new updater
self.changeInfoTextUpdater = task.defer(function()
while true do
if self.state.hoveringChangeInfo then
self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
else
self.setChangeInfoText(self:getChangeInfoText())
end
local elapsed = os.time() - self.props.patchData.timestamp
local updateInterval = 1
-- Update timestamp text as frequently as currently needed
for _, UnitData in ipairs(AGE_UNITS) do
local UnitSeconds = UnitData[1]
if elapsed > UnitSeconds then
updateInterval = UnitSeconds
break
end
end
task.wait(updateInterval)
end
end)
end
function ConnectedPage:stopChangeInfoTextUpdater()
if self.changeInfoTextUpdater then
task.cancel(self.changeInfoTextUpdater)
self.changeInfoTextUpdater = nil
end
end
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({
renderChanges = false,
hoveringChangeInfo = false,
showingSourceDiff = false,
oldSource = "",
newSource = "",
})
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
self:startChangeInfoTextUpdater()
end
function ConnectedPage:willUnmount()
self:stopChangeInfoTextUpdater()
end
function ConnectedPage:didUpdate(previousProps)
if self.props.patchData.timestamp ~= previousProps.patchData.timestamp then
-- New patch recieved
self:startChangeInfoTextUpdater()
self:setState({
showingSourceDiff = false,
})
end
end
function ConnectedPage:render() function ConnectedPage:render()
return Theme.with(function(theme) return Roact.createFragment({
return Roact.createFragment({ Header = e(Header, {
Padding = e("UIPadding", { transparency = self.props.transparency,
PaddingLeft = UDim.new(0, 20), layoutOrder = 1,
PaddingRight = UDim.new(0, 20), }),
}),
Layout = e("UIListLayout", { ConnectionDetails = e(ConnectionDetails, {
VerticalAlignment = Enum.VerticalAlignment.Center, projectName = self.state.projectName,
FillDirection = Enum.FillDirection.Vertical, address = self.state.address,
SortOrder = Enum.SortOrder.LayoutOrder, transparency = self.props.transparency,
Padding = UDim.new(0, 10), layoutOrder = 2,
}),
Header = e(Header, { onDisconnect = self.props.onDisconnect,
transparency = self.props.transparency, }),
layoutOrder = 1,
}),
ConnectionDetails = e(ConnectionDetails, { Layout = e("UIListLayout", {
projectName = self.state.projectName, VerticalAlignment = Enum.VerticalAlignment.Center,
address = self.state.address, FillDirection = Enum.FillDirection.Vertical,
transparency = self.props.transparency, SortOrder = Enum.SortOrder.LayoutOrder,
layoutOrder = 2, Padding = UDim.new(0, 10),
}),
onDisconnect = self.props.onDisconnect, Padding = e("UIPadding", {
}), PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
Buttons = e("Frame", { }),
Size = UDim2.new(1, 0, 0, 34), })
LayoutOrder = 3,
BackgroundTransparency = 1,
ZIndex = 2,
}, {
Settings = e(TextButton, {
text = "Settings",
style = "Bordered",
transparency = self.props.transparency,
layoutOrder = 1,
onClick = self.props.onNavigateSettings,
}, {
Tip = e(Tooltip.Trigger, {
text = "View and modify plugin settings",
}),
}),
Disconnect = e(TextButton, {
text = "Disconnect",
style = "Solid",
transparency = self.props.transparency,
layoutOrder = 2,
onClick = self.props.onDisconnect,
}, {
Tip = e(Tooltip.Trigger, {
text = "Disconnect from the Rojo sync server",
}),
}),
Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Right,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10),
}),
}),
ChangeInfo = e("TextButton", {
Text = self.changeInfoText,
Font = Enum.Font.Gotham,
TextSize = 14,
TextWrapped = true,
RichText = true,
TextColor3 = theme.Header.VersionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 28),
LayoutOrder = 4,
BackgroundTransparency = 1,
[Roact.Event.MouseEnter] = function()
self:setState({
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,
}, {
Tooltip = e(Tooltip.Trigger, {
text = if self.state.renderChanges then "Hide the changes" else "View the changes",
}),
}),
ChangesDrawer = e(ChangesDrawer, {
rendered = self.state.renderChanges,
transparency = self.props.transparency,
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,
initDockState = Enum.InitialDockState.Float,
overridePreviousState = false,
floatingSize = Vector2.new(500, 350),
minimumSize = Vector2.new(400, 250),
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
onClose = function()
self:setState({
showingSourceDiff = false,
})
end,
}, {
TooltipsProvider = e(Tooltip.Provider, nil, {
Tooltips = e(Tooltip.Container, nil),
Content = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
e(StringDiffVisualizer, {
size = UDim2.new(1, -10, 1, -10),
position = UDim2.new(0, 5, 0, 5),
anchorPoint = Vector2.new(0, 0),
transparency = self.props.transparency,
oldText = self.state.oldSource,
newText = self.state.newSource,
}),
}),
}),
}),
})
end)
end end
function ConnectedPage.getDerivedStateFromProps(props) function ConnectedPage.getDerivedStateFromProps(props)
@@ -455,4 +122,4 @@ function ConnectedPage.getDerivedStateFromProps(props)
} }
end end
return ConnectedPage return ConnectedPage

View File

@@ -1,8 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Spinner = require(Plugin.App.Components.Spinner) local Spinner = require(Plugin.App.Components.Spinner)
@@ -18,4 +17,4 @@ function ConnectingPage:render()
}) })
end end
return ConnectingPage return ConnectingPage

View File

@@ -2,16 +2,14 @@ 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 Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
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 BorderedContainer = require(Plugin.App.Components.BorderedContainer) local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local Tooltip = require(Plugin.App.Components.Tooltip)
local e = Roact.createElement local e = Roact.createElement
@@ -50,9 +48,7 @@ function Error:render()
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2 local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
local textBounds = TextService:GetTextSize( local textBounds = TextService:GetTextSize(
self.props.errorMessage, self.props.errorMessage, 16, Enum.Font.Code,
16,
Enum.Font.Code,
Vector2.new(containerSize.X, math.huge) Vector2.new(containerSize.X, math.huge)
) )
@@ -60,17 +56,8 @@ function Error:render()
end, end,
}, { }, {
ErrorMessage = Theme.with(function(theme) ErrorMessage = Theme.with(function(theme)
return e("TextBox", { return e("TextLabel", {
[Roact.Event.InputBegan] = function(rbx, input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
return
end
rbx.SelectionStart = 0
rbx.CursorPosition = #rbx.Text + 1
end,
Text = self.props.errorMessage, Text = self.props.errorMessage,
TextEditable = false,
Font = Enum.Font.Code, Font = Enum.Font.Code,
TextSize = 16, TextSize = 16,
TextColor3 = theme.ErrorColor, TextColor3 = theme.ErrorColor,
@@ -78,9 +65,10 @@ function Error:render()
TextYAlignment = Enum.TextYAlignment.Top, TextYAlignment = Enum.TextYAlignment.Top,
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
TextWrapped = true, TextWrapped = true,
ClearTextOnFocus = false,
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
}) })
end), end),
@@ -127,10 +115,6 @@ function ErrorPage:render()
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 1, layoutOrder = 1,
onClick = self.props.onClose, onClick = self.props.onClose,
}, {
Tip = e(Tooltip.Trigger, {
text = "Dismiss message",
}),
}), }),
Layout = e("UIListLayout", { Layout = e("UIListLayout", {
@@ -166,4 +150,4 @@ function ErrorPage.getDerivedStateFromProps(props)
} }
end end
return ErrorPage return ErrorPage

View File

@@ -1,8 +1,7 @@
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Rojo.Roact)
local Config = require(Plugin.Config) local Config = require(Plugin.Config)
@@ -10,7 +9,6 @@ local Theme = require(Plugin.App.Theme)
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)
local Header = require(Plugin.App.Components.Header) local Header = require(Plugin.App.Components.Header)
local Tooltip = require(Plugin.App.Components.Tooltip)
local PORT_WIDTH = 74 local PORT_WIDTH = 74
local DIVIDER_WIDTH = 1 local DIVIDER_WIDTH = 1
@@ -46,7 +44,7 @@ local function AddressEntry(props)
if props.onHostChange ~= nil then if props.onHostChange ~= nil then
props.onHostChange(object.Text) props.onHostChange(object.Text)
end end
end, end
}), }),
Port = e("TextBox", { Port = e("TextBox", {
@@ -110,7 +108,6 @@ function NotConnectedPage:render()
Size = UDim2.new(1, 0, 0, 34), Size = UDim2.new(1, 0, 0, 34),
LayoutOrder = 3, LayoutOrder = 3,
BackgroundTransparency = 1, BackgroundTransparency = 1,
ZIndex = 2,
}, { }, {
Settings = e(TextButton, { Settings = e(TextButton, {
text = "Settings", text = "Settings",
@@ -118,10 +115,6 @@ function NotConnectedPage:render()
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 1, layoutOrder = 1,
onClick = self.props.onNavigateSettings, onClick = self.props.onNavigateSettings,
}, {
Tip = e(Tooltip.Trigger, {
text = "View and modify plugin settings",
}),
}), }),
Connect = e(TextButton, { Connect = e(TextButton, {
@@ -130,10 +123,6 @@ function NotConnectedPage:render()
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 2, layoutOrder = 2,
onClick = self.props.onConnect, onClick = self.props.onConnect,
}, {
Tip = e(Tooltip.Trigger, {
text = "Connect to a Rojo sync server",
}),
}), }),
Layout = e("UIListLayout", { Layout = e("UIListLayout", {
@@ -159,4 +148,4 @@ function NotConnectedPage:render()
}) })
end end
return NotConnectedPage return NotConnectedPage

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