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