1import chroma, { Color, Scale } from "chroma-js";
2import { fontWeights } from "../../common";
3import { withOpacity } from "../../utils/color";
4import Theme, { buildPlayer, Syntax } from "./theme";
5
6export function colorRamp(color: Color): Scale {
7 let hue = color.hsl()[0];
8 let endColor = chroma.hsl(hue, 0.88, 0.96);
9 let startColor = chroma.hsl(hue, 0.68, 0.12);
10 return chroma.scale([startColor, color, endColor]).mode("hsl");
11}
12
13export function createTheme(
14 name: string,
15 isLight: boolean,
16 color_ramps: { [rampName: string]: Scale }
17): Theme {
18 let ramps: typeof color_ramps = {};
19 // Chromajs mutates the underlying ramp when you call domain. This causes problems because
20 // we now store the ramps object in the theme so that we can pull colors out of them.
21 // So instead of calling domain and storing the result, we have to construct new ramps for each
22 // theme so that we don't modify the passed in ramps.
23 // This combined with an error in the type definitions for chroma js means we have to cast the colors
24 // function to any in order to get the colors back out from the original ramps.
25 if (isLight) {
26 for (var rampName in color_ramps) {
27 ramps[rampName] = chroma
28 .scale((color_ramps[rampName].colors as any)())
29 .domain([1, 0]);
30 }
31 ramps.neutral = chroma
32 .scale((color_ramps.neutral.colors as any)())
33 .domain([7, 0]);
34 } else {
35 for (var rampName in color_ramps) {
36 ramps[rampName] = chroma
37 .scale((color_ramps[rampName].colors as any)())
38 .domain([0, 1]);
39 }
40 ramps.neutral = chroma
41 .scale((color_ramps.neutral.colors as any)())
42 .domain([0, 7]);
43 }
44
45 let blend = isLight ? 0.12 : 0.24;
46
47 function sample(ramp: Scale, index: number): string {
48 return ramp(index).hex();
49 }
50 const darkest = ramps.neutral(isLight ? 7 : 0).hex();
51
52 const backgroundColor = {
53 // Title bar
54 100: {
55 base: sample(ramps.neutral, 1.25),
56 hovered: sample(ramps.neutral, 1.5),
57 active: sample(ramps.neutral, 1.75),
58 },
59 // Midground (panels, etc)
60 300: {
61 base: sample(ramps.neutral, 1),
62 hovered: sample(ramps.neutral, 1.25),
63 active: sample(ramps.neutral, 1.5),
64 },
65 // Editor
66 500: {
67 base: sample(ramps.neutral, 0),
68 hovered: sample(ramps.neutral, 0.25),
69 active: sample(ramps.neutral, 0.5),
70 },
71 on300: {
72 base: sample(ramps.neutral, 0),
73 hovered: sample(ramps.neutral, 0.5),
74 active: sample(ramps.neutral, 1),
75 },
76 on500: {
77 base: sample(ramps.neutral, 1),
78 hovered: sample(ramps.neutral, 1.5),
79 active: sample(ramps.neutral, 2),
80 },
81 ok: {
82 base: withOpacity(sample(ramps.green, 0.5), 0.15),
83 hovered: withOpacity(sample(ramps.green, 0.5), 0.2),
84 active: withOpacity(sample(ramps.green, 0.5), 0.25),
85 },
86 error: {
87 base: withOpacity(sample(ramps.red, 0.5), 0.15),
88 hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
89 active: withOpacity(sample(ramps.red, 0.5), 0.25),
90 },
91 on500Error: {
92 base: sample(ramps.red, 0.05),
93 hovered: sample(ramps.red, 0.1),
94 active: sample(ramps.red, 0.15),
95 },
96 warning: {
97 base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
98 hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
99 active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
100 },
101 on500Warning: {
102 base: sample(ramps.yellow, 0.05),
103 hovered: sample(ramps.yellow, 0.1),
104 active: sample(ramps.yellow, 0.15),
105 },
106 info: {
107 base: withOpacity(sample(ramps.blue, 0.5), 0.15),
108 hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
109 active: withOpacity(sample(ramps.blue, 0.5), 0.25),
110 },
111 on500Info: {
112 base: sample(ramps.blue, 0.05),
113 hovered: sample(ramps.blue, 0.1),
114 active: sample(ramps.blue, 0.15),
115 },
116 on500Ok: {
117 base: sample(ramps.green, 0.05),
118 hovered: sample(ramps.green, 0.1),
119 active: sample(ramps.green, 0.15),
120 },
121 };
122
123 const borderColor = {
124 primary: sample(ramps.neutral, isLight ? 1.5 : 0),
125 secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
126 muted: sample(ramps.neutral, isLight ? 1.25 : 3),
127 active: sample(ramps.neutral, isLight ? 4 : 3),
128 onMedia: withOpacity(darkest, 0.1),
129 ok: sample(ramps.green, 0.3),
130 error: sample(ramps.red, 0.3),
131 warning: sample(ramps.yellow, 0.3),
132 info: sample(ramps.blue, 0.3),
133 };
134
135 const textColor = {
136 primary: sample(ramps.neutral, 6),
137 secondary: sample(ramps.neutral, 5),
138 muted: sample(ramps.neutral, 4),
139 placeholder: sample(ramps.neutral, 3),
140 active: sample(ramps.neutral, 7),
141 feature: sample(ramps.blue, 0.5),
142 ok: sample(ramps.green, 0.5),
143 error: sample(ramps.red, 0.5),
144 warning: sample(ramps.yellow, 0.5),
145 info: sample(ramps.blue, 0.5),
146 onMedia: darkest,
147 };
148
149 const player = {
150 1: buildPlayer(sample(ramps.blue, 0.5)),
151 2: buildPlayer(sample(ramps.green, 0.5)),
152 3: buildPlayer(sample(ramps.magenta, 0.5)),
153 4: buildPlayer(sample(ramps.orange, 0.5)),
154 5: buildPlayer(sample(ramps.violet, 0.5)),
155 6: buildPlayer(sample(ramps.cyan, 0.5)),
156 7: buildPlayer(sample(ramps.red, 0.5)),
157 8: buildPlayer(sample(ramps.yellow, 0.5)),
158 };
159
160 const editor = {
161 background: backgroundColor[500].base,
162 indent_guide: borderColor.muted,
163 indent_guide_active: borderColor.secondary,
164 line: {
165 active: sample(ramps.neutral, 1),
166 highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
167 },
168 highlight: {
169 selection: player[1].selectionColor,
170 occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
171 activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
172 matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
173 match: sample(ramps.violet, 0.15),
174 activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
175 related: backgroundColor[500].hovered,
176 },
177 gutter: {
178 primary: textColor.placeholder,
179 active: textColor.active,
180 },
181 };
182
183 const syntax: Syntax = {
184 primary: {
185 color: sample(ramps.neutral, 7),
186 weight: fontWeights.normal,
187 },
188 "variable.special": {
189 color: sample(ramps.blue, 0.8),
190 weight: fontWeights.normal,
191 },
192 comment: {
193 color: sample(ramps.neutral, 5),
194 weight: fontWeights.normal,
195 },
196 punctuation: {
197 color: sample(ramps.neutral, 6),
198 weight: fontWeights.normal,
199 },
200 constant: {
201 color: sample(ramps.neutral, 4),
202 weight: fontWeights.normal,
203 },
204 keyword: {
205 color: sample(ramps.blue, 0.5),
206 weight: fontWeights.normal,
207 },
208 function: {
209 color: sample(ramps.yellow, 0.5),
210 weight: fontWeights.normal,
211 },
212 type: {
213 color: sample(ramps.cyan, 0.5),
214 weight: fontWeights.normal,
215 },
216 constructor: {
217 color: sample(ramps.cyan, 0.5),
218 weight: fontWeights.normal,
219 },
220 property: {
221 color: sample(ramps.blue, 0.6),
222 weight: fontWeights.normal,
223 },
224 enum: {
225 color: sample(ramps.orange, 0.5),
226 weight: fontWeights.normal,
227 },
228 operator: {
229 color: sample(ramps.orange, 0.5),
230 weight: fontWeights.normal,
231 },
232 string: {
233 color: sample(ramps.orange, 0.5),
234 weight: fontWeights.normal,
235 },
236 number: {
237 color: sample(ramps.green, 0.5),
238 weight: fontWeights.normal,
239 },
240 boolean: {
241 color: sample(ramps.green, 0.5),
242 weight: fontWeights.normal,
243 },
244 predictive: {
245 color: textColor.muted,
246 weight: fontWeights.normal,
247 },
248 title: {
249 color: sample(ramps.yellow, 0.5),
250 weight: fontWeights.bold,
251 },
252 emphasis: {
253 color: textColor.feature,
254 weight: fontWeights.normal,
255 },
256 "emphasis.strong": {
257 color: textColor.feature,
258 weight: fontWeights.bold,
259 },
260 linkUri: {
261 color: sample(ramps.green, 0.5),
262 weight: fontWeights.normal,
263 underline: true,
264 },
265 linkText: {
266 color: sample(ramps.orange, 0.5),
267 weight: fontWeights.normal,
268 italic: true,
269 },
270 };
271
272 const shadow = withOpacity(
273 ramps
274 .neutral(isLight ? 7 : 0)
275 .darken()
276 .hex(),
277 blend
278 );
279
280 return {
281 name,
282 isLight,
283 backgroundColor,
284 borderColor,
285 textColor,
286 iconColor: textColor,
287 editor,
288 syntax,
289 player,
290 shadow,
291 ramps,
292 };
293}