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