1import chroma, { Scale, Color } from "chroma-js"
2import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
3export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
4import {
5 ThemeConfig,
6 ThemeAppearance,
7 ThemeConfigInputColors,
8} from "./theme_config"
9import { get_ramps } from "./ramps"
10
11export interface Theme {
12 name: string
13 is_light: boolean
14
15 /**
16 * App background, other elements that should sit directly on top of the background.
17 */
18 lowest: Layer
19 /**
20 * Panels, tabs, other UI surfaces that sit on top of the background.
21 */
22 middle: Layer
23 /**
24 * Editors like code buffers, conversation editors, etc.
25 */
26 highest: Layer
27
28 ramps: RampSet
29
30 popover_shadow: Shadow
31 modal_shadow: Shadow
32
33 players: Players
34 syntax?: Partial<ThemeSyntax>
35 color_family: ColorFamily
36}
37
38export interface Meta {
39 name: string
40 author: string
41 url: string
42 license: License
43}
44
45export interface License {
46 SPDX: SPDXExpression
47}
48
49// License name -> License text
50export interface Licenses {
51 [key: string]: string
52}
53
54// FIXME: Add support for the SPDX expression syntax
55export type SPDXExpression = "MIT"
56
57export interface Player {
58 cursor: string
59 selection: string
60}
61
62export interface Players {
63 "0": Player
64 "1": Player
65 "2": Player
66 "3": Player
67 "4": Player
68 "5": Player
69 "6": Player
70 "7": Player
71}
72
73export interface ColorFamily {
74 range: ColorFamilyRange
75}
76
77export type ColorFamilyRange = Partial<{ [K in keyof RampSet]: number }>
78
79export interface Shadow {
80 blur: number
81 color: string
82 offset: number[]
83}
84
85export type StyleSets = keyof Layer
86export interface Layer {
87 base: StyleSet
88 variant: StyleSet
89 on: StyleSet
90 accent: StyleSet
91 positive: StyleSet
92 warning: StyleSet
93 negative: StyleSet
94}
95
96export interface RampSet {
97 neutral: Scale
98 red: Scale
99 orange: Scale
100 yellow: Scale
101 green: Scale
102 cyan: Scale
103 blue: Scale
104 violet: Scale
105 magenta: Scale
106}
107
108export type Styles = keyof StyleSet
109export interface StyleSet {
110 default: Style
111 active: Style
112 disabled: Style
113 hovered: Style
114 pressed: Style
115 inverted: Style
116}
117
118export interface Style {
119 background: string
120 border: string
121 foreground: string
122}
123
124export function create_theme(theme: ThemeConfig): Theme {
125 const {
126 name,
127 appearance,
128 input_color,
129 override: { syntax },
130 } = theme
131
132 const is_light = appearance === ThemeAppearance.Light
133 const color_ramps: ThemeConfigInputColors = input_color
134
135 // Chromajs scales from 0 to 1 flipped if is_light is true
136 const ramps = get_ramps(is_light, color_ramps)
137 const lowest = lowest_layer(ramps)
138 const middle = middle_layer(ramps)
139 const highest = highest_layer(ramps)
140
141 const popover_shadow = {
142 blur: 4,
143 color: ramps
144 .neutral(is_light ? 7 : 0)
145 .darken()
146 .alpha(0.2)
147 .hex(), // TODO used blend previously. Replace with something else
148 offset: [1, 2],
149 }
150
151 const modal_shadow = {
152 blur: 16,
153 color: ramps
154 .neutral(is_light ? 7 : 0)
155 .darken()
156 .alpha(0.2)
157 .hex(), // TODO used blend previously. Replace with something else
158 offset: [0, 2],
159 }
160
161 const players = {
162 "0": player(ramps.blue),
163 "1": player(ramps.green),
164 "2": player(ramps.magenta),
165 "3": player(ramps.orange),
166 "4": player(ramps.violet),
167 "5": player(ramps.cyan),
168 "6": player(ramps.red),
169 "7": player(ramps.yellow),
170 }
171
172 const color_family = {
173 range: build_color_family(ramps)
174 }
175
176 return {
177 name,
178 is_light,
179
180 ramps,
181
182 lowest,
183 middle,
184 highest,
185
186 popover_shadow,
187 modal_shadow,
188
189 players,
190 syntax,
191 color_family,
192 }
193}
194
195function player(ramp: Scale): Player {
196 return {
197 selection: ramp(0.5).alpha(0.24).hex(),
198 cursor: ramp(0.5).hex(),
199 }
200}
201
202function build_color_family(ramps: RampSet): ColorFamilyRange {
203 const color_family: ColorFamilyRange = {}
204
205 for (const ramp in ramps) {
206 color_family[ramp as keyof RampSet] = chroma(ramps[ramp as keyof RampSet](0.5)).luminance()
207 }
208
209 return color_family
210}
211
212function lowest_layer(ramps: RampSet): Layer {
213 return {
214 base: build_style_set(ramps.neutral, 0.2, 1),
215 variant: build_style_set(ramps.neutral, 0.2, 0.7),
216 on: build_style_set(ramps.neutral, 0.1, 1),
217 accent: build_style_set(ramps.blue, 0.1, 0.5),
218 positive: build_style_set(ramps.green, 0.1, 0.5),
219 warning: build_style_set(ramps.yellow, 0.1, 0.5),
220 negative: build_style_set(ramps.red, 0.1, 0.5),
221 }
222}
223
224function middle_layer(ramps: RampSet): Layer {
225 return {
226 base: build_style_set(ramps.neutral, 0.1, 1),
227 variant: build_style_set(ramps.neutral, 0.1, 0.7),
228 on: build_style_set(ramps.neutral, 0, 1),
229 accent: build_style_set(ramps.blue, 0.1, 0.5),
230 positive: build_style_set(ramps.green, 0.1, 0.5),
231 warning: build_style_set(ramps.yellow, 0.1, 0.5),
232 negative: build_style_set(ramps.red, 0.1, 0.5),
233 }
234}
235
236function highest_layer(ramps: RampSet): Layer {
237 return {
238 base: build_style_set(ramps.neutral, 0, 1),
239 variant: build_style_set(ramps.neutral, 0, 0.7),
240 on: build_style_set(ramps.neutral, 0.1, 1),
241 accent: build_style_set(ramps.blue, 0.1, 0.5),
242 positive: build_style_set(ramps.green, 0.1, 0.5),
243 warning: build_style_set(ramps.yellow, 0.1, 0.5),
244 negative: build_style_set(ramps.red, 0.1, 0.5),
245 }
246}
247
248function build_style_set(
249 ramp: Scale,
250 background_base: number,
251 foreground_base: number,
252 step = 0.08
253): StyleSet {
254 const style_definitions = build_style_definition(
255 background_base,
256 foreground_base,
257 step
258 )
259
260 function color_string(index_or_color: number | Color): string {
261 if (typeof index_or_color === "number") {
262 return ramp(index_or_color).hex()
263 } else {
264 return index_or_color.hex()
265 }
266 }
267
268 function build_style(style: Styles): Style {
269 return {
270 background: color_string(style_definitions.background[style]),
271 border: color_string(style_definitions.border[style]),
272 foreground: color_string(style_definitions.foreground[style]),
273 }
274 }
275
276 return {
277 default: build_style("default"),
278 hovered: build_style("hovered"),
279 pressed: build_style("pressed"),
280 active: build_style("active"),
281 disabled: build_style("disabled"),
282 inverted: build_style("inverted"),
283 }
284}
285
286function build_style_definition(bg_base: number, fg_base: number, step = 0.08) {
287 return {
288 background: {
289 default: bg_base,
290 hovered: bg_base + step,
291 pressed: bg_base + step * 1.5,
292 active: bg_base + step * 2.2,
293 disabled: bg_base,
294 inverted: fg_base + step * 6,
295 },
296 border: {
297 default: bg_base + step * 1,
298 hovered: bg_base + step,
299 pressed: bg_base + step,
300 active: bg_base + step * 3,
301 disabled: bg_base + step * 0.5,
302 inverted: bg_base - step * 3,
303 },
304 foreground: {
305 default: fg_base,
306 hovered: fg_base,
307 pressed: fg_base,
308 active: fg_base + step * 6,
309 disabled: bg_base + step * 4,
310 inverted: bg_base + step * 2,
311 },
312 }
313}