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