create_theme.ts

  1import { Scale, Color } from "chroma-js"
  2import {
  3    ThemeConfig,
  4    ThemeAppearance,
  5    ThemeConfigInputColors,
  6} from "./theme_config"
  7import { get_ramps } from "./ramps"
  8import { syntaxStyle } from "./syntax"
  9import { Syntax } from "../types/syntax"
 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: Syntax
 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 { name, appearance, input_color } = theme
119
120    const is_light = appearance === ThemeAppearance.Light
121    const color_ramps: ThemeConfigInputColors = input_color
122
123    // Chromajs scales from 0 to 1 flipped if is_light is true
124    const ramps = get_ramps(is_light, color_ramps)
125    const lowest = lowest_layer(ramps)
126    const middle = middle_layer(ramps)
127    const highest = highest_layer(ramps)
128
129    const popover_shadow = {
130        blur: 4,
131        color: ramps
132            .neutral(is_light ? 7 : 0)
133            .darken()
134            .alpha(0.2)
135            .hex(), // TODO used blend previously. Replace with something else
136        offset: [1, 2],
137    }
138
139    const modal_shadow = {
140        blur: 16,
141        color: ramps
142            .neutral(is_light ? 7 : 0)
143            .darken()
144            .alpha(0.2)
145            .hex(), // TODO used blend previously. Replace with something else
146        offset: [0, 2],
147    }
148
149    const players = {
150        "0": player(ramps.blue),
151        "1": player(ramps.green),
152        "2": player(ramps.magenta),
153        "3": player(ramps.orange),
154        "4": player(ramps.violet),
155        "5": player(ramps.cyan),
156        "6": player(ramps.red),
157        "7": player(ramps.yellow),
158    }
159
160    const syntax = syntaxStyle(
161        ramps,
162        theme.override.syntax ? theme.override.syntax : {}
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}