@el4cteo/rbx-studio-mcp 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge/rpc.js +73 -0
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/tools/input.js +38 -5
- package/dist/tools/input.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +1101 -998
- package/plugin/src/History.luau +187 -0
- package/plugin/src/Mirror.luau +145 -0
- package/plugin/src/ThemePicker.luau +458 -458
- package/plugin/src/Themes/Aurora.luau +161 -147
- package/plugin/src/Themes/Void.luau +266 -230
- package/plugin/src/Undo.luau +29 -0
- package/plugin/src/Visuals.luau +56 -6
- package/plugin/src/handlers/Device.luau +21 -1
- package/plugin/src/handlers/Input.luau +583 -493
- package/plugin/src/init.server.luau +260 -10
|
@@ -1,458 +1,458 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
The preset drawer: a tab on the panel's right edge that opens on hover.
|
|
4
|
-
|
|
5
|
-
Kept out of the header on purpose. The header is where connection facts live
|
|
6
|
-
and it is already carrying a status light, an address, a client badge and
|
|
7
|
-
three buttons; a ninth control there would push something off the edge on a
|
|
8
|
-
narrow dock. A theme is also not something anyone changes twice in a
|
|
9
|
-
session, so it has no claim on permanent width -- an edge tab costs fourteen
|
|
10
|
-
pixels and is invisible until you go looking for it.
|
|
11
|
-
|
|
12
|
-
Hover rather than click to open, because the drawer is a preview as much as
|
|
13
|
-
a menu: each row wears its own preset's background, text colour and cell
|
|
14
|
-
miniature, so running the pointer down the list shows you the eight themes
|
|
15
|
-
rather than eight names. Clicking picks; leaving closes.
|
|
16
|
-
|
|
17
|
-
The grace period on close is the part that makes it usable. Without it, the
|
|
18
|
-
pointer crossing the one-pixel gap between the tab and the drawer counts as
|
|
19
|
-
leaving, and the menu shuts under the hand reaching for it.
|
|
20
|
-
]]
|
|
21
|
-
|
|
22
|
-
local TweenService = game:GetService("TweenService")
|
|
23
|
-
|
|
24
|
-
local Themes = require(script.Parent.Themes)
|
|
25
|
-
|
|
26
|
-
local ThemePicker = {}
|
|
27
|
-
|
|
28
|
-
local TAB_WIDTH = 14
|
|
29
|
-
local TAB_HEIGHT = 48
|
|
30
|
-
local DRAWER_WIDTH = 196
|
|
31
|
-
local ROW_HEIGHT = 34
|
|
32
|
-
local PADDING = 6
|
|
33
|
-
|
|
34
|
-
-- Long enough to cross the gap between tab and drawer without thinking about
|
|
35
|
-
-- it, short enough that a drawer left open behind the pointer still closes.
|
|
36
|
-
local GRACE = 0.4
|
|
37
|
-
|
|
38
|
-
--[[
|
|
39
|
-
Explicit depths for every layer of the drawer.
|
|
40
|
-
|
|
41
|
-
A plugin widget draws with ZIndexBehavior.Global, where a child does NOT
|
|
42
|
-
inherit its parent's depth -- so the rows, built with the default ZIndex of
|
|
43
|
-
1, rendered UNDER the drawer's own background at 30 and the menu opened
|
|
44
|
-
empty. Nothing about that is visible in the code unless you already know the
|
|
45
|
-
rule, so the depths are named rather than sprinkled as numbers.
|
|
46
|
-
]]
|
|
47
|
-
local Z_DRAWER = 30
|
|
48
|
-
local Z_LIST = 31
|
|
49
|
-
local Z_ROW = 32
|
|
50
|
-
local Z_ROW_CONTENT = 33
|
|
51
|
-
local Z_TAB = 36
|
|
52
|
-
local Z_VEIL = 50
|
|
53
|
-
|
|
54
|
-
--[[
|
|
55
|
-
Stamps a depth on an Instance and everything inside it.
|
|
56
|
-
|
|
57
|
-
Needed because a preset draws its own swatch and has no reason to know it is
|
|
58
|
-
being drawn inside a menu. Rather than adding a ZIndex argument to the theme
|
|
59
|
-
contract -- which would put a menu detail into eight renderers -- the swatch
|
|
60
|
-
is drawn first and pushed to the right depth afterwards.
|
|
61
|
-
]]
|
|
62
|
-
local function stack(instance: Instance, depth: number)
|
|
63
|
-
if instance:IsA("GuiObject") then
|
|
64
|
-
instance.ZIndex = depth
|
|
65
|
-
end
|
|
66
|
-
for _, child in instance:GetDescendants() do
|
|
67
|
-
if child:IsA("GuiObject") then
|
|
68
|
-
child.ZIndex = depth
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
local OPEN = TweenInfo.new(0.22, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
|
|
74
|
-
local SHUT = TweenInfo.new(0.16, Enum.EasingStyle.Quint, Enum.EasingDirection.In)
|
|
75
|
-
|
|
76
|
-
type Runtime = {
|
|
77
|
-
drawer: Frame?,
|
|
78
|
-
tab: TextButton?,
|
|
79
|
-
chevron: TextLabel?,
|
|
80
|
-
rows: { { frame: Frame, id: string, marker: TextLabel } },
|
|
81
|
-
open: boolean,
|
|
82
|
-
-- Bumped on every hover, so a grace timer from an earlier one cannot close
|
|
83
|
-
-- a drawer the pointer has since come back to.
|
|
84
|
-
generation: number,
|
|
85
|
-
root: Frame?,
|
|
86
|
-
onPicked: ((string) -> ())?,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
local runtime: Runtime = {
|
|
90
|
-
drawer = nil,
|
|
91
|
-
tab = nil,
|
|
92
|
-
chevron = nil,
|
|
93
|
-
rows = {},
|
|
94
|
-
open = false,
|
|
95
|
-
generation = 0,
|
|
96
|
-
root = nil,
|
|
97
|
-
onPicked = nil,
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
local function drawerHeight(): number
|
|
101
|
-
return #Themes.list() * ROW_HEIGHT + PADDING * 2
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
local function setOpen(open: boolean)
|
|
105
|
-
local drawer = runtime.drawer
|
|
106
|
-
local chevron = runtime.chevron
|
|
107
|
-
if drawer == nil then
|
|
108
|
-
return
|
|
109
|
-
end
|
|
110
|
-
if runtime.open == open then
|
|
111
|
-
return
|
|
112
|
-
end
|
|
113
|
-
runtime.open = open
|
|
114
|
-
|
|
115
|
-
--[[
|
|
116
|
-
Size and position tween together, with the drawer anchored to its own
|
|
117
|
-
right edge at the tab's height. That is what makes it appear to grow out
|
|
118
|
-
of the tab rather than slide in from off-screen: at width zero it is
|
|
119
|
-
exactly where the pointer is.
|
|
120
|
-
]]
|
|
121
|
-
local height = drawerHeight()
|
|
122
|
-
TweenService:Create(drawer, if open then OPEN else SHUT, {
|
|
123
|
-
Size = UDim2.fromOffset(if open then DRAWER_WIDTH else 0, height),
|
|
124
|
-
}):Play()
|
|
125
|
-
|
|
126
|
-
if chevron then
|
|
127
|
-
-- Points the way it will move: into the panel when it will open, back
|
|
128
|
-
-- out at the edge when it will close.
|
|
129
|
-
TweenService:Create(chevron, if open then OPEN else SHUT, {
|
|
130
|
-
Rotation = if open then 180 else 0,
|
|
131
|
-
}):Play()
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
--[[
|
|
136
|
-
Closes after the grace period, unless the pointer came back.
|
|
137
|
-
]]
|
|
138
|
-
local function scheduleClose()
|
|
139
|
-
runtime.generation += 1
|
|
140
|
-
local generation = runtime.generation
|
|
141
|
-
task.delay(GRACE, function()
|
|
142
|
-
if runtime.generation == generation then
|
|
143
|
-
setOpen(false)
|
|
144
|
-
end
|
|
145
|
-
end)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
local function hold()
|
|
149
|
-
runtime.generation += 1
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
--[[
|
|
153
|
-
Fades the panel through its own background while the palette changes.
|
|
154
|
-
|
|
155
|
-
A theme switch repaints thirty Instances and re-renders three hundred log
|
|
156
|
-
rows, and doing that in one frame reads as a flicker -- the eye catches the
|
|
157
|
-
half-repainted state even though there is not one. A third of a second of
|
|
158
|
-
the outgoing background over the top hides the seam and, more usefully, says
|
|
159
|
-
that something deliberate just happened.
|
|
160
|
-
]]
|
|
161
|
-
local function crossfade(root: Frame, from: Color3, apply: () -> ())
|
|
162
|
-
local veil = Instance.new("Frame")
|
|
163
|
-
veil.Name = "ThemeFade"
|
|
164
|
-
veil.Size = UDim2.fromScale(1, 1)
|
|
165
|
-
veil.BackgroundColor3 = from
|
|
166
|
-
veil.BackgroundTransparency = 0.15
|
|
167
|
-
veil.BorderSizePixel = 0
|
|
168
|
-
veil.ZIndex = Z_VEIL
|
|
169
|
-
veil.Parent = root
|
|
170
|
-
|
|
171
|
-
apply()
|
|
172
|
-
|
|
173
|
-
local fade = TweenService:Create(
|
|
174
|
-
veil,
|
|
175
|
-
TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
|
|
176
|
-
{ BackgroundTransparency = 1 }
|
|
177
|
-
)
|
|
178
|
-
fade.Completed:Connect(function()
|
|
179
|
-
veil:Destroy()
|
|
180
|
-
end)
|
|
181
|
-
fade:Play()
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
local function markActive()
|
|
185
|
-
local activeId = Themes.activeId()
|
|
186
|
-
for _, row in runtime.rows do
|
|
187
|
-
row.marker.Text = if row.id == activeId then "\u{25C6}" else ""
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
--[[
|
|
192
|
-
One row: the preset's own background, its cell miniature, its name.
|
|
193
|
-
|
|
194
|
-
Drawn in the preset's palette rather than the console's, which is the whole
|
|
195
|
-
reason the drawer is worth opening. A list of eight words in the current
|
|
196
|
-
theme's colours would tell you nothing you could not have guessed.
|
|
197
|
-
]]
|
|
198
|
-
local function buildRow(parent: Instance, theme: Themes.Theme, order: number)
|
|
199
|
-
local palette = theme.palette
|
|
200
|
-
|
|
201
|
-
local row = Instance.new("TextButton")
|
|
202
|
-
row.Name = theme.id
|
|
203
|
-
row.Text = ""
|
|
204
|
-
row.AutoButtonColor = false
|
|
205
|
-
row.BackgroundColor3 = palette.background
|
|
206
|
-
row.BorderSizePixel = 0
|
|
207
|
-
row.Size = UDim2.new(1, 0, 0, ROW_HEIGHT - 2)
|
|
208
|
-
row.LayoutOrder = order
|
|
209
|
-
row.ZIndex = Z_ROW
|
|
210
|
-
row.Parent = parent
|
|
211
|
-
|
|
212
|
-
local corner = Instance.new("UICorner")
|
|
213
|
-
corner.CornerRadius = UDim.new(0, 3)
|
|
214
|
-
corner.Parent = row
|
|
215
|
-
|
|
216
|
-
local stroke = Instance.new("UIStroke")
|
|
217
|
-
stroke.Color = palette.rule
|
|
218
|
-
stroke.Transparency = 0.75
|
|
219
|
-
stroke.Parent = row
|
|
220
|
-
|
|
221
|
-
--[[
|
|
222
|
-
The miniature. Clipped, because a preset draws to the edges of whatever
|
|
223
|
-
it is given and a swatch that spills over the row would land on its
|
|
224
|
-
neighbour.
|
|
225
|
-
]]
|
|
226
|
-
local swatch = Instance.new("Frame")
|
|
227
|
-
swatch.Name = "Swatch"
|
|
228
|
-
swatch.AnchorPoint = Vector2.new(0, 0.5)
|
|
229
|
-
swatch.Position = UDim2.new(0, 6, 0.5, 0)
|
|
230
|
-
swatch.Size = UDim2.fromOffset(40, 24)
|
|
231
|
-
swatch.BackgroundColor3 = palette.surface
|
|
232
|
-
swatch.BorderSizePixel = 0
|
|
233
|
-
swatch.ClipsDescendants = true
|
|
234
|
-
swatch.ZIndex = Z_ROW_CONTENT
|
|
235
|
-
swatch.Parent = row
|
|
236
|
-
|
|
237
|
-
local swatchCorner = Instance.new("UICorner")
|
|
238
|
-
swatchCorner.CornerRadius = UDim.new(0, 2)
|
|
239
|
-
swatchCorner.Parent = swatch
|
|
240
|
-
|
|
241
|
-
-- A preset that cannot draw its own thumbnail must not take the menu with
|
|
242
|
-
-- it; an empty swatch is a bad row, a broken drawer is a broken plugin.
|
|
243
|
-
local ok, err = pcall(theme.swatch, swatch, palette)
|
|
244
|
-
if not ok then
|
|
245
|
-
warn(string.format("[rbx-studio] theme %q failed to draw a swatch: %s", theme.id, tostring(err)))
|
|
246
|
-
end
|
|
247
|
-
-- Whatever the preset just built is at depth 1 and would be invisible.
|
|
248
|
-
stack(swatch, Z_ROW_CONTENT)
|
|
249
|
-
|
|
250
|
-
local label = Instance.new("TextLabel")
|
|
251
|
-
label.Name = "Label"
|
|
252
|
-
label.BackgroundTransparency = 1
|
|
253
|
-
label.Position = UDim2.new(0, 52, 0, 4)
|
|
254
|
-
label.Size = UDim2.new(1, -70, 0, 13)
|
|
255
|
-
label.Font = Enum.Font.Code
|
|
256
|
-
label.TextSize = 12
|
|
257
|
-
label.TextColor3 = palette.text
|
|
258
|
-
label.TextXAlignment = Enum.TextXAlignment.Left
|
|
259
|
-
label.Text = theme.label
|
|
260
|
-
label.ZIndex = Z_ROW_CONTENT
|
|
261
|
-
label.Parent = row
|
|
262
|
-
|
|
263
|
-
local blurb = Instance.new("TextLabel")
|
|
264
|
-
blurb.Name = "Blurb"
|
|
265
|
-
blurb.BackgroundTransparency = 1
|
|
266
|
-
blurb.Position = UDim2.new(0, 52, 0, 16)
|
|
267
|
-
blurb.Size = UDim2.new(1, -70, 0, 12)
|
|
268
|
-
blurb.Font = Enum.Font.Code
|
|
269
|
-
blurb.TextSize = 10
|
|
270
|
-
blurb.TextColor3 = palette.dim
|
|
271
|
-
blurb.TextXAlignment = Enum.TextXAlignment.Left
|
|
272
|
-
blurb.TextTruncate = Enum.TextTruncate.AtEnd
|
|
273
|
-
blurb.Text = theme.blurb
|
|
274
|
-
blurb.ZIndex = Z_ROW_CONTENT
|
|
275
|
-
blurb.Parent = row
|
|
276
|
-
|
|
277
|
-
local marker = Instance.new("TextLabel")
|
|
278
|
-
marker.Name = "Marker"
|
|
279
|
-
marker.AnchorPoint = Vector2.new(1, 0.5)
|
|
280
|
-
marker.Position = UDim2.new(1, -6, 0.5, 0)
|
|
281
|
-
marker.Size = UDim2.fromOffset(12, 12)
|
|
282
|
-
marker.BackgroundTransparency = 1
|
|
283
|
-
marker.Font = Enum.Font.Code
|
|
284
|
-
marker.TextSize = 11
|
|
285
|
-
marker.TextColor3 = palette.rule
|
|
286
|
-
marker.Text = ""
|
|
287
|
-
marker.ZIndex = Z_ROW_CONTENT
|
|
288
|
-
marker.Parent = row
|
|
289
|
-
|
|
290
|
-
row.MouseEnter:Connect(function()
|
|
291
|
-
hold()
|
|
292
|
-
stroke.Transparency = 0.2
|
|
293
|
-
end)
|
|
294
|
-
row.MouseLeave:Connect(function()
|
|
295
|
-
stroke.Transparency = 0.75
|
|
296
|
-
end)
|
|
297
|
-
|
|
298
|
-
row.MouseButton1Click:Connect(function()
|
|
299
|
-
local root = runtime.root
|
|
300
|
-
if root == nil then
|
|
301
|
-
return
|
|
302
|
-
end
|
|
303
|
-
-- Re-picking the current theme is a no-op rather than a free repaint of
|
|
304
|
-
-- three hundred log rows.
|
|
305
|
-
if not Themes.use(theme.id) then
|
|
306
|
-
setOpen(false)
|
|
307
|
-
return
|
|
308
|
-
end
|
|
309
|
-
local previous = root.BackgroundColor3
|
|
310
|
-
setOpen(false)
|
|
311
|
-
crossfade(root, previous, function()
|
|
312
|
-
local handler = runtime.onPicked
|
|
313
|
-
if handler then
|
|
314
|
-
handler(theme.id)
|
|
315
|
-
end
|
|
316
|
-
end)
|
|
317
|
-
markActive()
|
|
318
|
-
end)
|
|
319
|
-
|
|
320
|
-
table.insert(runtime.rows, { frame = row, id = theme.id, marker = marker })
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
--[[
|
|
324
|
-
Builds the tab and the drawer.
|
|
325
|
-
|
|
326
|
-
`onPicked` is called with the new id AFTER `Themes.use` has switched, so the
|
|
327
|
-
caller repaints and persists rather than this module knowing how either is
|
|
328
|
-
done -- `plugin:SetSetting` is not reachable from a ModuleScript anyway.
|
|
329
|
-
]]
|
|
330
|
-
function ThemePicker.mount(root: Frame, onPicked: (string) -> ())
|
|
331
|
-
runtime.root = root
|
|
332
|
-
runtime.onPicked = onPicked
|
|
333
|
-
runtime.rows = {}
|
|
334
|
-
|
|
335
|
-
local palette = Themes.palette()
|
|
336
|
-
|
|
337
|
-
local drawer = Instance.new("Frame")
|
|
338
|
-
drawer.Name = "ThemeDrawer"
|
|
339
|
-
-- Anchored to its own right edge at the tab's height: at width zero it is a
|
|
340
|
-
-- line where the tab is, which is what it grows out of.
|
|
341
|
-
drawer.AnchorPoint = Vector2.new(1, 0.5)
|
|
342
|
-
drawer.Position = UDim2.new(1, -TAB_WIDTH, 0.5, 0)
|
|
343
|
-
drawer.Size = UDim2.fromOffset(0, drawerHeight())
|
|
344
|
-
drawer.BackgroundColor3 = palette.surface
|
|
345
|
-
drawer.BorderSizePixel = 0
|
|
346
|
-
drawer.ClipsDescendants = true
|
|
347
|
-
drawer.ZIndex = Z_DRAWER
|
|
348
|
-
drawer.Parent = root
|
|
349
|
-
runtime.drawer = drawer
|
|
350
|
-
|
|
351
|
-
local drawerCorner = Instance.new("UICorner")
|
|
352
|
-
drawerCorner.CornerRadius = UDim.new(0, 4)
|
|
353
|
-
drawerCorner.Parent = drawer
|
|
354
|
-
|
|
355
|
-
local drawerStroke = Instance.new("UIStroke")
|
|
356
|
-
drawerStroke.Color = palette.rule
|
|
357
|
-
drawerStroke.Transparency = 0.6
|
|
358
|
-
drawerStroke.Parent = drawer
|
|
359
|
-
|
|
360
|
-
local list = Instance.new("Frame")
|
|
361
|
-
list.Name = "Rows"
|
|
362
|
-
list.Position = UDim2.fromOffset(PADDING, PADDING)
|
|
363
|
-
-- Sized to the open width rather than to the parent, so the rows do not
|
|
364
|
-
-- squash and reflow through every frame of the tween.
|
|
365
|
-
list.Size = UDim2.fromOffset(DRAWER_WIDTH - PADDING * 2, drawerHeight() - PADDING * 2)
|
|
366
|
-
list.BackgroundTransparency = 1
|
|
367
|
-
list.ZIndex = Z_LIST
|
|
368
|
-
list.Parent = drawer
|
|
369
|
-
|
|
370
|
-
local layout = Instance.new("UIListLayout")
|
|
371
|
-
layout.FillDirection = Enum.FillDirection.Vertical
|
|
372
|
-
layout.Padding = UDim.new(0, 2)
|
|
373
|
-
layout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
374
|
-
layout.Parent = list
|
|
375
|
-
|
|
376
|
-
for order, theme in Themes.list() do
|
|
377
|
-
buildRow(list, theme, order)
|
|
378
|
-
end
|
|
379
|
-
markActive()
|
|
380
|
-
|
|
381
|
-
local tab = Instance.new("TextButton")
|
|
382
|
-
tab.Name = "ThemeTab"
|
|
383
|
-
tab.Text = ""
|
|
384
|
-
tab.AutoButtonColor = false
|
|
385
|
-
tab.AnchorPoint = Vector2.new(1, 0.5)
|
|
386
|
-
tab.Position = UDim2.new(1, 0, 0.5, 0)
|
|
387
|
-
tab.Size = UDim2.fromOffset(TAB_WIDTH, TAB_HEIGHT)
|
|
388
|
-
tab.BackgroundColor3 = palette.surface
|
|
389
|
-
tab.BorderSizePixel = 0
|
|
390
|
-
tab.ZIndex = Z_TAB
|
|
391
|
-
tab.Parent = root
|
|
392
|
-
runtime.tab = tab
|
|
393
|
-
|
|
394
|
-
local tabCorner = Instance.new("UICorner")
|
|
395
|
-
tabCorner.CornerRadius = UDim.new(0, 3)
|
|
396
|
-
tabCorner.Parent = tab
|
|
397
|
-
|
|
398
|
-
local chevron = Instance.new("TextLabel")
|
|
399
|
-
chevron.Name = "Chevron"
|
|
400
|
-
chevron.AnchorPoint = Vector2.new(0.5, 0.5)
|
|
401
|
-
chevron.Position = UDim2.fromScale(0.5, 0.5)
|
|
402
|
-
chevron.Size = UDim2.fromOffset(TAB_WIDTH, TAB_WIDTH)
|
|
403
|
-
chevron.BackgroundTransparency = 1
|
|
404
|
-
chevron.Font = Enum.Font.Code
|
|
405
|
-
chevron.TextSize = 12
|
|
406
|
-
chevron.TextColor3 = palette.rule
|
|
407
|
-
chevron.Text = "\u{25C0}"
|
|
408
|
-
chevron.ZIndex = Z_TAB + 1
|
|
409
|
-
chevron.Parent = tab
|
|
410
|
-
runtime.chevron = chevron
|
|
411
|
-
|
|
412
|
-
tab.MouseEnter:Connect(function()
|
|
413
|
-
hold()
|
|
414
|
-
setOpen(true)
|
|
415
|
-
end)
|
|
416
|
-
tab.MouseLeave:Connect(scheduleClose)
|
|
417
|
-
drawer.MouseEnter:Connect(hold)
|
|
418
|
-
drawer.MouseLeave:Connect(scheduleClose)
|
|
419
|
-
|
|
420
|
-
-- Clicking works too. Hover is the affordance, but a tab that only responds
|
|
421
|
-
-- to hover is unreachable to anyone driving Studio from the keyboard or a
|
|
422
|
-
-- tablet, and this costs one line.
|
|
423
|
-
tab.MouseButton1Click:Connect(function()
|
|
424
|
-
hold()
|
|
425
|
-
setOpen(not runtime.open)
|
|
426
|
-
end)
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
--[[
|
|
430
|
-
Recolours the tab and drawer chrome after a switch.
|
|
431
|
-
|
|
432
|
-
The ROWS are deliberately left alone: each one wears its own preset's
|
|
433
|
-
palette and always did, which is what makes the drawer a preview rather
|
|
434
|
-
than a list.
|
|
435
|
-
]]
|
|
436
|
-
function ThemePicker.applyTheme()
|
|
437
|
-
local palette = Themes.palette()
|
|
438
|
-
local drawer = runtime.drawer
|
|
439
|
-
local tab = runtime.tab
|
|
440
|
-
local chevron = runtime.chevron
|
|
441
|
-
|
|
442
|
-
if drawer then
|
|
443
|
-
drawer.BackgroundColor3 = palette.surface
|
|
444
|
-
local stroke = drawer:FindFirstChildOfClass("UIStroke")
|
|
445
|
-
if stroke then
|
|
446
|
-
stroke.Color = palette.rule
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
if tab then
|
|
450
|
-
tab.BackgroundColor3 = palette.surface
|
|
451
|
-
end
|
|
452
|
-
if chevron then
|
|
453
|
-
chevron.TextColor3 = palette.rule
|
|
454
|
-
end
|
|
455
|
-
markActive()
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
return ThemePicker
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
The preset drawer: a tab on the panel's right edge that opens on hover.
|
|
4
|
+
|
|
5
|
+
Kept out of the header on purpose. The header is where connection facts live
|
|
6
|
+
and it is already carrying a status light, an address, a client badge and
|
|
7
|
+
three buttons; a ninth control there would push something off the edge on a
|
|
8
|
+
narrow dock. A theme is also not something anyone changes twice in a
|
|
9
|
+
session, so it has no claim on permanent width -- an edge tab costs fourteen
|
|
10
|
+
pixels and is invisible until you go looking for it.
|
|
11
|
+
|
|
12
|
+
Hover rather than click to open, because the drawer is a preview as much as
|
|
13
|
+
a menu: each row wears its own preset's background, text colour and cell
|
|
14
|
+
miniature, so running the pointer down the list shows you the eight themes
|
|
15
|
+
rather than eight names. Clicking picks; leaving closes.
|
|
16
|
+
|
|
17
|
+
The grace period on close is the part that makes it usable. Without it, the
|
|
18
|
+
pointer crossing the one-pixel gap between the tab and the drawer counts as
|
|
19
|
+
leaving, and the menu shuts under the hand reaching for it.
|
|
20
|
+
]]
|
|
21
|
+
|
|
22
|
+
local TweenService = game:GetService("TweenService")
|
|
23
|
+
|
|
24
|
+
local Themes = require(script.Parent.Themes)
|
|
25
|
+
|
|
26
|
+
local ThemePicker = {}
|
|
27
|
+
|
|
28
|
+
local TAB_WIDTH = 14
|
|
29
|
+
local TAB_HEIGHT = 48
|
|
30
|
+
local DRAWER_WIDTH = 196
|
|
31
|
+
local ROW_HEIGHT = 34
|
|
32
|
+
local PADDING = 6
|
|
33
|
+
|
|
34
|
+
-- Long enough to cross the gap between tab and drawer without thinking about
|
|
35
|
+
-- it, short enough that a drawer left open behind the pointer still closes.
|
|
36
|
+
local GRACE = 0.4
|
|
37
|
+
|
|
38
|
+
--[[
|
|
39
|
+
Explicit depths for every layer of the drawer.
|
|
40
|
+
|
|
41
|
+
A plugin widget draws with ZIndexBehavior.Global, where a child does NOT
|
|
42
|
+
inherit its parent's depth -- so the rows, built with the default ZIndex of
|
|
43
|
+
1, rendered UNDER the drawer's own background at 30 and the menu opened
|
|
44
|
+
empty. Nothing about that is visible in the code unless you already know the
|
|
45
|
+
rule, so the depths are named rather than sprinkled as numbers.
|
|
46
|
+
]]
|
|
47
|
+
local Z_DRAWER = 30
|
|
48
|
+
local Z_LIST = 31
|
|
49
|
+
local Z_ROW = 32
|
|
50
|
+
local Z_ROW_CONTENT = 33
|
|
51
|
+
local Z_TAB = 36
|
|
52
|
+
local Z_VEIL = 50
|
|
53
|
+
|
|
54
|
+
--[[
|
|
55
|
+
Stamps a depth on an Instance and everything inside it.
|
|
56
|
+
|
|
57
|
+
Needed because a preset draws its own swatch and has no reason to know it is
|
|
58
|
+
being drawn inside a menu. Rather than adding a ZIndex argument to the theme
|
|
59
|
+
contract -- which would put a menu detail into eight renderers -- the swatch
|
|
60
|
+
is drawn first and pushed to the right depth afterwards.
|
|
61
|
+
]]
|
|
62
|
+
local function stack(instance: Instance, depth: number)
|
|
63
|
+
if instance:IsA("GuiObject") then
|
|
64
|
+
instance.ZIndex = depth
|
|
65
|
+
end
|
|
66
|
+
for _, child in instance:GetDescendants() do
|
|
67
|
+
if child:IsA("GuiObject") then
|
|
68
|
+
child.ZIndex = depth
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
local OPEN = TweenInfo.new(0.22, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
|
|
74
|
+
local SHUT = TweenInfo.new(0.16, Enum.EasingStyle.Quint, Enum.EasingDirection.In)
|
|
75
|
+
|
|
76
|
+
type Runtime = {
|
|
77
|
+
drawer: Frame?,
|
|
78
|
+
tab: TextButton?,
|
|
79
|
+
chevron: TextLabel?,
|
|
80
|
+
rows: { { frame: Frame, id: string, marker: TextLabel } },
|
|
81
|
+
open: boolean,
|
|
82
|
+
-- Bumped on every hover, so a grace timer from an earlier one cannot close
|
|
83
|
+
-- a drawer the pointer has since come back to.
|
|
84
|
+
generation: number,
|
|
85
|
+
root: Frame?,
|
|
86
|
+
onPicked: ((string) -> ())?,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
local runtime: Runtime = {
|
|
90
|
+
drawer = nil,
|
|
91
|
+
tab = nil,
|
|
92
|
+
chevron = nil,
|
|
93
|
+
rows = {},
|
|
94
|
+
open = false,
|
|
95
|
+
generation = 0,
|
|
96
|
+
root = nil,
|
|
97
|
+
onPicked = nil,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
local function drawerHeight(): number
|
|
101
|
+
return #Themes.list() * ROW_HEIGHT + PADDING * 2
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
local function setOpen(open: boolean)
|
|
105
|
+
local drawer = runtime.drawer
|
|
106
|
+
local chevron = runtime.chevron
|
|
107
|
+
if drawer == nil then
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
if runtime.open == open then
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
runtime.open = open
|
|
114
|
+
|
|
115
|
+
--[[
|
|
116
|
+
Size and position tween together, with the drawer anchored to its own
|
|
117
|
+
right edge at the tab's height. That is what makes it appear to grow out
|
|
118
|
+
of the tab rather than slide in from off-screen: at width zero it is
|
|
119
|
+
exactly where the pointer is.
|
|
120
|
+
]]
|
|
121
|
+
local height = drawerHeight()
|
|
122
|
+
TweenService:Create(drawer, if open then OPEN else SHUT, {
|
|
123
|
+
Size = UDim2.fromOffset(if open then DRAWER_WIDTH else 0, height),
|
|
124
|
+
}):Play()
|
|
125
|
+
|
|
126
|
+
if chevron then
|
|
127
|
+
-- Points the way it will move: into the panel when it will open, back
|
|
128
|
+
-- out at the edge when it will close.
|
|
129
|
+
TweenService:Create(chevron, if open then OPEN else SHUT, {
|
|
130
|
+
Rotation = if open then 180 else 0,
|
|
131
|
+
}):Play()
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
--[[
|
|
136
|
+
Closes after the grace period, unless the pointer came back.
|
|
137
|
+
]]
|
|
138
|
+
local function scheduleClose()
|
|
139
|
+
runtime.generation += 1
|
|
140
|
+
local generation = runtime.generation
|
|
141
|
+
task.delay(GRACE, function()
|
|
142
|
+
if runtime.generation == generation then
|
|
143
|
+
setOpen(false)
|
|
144
|
+
end
|
|
145
|
+
end)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
local function hold()
|
|
149
|
+
runtime.generation += 1
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
--[[
|
|
153
|
+
Fades the panel through its own background while the palette changes.
|
|
154
|
+
|
|
155
|
+
A theme switch repaints thirty Instances and re-renders three hundred log
|
|
156
|
+
rows, and doing that in one frame reads as a flicker -- the eye catches the
|
|
157
|
+
half-repainted state even though there is not one. A third of a second of
|
|
158
|
+
the outgoing background over the top hides the seam and, more usefully, says
|
|
159
|
+
that something deliberate just happened.
|
|
160
|
+
]]
|
|
161
|
+
local function crossfade(root: Frame, from: Color3, apply: () -> ())
|
|
162
|
+
local veil = Instance.new("Frame")
|
|
163
|
+
veil.Name = "ThemeFade"
|
|
164
|
+
veil.Size = UDim2.fromScale(1, 1)
|
|
165
|
+
veil.BackgroundColor3 = from
|
|
166
|
+
veil.BackgroundTransparency = 0.15
|
|
167
|
+
veil.BorderSizePixel = 0
|
|
168
|
+
veil.ZIndex = Z_VEIL
|
|
169
|
+
veil.Parent = root
|
|
170
|
+
|
|
171
|
+
apply()
|
|
172
|
+
|
|
173
|
+
local fade = TweenService:Create(
|
|
174
|
+
veil,
|
|
175
|
+
TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
|
|
176
|
+
{ BackgroundTransparency = 1 }
|
|
177
|
+
)
|
|
178
|
+
fade.Completed:Connect(function()
|
|
179
|
+
veil:Destroy()
|
|
180
|
+
end)
|
|
181
|
+
fade:Play()
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
local function markActive()
|
|
185
|
+
local activeId = Themes.activeId()
|
|
186
|
+
for _, row in runtime.rows do
|
|
187
|
+
row.marker.Text = if row.id == activeId then "\u{25C6}" else ""
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
--[[
|
|
192
|
+
One row: the preset's own background, its cell miniature, its name.
|
|
193
|
+
|
|
194
|
+
Drawn in the preset's palette rather than the console's, which is the whole
|
|
195
|
+
reason the drawer is worth opening. A list of eight words in the current
|
|
196
|
+
theme's colours would tell you nothing you could not have guessed.
|
|
197
|
+
]]
|
|
198
|
+
local function buildRow(parent: Instance, theme: Themes.Theme, order: number)
|
|
199
|
+
local palette = theme.palette
|
|
200
|
+
|
|
201
|
+
local row = Instance.new("TextButton")
|
|
202
|
+
row.Name = theme.id
|
|
203
|
+
row.Text = ""
|
|
204
|
+
row.AutoButtonColor = false
|
|
205
|
+
row.BackgroundColor3 = palette.background
|
|
206
|
+
row.BorderSizePixel = 0
|
|
207
|
+
row.Size = UDim2.new(1, 0, 0, ROW_HEIGHT - 2)
|
|
208
|
+
row.LayoutOrder = order
|
|
209
|
+
row.ZIndex = Z_ROW
|
|
210
|
+
row.Parent = parent
|
|
211
|
+
|
|
212
|
+
local corner = Instance.new("UICorner")
|
|
213
|
+
corner.CornerRadius = UDim.new(0, 3)
|
|
214
|
+
corner.Parent = row
|
|
215
|
+
|
|
216
|
+
local stroke = Instance.new("UIStroke")
|
|
217
|
+
stroke.Color = palette.rule
|
|
218
|
+
stroke.Transparency = 0.75
|
|
219
|
+
stroke.Parent = row
|
|
220
|
+
|
|
221
|
+
--[[
|
|
222
|
+
The miniature. Clipped, because a preset draws to the edges of whatever
|
|
223
|
+
it is given and a swatch that spills over the row would land on its
|
|
224
|
+
neighbour.
|
|
225
|
+
]]
|
|
226
|
+
local swatch = Instance.new("Frame")
|
|
227
|
+
swatch.Name = "Swatch"
|
|
228
|
+
swatch.AnchorPoint = Vector2.new(0, 0.5)
|
|
229
|
+
swatch.Position = UDim2.new(0, 6, 0.5, 0)
|
|
230
|
+
swatch.Size = UDim2.fromOffset(40, 24)
|
|
231
|
+
swatch.BackgroundColor3 = palette.surface
|
|
232
|
+
swatch.BorderSizePixel = 0
|
|
233
|
+
swatch.ClipsDescendants = true
|
|
234
|
+
swatch.ZIndex = Z_ROW_CONTENT
|
|
235
|
+
swatch.Parent = row
|
|
236
|
+
|
|
237
|
+
local swatchCorner = Instance.new("UICorner")
|
|
238
|
+
swatchCorner.CornerRadius = UDim.new(0, 2)
|
|
239
|
+
swatchCorner.Parent = swatch
|
|
240
|
+
|
|
241
|
+
-- A preset that cannot draw its own thumbnail must not take the menu with
|
|
242
|
+
-- it; an empty swatch is a bad row, a broken drawer is a broken plugin.
|
|
243
|
+
local ok, err = pcall(theme.swatch, swatch, palette)
|
|
244
|
+
if not ok then
|
|
245
|
+
warn(string.format("[rbx-studio] theme %q failed to draw a swatch: %s", theme.id, tostring(err)))
|
|
246
|
+
end
|
|
247
|
+
-- Whatever the preset just built is at depth 1 and would be invisible.
|
|
248
|
+
stack(swatch, Z_ROW_CONTENT)
|
|
249
|
+
|
|
250
|
+
local label = Instance.new("TextLabel")
|
|
251
|
+
label.Name = "Label"
|
|
252
|
+
label.BackgroundTransparency = 1
|
|
253
|
+
label.Position = UDim2.new(0, 52, 0, 4)
|
|
254
|
+
label.Size = UDim2.new(1, -70, 0, 13)
|
|
255
|
+
label.Font = Enum.Font.Code
|
|
256
|
+
label.TextSize = 12
|
|
257
|
+
label.TextColor3 = palette.text
|
|
258
|
+
label.TextXAlignment = Enum.TextXAlignment.Left
|
|
259
|
+
label.Text = theme.label
|
|
260
|
+
label.ZIndex = Z_ROW_CONTENT
|
|
261
|
+
label.Parent = row
|
|
262
|
+
|
|
263
|
+
local blurb = Instance.new("TextLabel")
|
|
264
|
+
blurb.Name = "Blurb"
|
|
265
|
+
blurb.BackgroundTransparency = 1
|
|
266
|
+
blurb.Position = UDim2.new(0, 52, 0, 16)
|
|
267
|
+
blurb.Size = UDim2.new(1, -70, 0, 12)
|
|
268
|
+
blurb.Font = Enum.Font.Code
|
|
269
|
+
blurb.TextSize = 10
|
|
270
|
+
blurb.TextColor3 = palette.dim
|
|
271
|
+
blurb.TextXAlignment = Enum.TextXAlignment.Left
|
|
272
|
+
blurb.TextTruncate = Enum.TextTruncate.AtEnd
|
|
273
|
+
blurb.Text = theme.blurb
|
|
274
|
+
blurb.ZIndex = Z_ROW_CONTENT
|
|
275
|
+
blurb.Parent = row
|
|
276
|
+
|
|
277
|
+
local marker = Instance.new("TextLabel")
|
|
278
|
+
marker.Name = "Marker"
|
|
279
|
+
marker.AnchorPoint = Vector2.new(1, 0.5)
|
|
280
|
+
marker.Position = UDim2.new(1, -6, 0.5, 0)
|
|
281
|
+
marker.Size = UDim2.fromOffset(12, 12)
|
|
282
|
+
marker.BackgroundTransparency = 1
|
|
283
|
+
marker.Font = Enum.Font.Code
|
|
284
|
+
marker.TextSize = 11
|
|
285
|
+
marker.TextColor3 = palette.rule
|
|
286
|
+
marker.Text = ""
|
|
287
|
+
marker.ZIndex = Z_ROW_CONTENT
|
|
288
|
+
marker.Parent = row
|
|
289
|
+
|
|
290
|
+
row.MouseEnter:Connect(function()
|
|
291
|
+
hold()
|
|
292
|
+
stroke.Transparency = 0.2
|
|
293
|
+
end)
|
|
294
|
+
row.MouseLeave:Connect(function()
|
|
295
|
+
stroke.Transparency = 0.75
|
|
296
|
+
end)
|
|
297
|
+
|
|
298
|
+
row.MouseButton1Click:Connect(function()
|
|
299
|
+
local root = runtime.root
|
|
300
|
+
if root == nil then
|
|
301
|
+
return
|
|
302
|
+
end
|
|
303
|
+
-- Re-picking the current theme is a no-op rather than a free repaint of
|
|
304
|
+
-- three hundred log rows.
|
|
305
|
+
if not Themes.use(theme.id) then
|
|
306
|
+
setOpen(false)
|
|
307
|
+
return
|
|
308
|
+
end
|
|
309
|
+
local previous = root.BackgroundColor3
|
|
310
|
+
setOpen(false)
|
|
311
|
+
crossfade(root, previous, function()
|
|
312
|
+
local handler = runtime.onPicked
|
|
313
|
+
if handler then
|
|
314
|
+
handler(theme.id)
|
|
315
|
+
end
|
|
316
|
+
end)
|
|
317
|
+
markActive()
|
|
318
|
+
end)
|
|
319
|
+
|
|
320
|
+
table.insert(runtime.rows, { frame = row, id = theme.id, marker = marker })
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
--[[
|
|
324
|
+
Builds the tab and the drawer.
|
|
325
|
+
|
|
326
|
+
`onPicked` is called with the new id AFTER `Themes.use` has switched, so the
|
|
327
|
+
caller repaints and persists rather than this module knowing how either is
|
|
328
|
+
done -- `plugin:SetSetting` is not reachable from a ModuleScript anyway.
|
|
329
|
+
]]
|
|
330
|
+
function ThemePicker.mount(root: Frame, onPicked: (string) -> ())
|
|
331
|
+
runtime.root = root
|
|
332
|
+
runtime.onPicked = onPicked
|
|
333
|
+
runtime.rows = {}
|
|
334
|
+
|
|
335
|
+
local palette = Themes.palette()
|
|
336
|
+
|
|
337
|
+
local drawer = Instance.new("Frame")
|
|
338
|
+
drawer.Name = "ThemeDrawer"
|
|
339
|
+
-- Anchored to its own right edge at the tab's height: at width zero it is a
|
|
340
|
+
-- line where the tab is, which is what it grows out of.
|
|
341
|
+
drawer.AnchorPoint = Vector2.new(1, 0.5)
|
|
342
|
+
drawer.Position = UDim2.new(1, -TAB_WIDTH, 0.5, 0)
|
|
343
|
+
drawer.Size = UDim2.fromOffset(0, drawerHeight())
|
|
344
|
+
drawer.BackgroundColor3 = palette.surface
|
|
345
|
+
drawer.BorderSizePixel = 0
|
|
346
|
+
drawer.ClipsDescendants = true
|
|
347
|
+
drawer.ZIndex = Z_DRAWER
|
|
348
|
+
drawer.Parent = root
|
|
349
|
+
runtime.drawer = drawer
|
|
350
|
+
|
|
351
|
+
local drawerCorner = Instance.new("UICorner")
|
|
352
|
+
drawerCorner.CornerRadius = UDim.new(0, 4)
|
|
353
|
+
drawerCorner.Parent = drawer
|
|
354
|
+
|
|
355
|
+
local drawerStroke = Instance.new("UIStroke")
|
|
356
|
+
drawerStroke.Color = palette.rule
|
|
357
|
+
drawerStroke.Transparency = 0.6
|
|
358
|
+
drawerStroke.Parent = drawer
|
|
359
|
+
|
|
360
|
+
local list = Instance.new("Frame")
|
|
361
|
+
list.Name = "Rows"
|
|
362
|
+
list.Position = UDim2.fromOffset(PADDING, PADDING)
|
|
363
|
+
-- Sized to the open width rather than to the parent, so the rows do not
|
|
364
|
+
-- squash and reflow through every frame of the tween.
|
|
365
|
+
list.Size = UDim2.fromOffset(DRAWER_WIDTH - PADDING * 2, drawerHeight() - PADDING * 2)
|
|
366
|
+
list.BackgroundTransparency = 1
|
|
367
|
+
list.ZIndex = Z_LIST
|
|
368
|
+
list.Parent = drawer
|
|
369
|
+
|
|
370
|
+
local layout = Instance.new("UIListLayout")
|
|
371
|
+
layout.FillDirection = Enum.FillDirection.Vertical
|
|
372
|
+
layout.Padding = UDim.new(0, 2)
|
|
373
|
+
layout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
374
|
+
layout.Parent = list
|
|
375
|
+
|
|
376
|
+
for order, theme in Themes.list() do
|
|
377
|
+
buildRow(list, theme, order)
|
|
378
|
+
end
|
|
379
|
+
markActive()
|
|
380
|
+
|
|
381
|
+
local tab = Instance.new("TextButton")
|
|
382
|
+
tab.Name = "ThemeTab"
|
|
383
|
+
tab.Text = ""
|
|
384
|
+
tab.AutoButtonColor = false
|
|
385
|
+
tab.AnchorPoint = Vector2.new(1, 0.5)
|
|
386
|
+
tab.Position = UDim2.new(1, 0, 0.5, 0)
|
|
387
|
+
tab.Size = UDim2.fromOffset(TAB_WIDTH, TAB_HEIGHT)
|
|
388
|
+
tab.BackgroundColor3 = palette.surface
|
|
389
|
+
tab.BorderSizePixel = 0
|
|
390
|
+
tab.ZIndex = Z_TAB
|
|
391
|
+
tab.Parent = root
|
|
392
|
+
runtime.tab = tab
|
|
393
|
+
|
|
394
|
+
local tabCorner = Instance.new("UICorner")
|
|
395
|
+
tabCorner.CornerRadius = UDim.new(0, 3)
|
|
396
|
+
tabCorner.Parent = tab
|
|
397
|
+
|
|
398
|
+
local chevron = Instance.new("TextLabel")
|
|
399
|
+
chevron.Name = "Chevron"
|
|
400
|
+
chevron.AnchorPoint = Vector2.new(0.5, 0.5)
|
|
401
|
+
chevron.Position = UDim2.fromScale(0.5, 0.5)
|
|
402
|
+
chevron.Size = UDim2.fromOffset(TAB_WIDTH, TAB_WIDTH)
|
|
403
|
+
chevron.BackgroundTransparency = 1
|
|
404
|
+
chevron.Font = Enum.Font.Code
|
|
405
|
+
chevron.TextSize = 12
|
|
406
|
+
chevron.TextColor3 = palette.rule
|
|
407
|
+
chevron.Text = "\u{25C0}"
|
|
408
|
+
chevron.ZIndex = Z_TAB + 1
|
|
409
|
+
chevron.Parent = tab
|
|
410
|
+
runtime.chevron = chevron
|
|
411
|
+
|
|
412
|
+
tab.MouseEnter:Connect(function()
|
|
413
|
+
hold()
|
|
414
|
+
setOpen(true)
|
|
415
|
+
end)
|
|
416
|
+
tab.MouseLeave:Connect(scheduleClose)
|
|
417
|
+
drawer.MouseEnter:Connect(hold)
|
|
418
|
+
drawer.MouseLeave:Connect(scheduleClose)
|
|
419
|
+
|
|
420
|
+
-- Clicking works too. Hover is the affordance, but a tab that only responds
|
|
421
|
+
-- to hover is unreachable to anyone driving Studio from the keyboard or a
|
|
422
|
+
-- tablet, and this costs one line.
|
|
423
|
+
tab.MouseButton1Click:Connect(function()
|
|
424
|
+
hold()
|
|
425
|
+
setOpen(not runtime.open)
|
|
426
|
+
end)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
--[[
|
|
430
|
+
Recolours the tab and drawer chrome after a switch.
|
|
431
|
+
|
|
432
|
+
The ROWS are deliberately left alone: each one wears its own preset's
|
|
433
|
+
palette and always did, which is what makes the drawer a preview rather
|
|
434
|
+
than a list.
|
|
435
|
+
]]
|
|
436
|
+
function ThemePicker.applyTheme()
|
|
437
|
+
local palette = Themes.palette()
|
|
438
|
+
local drawer = runtime.drawer
|
|
439
|
+
local tab = runtime.tab
|
|
440
|
+
local chevron = runtime.chevron
|
|
441
|
+
|
|
442
|
+
if drawer then
|
|
443
|
+
drawer.BackgroundColor3 = palette.surface
|
|
444
|
+
local stroke = drawer:FindFirstChildOfClass("UIStroke")
|
|
445
|
+
if stroke then
|
|
446
|
+
stroke.Color = palette.rule
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
if tab then
|
|
450
|
+
tab.BackgroundColor3 = palette.surface
|
|
451
|
+
end
|
|
452
|
+
if chevron then
|
|
453
|
+
chevron.TextColor3 = palette.rule
|
|
454
|
+
end
|
|
455
|
+
markActive()
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
return ThemePicker
|