create_theme.ts

  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}