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 };
117
118 const borderColor = {
119 primary: sample(ramps.neutral, isLight ? 1.5 : 0),
120 secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
121 muted: sample(ramps.neutral, isLight ? 1 : 3),
122 active: sample(ramps.neutral, isLight ? 4 : 3),
123 onMedia: withOpacity(darkest, 0.1),
124 ok: sample(ramps.green, 0.3),
125 error: sample(ramps.red, 0.3),
126 warning: sample(ramps.yellow, 0.3),
127 info: sample(ramps.blue, 0.3),
128 };
129
130 const textColor = {
131 primary: sample(ramps.neutral, 6),
132 secondary: sample(ramps.neutral, 5),
133 muted: sample(ramps.neutral, 4),
134 placeholder: sample(ramps.neutral, 3),
135 active: sample(ramps.neutral, 7),
136 feature: sample(ramps.blue, 0.5),
137 ok: sample(ramps.green, 0.5),
138 error: sample(ramps.red, 0.5),
139 warning: sample(ramps.yellow, 0.5),
140 success: sample(ramps.green, 0.5),
141 info: sample(ramps.blue, 0.5),
142 onMedia: darkest,
143 };
144
145 const player = {
146 1: buildPlayer(sample(ramps.blue, 0.5)),
147 2: buildPlayer(sample(ramps.green, 0.5)),
148 3: buildPlayer(sample(ramps.magenta, 0.5)),
149 4: buildPlayer(sample(ramps.orange, 0.5)),
150 5: buildPlayer(sample(ramps.violet, 0.5)),
151 6: buildPlayer(sample(ramps.cyan, 0.5)),
152 7: buildPlayer(sample(ramps.red, 0.5)),
153 8: buildPlayer(sample(ramps.yellow, 0.5)),
154 };
155
156 const editor = {
157 background: backgroundColor[500].base,
158 indent_guide: borderColor.muted,
159 indent_guide_active: borderColor.secondary,
160 line: {
161 active: sample(ramps.neutral, 1),
162 highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
163 },
164 highlight: {
165 selection: player[1].selectionColor,
166 occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
167 activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
168 matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
169 match: sample(ramps.violet, 0.15),
170 activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
171 related: backgroundColor[500].hovered,
172 },
173 gutter: {
174 primary: textColor.placeholder,
175 active: textColor.active,
176 },
177 };
178
179 const syntax: Syntax = {
180 primary: {
181 color: sample(ramps.neutral, 7),
182 weight: fontWeights.normal,
183 },
184 comment: {
185 color: sample(ramps.neutral, 5),
186 weight: fontWeights.normal,
187 },
188 punctuation: {
189 color: sample(ramps.neutral, 6),
190 weight: fontWeights.normal,
191 },
192 constant: {
193 color: sample(ramps.neutral, 4),
194 weight: fontWeights.normal,
195 },
196 keyword: {
197 color: sample(ramps.blue, 0.5),
198 weight: fontWeights.normal,
199 },
200 function: {
201 color: sample(ramps.yellow, 0.5),
202 weight: fontWeights.normal,
203 },
204 type: {
205 color: sample(ramps.cyan, 0.5),
206 weight: fontWeights.normal,
207 },
208 constructor: {
209 color: sample(ramps.blue, 0.5),
210 weight: fontWeights.normal,
211 },
212 variant: {
213 color: sample(ramps.blue, 0.5),
214 weight: fontWeights.normal,
215 },
216 property: {
217 color: sample(ramps.blue, 0.5),
218 weight: fontWeights.normal,
219 },
220 enum: {
221 color: sample(ramps.orange, 0.5),
222 weight: fontWeights.normal,
223 },
224 operator: {
225 color: sample(ramps.orange, 0.5),
226 weight: fontWeights.normal,
227 },
228 string: {
229 color: sample(ramps.orange, 0.5),
230 weight: fontWeights.normal,
231 },
232 number: {
233 color: sample(ramps.green, 0.5),
234 weight: fontWeights.normal,
235 },
236 boolean: {
237 color: sample(ramps.green, 0.5),
238 weight: fontWeights.normal,
239 },
240 predictive: {
241 color: textColor.muted,
242 weight: fontWeights.normal,
243 },
244 title: {
245 color: sample(ramps.yellow, 0.5),
246 weight: fontWeights.bold,
247 },
248 emphasis: {
249 color: textColor.feature,
250 weight: fontWeights.normal,
251 },
252 "emphasis.strong": {
253 color: textColor.feature,
254 weight: fontWeights.bold,
255 },
256 linkUri: {
257 color: sample(ramps.green, 0.5),
258 weight: fontWeights.normal,
259 underline: true,
260 },
261 linkText: {
262 color: sample(ramps.orange, 0.5),
263 weight: fontWeights.normal,
264 italic: true,
265 },
266 };
267
268 const shadow = withOpacity(
269 ramps
270 .neutral(isLight ? 7 : 0)
271 .darken()
272 .hex(),
273 blend
274 );
275
276 return {
277 name,
278 isLight,
279 backgroundColor,
280 borderColor,
281 textColor,
282 iconColor: textColor,
283 editor,
284 syntax,
285 player,
286 shadow,
287 ramps,
288 };
289}